Projects

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#

WORKFLOW.md
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 URL

In the Workflow tab, Settings → Branch & PR has the same options. At hand-off offers four choices:

ChoiceWrites
Open a draft PR / MRpush: true, pull_request: draft
Open a PR / MRpush: true, pull_request: ready
Push the branch onlypush: true, pull_request: off
Keep it in this repopush: false, pull_request: off
Note

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#

WhenWhat happens
First dispatchThe 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 attemptThe workspace's origin/<base> is brought up to date, so the agent can rebase.
After every attempt, and before any cleanupThe 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-offThe 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#

RemoteHow
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 elseThe 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.

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 with fetch.fsckObjects before importing.
  • With Cloudflare, git runs in the Cloudflare sandbox and branches come back as verified bundles too.
  • The Workspaces and Board screens read HEAD and refs as plain files and never run git in a workspace. Workspaces whose .git is a symlink, a gitdir: 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, your base, and anything under release/ or hotfix/. 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:

YAML
hooks:
  after_create: |
    git clone --quiet "$SYMPHONY_WORKFLOW_DIR" .
    git checkout -b "$SYMPHONY_ISSUE_BRANCH"
  before_run: git fetch --quiet origin || true

When 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.

See also#