Branches and pull requests
How SI Fabrik clones each task's workspace, saves the agent's branch back into your repository, and opens a pull request at hand-off.
With managed git (the default), every task gets its own branch, symphony/<task-id>. SI Fabrik clones it into the task's workspace, saves it back into your project repository after every attempt so work is never lost, and at hand-off pushes it and opens a pull request or merge request. The agent only commits; it never pushes or opens PRs itself. In Docker sandboxes, git push to github.com is refused outright. See Read-only GitHub and package registries.
Configure it#
git:
base: main # default: the repository's current branch
save: true # save the task branch into this repo after every attempt
push: true # push it at hand-off
remote: origin
pull_request: draft # draft | ready | off
# provider: github # github | gitlab | none; default: detected from the remote URLIn the Workflow tab, Settings → Branch & PR has the same options. At hand-off offers four choices:
| Choice | Writes |
|---|---|
| Open a draft PR / MR | push: true, pull_request: draft |
| Open a PR / MR | push: true, pull_request: ready |
| Push the branch only | push: true, pull_request: off |
| Keep it in this repo | push: false, pull_request: off |
A pull request needs the branch on the remote, so any pull_request other than off turns push on. Without a git: block at all, branches are saved but not pushed.
The four moments#
| When | What happens |
|---|---|
| First dispatch | The project is cloned into the workspace with git clone --no-local, so no git objects are hard-linked with your repository. The task branch starts from base, or resumes from a saved copy if one exists. |
| Before each attempt | The workspace's origin/<base> is brought up to date, so the agent can rebase. |
| After every attempt, and before any cleanup | The task branch is saved into your repository as refs/heads/symphony/<task-id>. This only ever fast-forwards. If the branch has diverged, or is checked out in your repository, the work is kept under refs/harness/saved/… instead. A workspace whose branch couldn't be saved is never deleted. |
| Hand-off | The branch is pushed to remote, without forcing. A PR or MR is opened, or the existing one updated, and its link is posted on the task. |
You can also save a branch by hand: open a task on the board and press Save Branch in its Workspace section. It fast-forwards only, and runs sandboxed when Docker is on.
How pull requests are opened#
| Remote | How |
|---|---|
GitHub (github.com, or the host in GH_HOST) | gh pr create --draft (or without --draft for ready). The body is the agent's hand-off summary with secrets scrubbed, plus a link to the task. |
GitLab (gitlab.com, or hosts named gitlab.*) | GitLab push options: merge_request.create, target, title, description. Uses your normal push credentials; no CLI needed. Draft MRs get a Draft: title prefix. |
| Anything else | The branch is pushed; you open the PR yourself. Or set git.provider. |
If the task filed a linked task that's still open at hand-off, its pull request is opened as a draft and says what it depends on.
Push credentials#
SI Fabrik uses your own git and gh login on the Mac. Nothing to configure if git push and gh pr create already work in your terminal.
You can set HARNESS_GITHUB_TOKEN or HARNESS_GITLAB_TOKEN in Settings → Environment to use a dedicated token instead.
Set a token in secrets.env (or as a server variable from the app):
# Fine-grained token limited to the project repos:
# Contents: read/write, Pull requests: read/write. Requires `gh` on the server for PRs.
HARNESS_GITHUB_TOKEN=github_pat_…
# HARNESS_GITLAB_TOKEN=glpat-…These tokens are stripped from every agent and hook environment. Git gets them through a credential helper that reads the environment, so they never appear in a command line or on disk.
Security model#
An agent can rewrite anything in its workspace, including .git/config and .git/hooks. Running git on the host inside that repository would execute whatever the agent planted there (an fsmonitor, hooks, uploadpack overrides), which would be a way out of the sandbox. So SI Fabrik follows these rules:
- Host-side git runs only in your project repository, with hooks and fsmonitor disabled,
GIT_CONFIG_NOSYSTEM, and no prompts (ssh -o BatchMode=yes). - With Docker on, git that touches the workspace runs in a throwaway container: no network, a read-only root filesystem, all capabilities dropped,
no-new-privileges, and pid and memory limits. The workspace is mounted read-only for saves. Objects cross the boundary as git bundles, which the host checks withfetch.fsckObjectsbefore importing. - With Cloudflare, git runs in the Cloudflare sandbox and branches come back as verified bundles too.
- The Workspaces and Board screens read
HEADand refs as plain files and never run git in a workspace. Workspaces whose.gitis a symlink, agitdir:file, or has alternates are refused. - Branch names are validated: no option-like or path-traversal names. Protected branches are never task branches:
main,master,develop,development,trunk,production,prod,staging,release, yourbase, and anything underrelease/orhotfix/. SI Fabrik never force-pushes. - Text published for the agent is scrubbed for known secret values and token patterns: PR bodies, MR descriptions and task comments.
Old default hooks#
Earlier versions generated these hooks:
hooks:
after_create: |
git clone --quiet "$SYMPHONY_WORKFLOW_DIR" .
git checkout -b "$SYMPHONY_ISSUE_BRANCH"
before_run: git fetch --quiet origin || trueWhen SI Fabrik finds them exactly as generated, it ignores them and uses managed git. Any other after_create hook turns managed git off (git.manage_workspace defaults to false then), and the inspector warns when hooks run on the host with Docker on.