$ backgroundclaude
blog · 2026-04-10 · 7 min read

git worktree for Claude Code: the isolation pattern every background agent uses

If you've run two Claude Code sessions at once on the same repo, you've already had the bug. One session checks out feature-A, the other checks out feature-B, they both call npm install, one of them clobbers package-lock.json, and suddenly the first run is committing against a tree it never saw. The fix is as old as git and nobody uses it until they have to: git worktree.

The problem, in one failure mode

A repo has exactly one working directory and exactly one .git/index. Every git checkout, every git add, every git commit mutates them. If two processes do any of those concurrently, you get one of the more colorful categories of git errors:

fatal: Unable to create '.git/index.lock': File exists.
Another git process seems to be running in this repository...

Or worse — no error at all, just a commit on the wrong branch because the second process raced the first's checkout. In a background-agent setup where runs are triggered by webhooks, that means a typo one morning manifests as two PRs against each other's branches by afternoon.

What git worktree actually is

A git worktree is a second working directory attached to the same repo but with its own checkout and its own HEAD. Same history, same refs, same objects in .git/objects — but a different filesystem layout, on a different branch, with its own staging area. Every action inside one worktree is invisible to the others.

This has existed since git 2.5 (2015). It was designed for humans who wanted to review a PR without losing their WIP on another branch. It turns out to be the exact primitive background agents need.

The three commands you'll actually use

# Create a new worktree at ../ws-ENG-1234 on a new branch
git worktree add ../ws-ENG-1234 -b feature/ENG-1234

# List all worktrees attached to this repo
git worktree list

# Remove a worktree once the run is done (safe — leaves branch alone)
git worktree remove ../ws-ENG-1234

That's the whole API for our purposes. Create, list, remove. Every other git worktree subcommand is nice-to-have.

Why it's the background agent pattern

A background agent processing issues looks roughly like this:

webhook arrives → extract issue ID → spawn a run
         ↓
  create an isolated workspace
         ↓
  run claude -p inside it
         ↓
  git push, open PR, clean up

The word “isolated” is doing a lot of work there. You need isolation on three axes simultaneously:

A fresh git clone gives you all three but at the cost of re-downloading the whole history every time, which means seconds-to-minutes of latency per run and gigabytes of disk for a busy day. A git worktree gives you all three and shares the object database, so the cost of a new workspace is basically one checkout plus an npm install.

The Cyrus pattern, verbatim

From the ceedaragents/cyrus README:

Cyrus monitors (Linear|Github|GitLab|Slack) issues assigned to it, creates isolated Git worktrees for each issue, runs (Claude Code|Codex|Cursor|Gemini) sessions to process them, and streams detailed agent activity updates back to (Linear|Github), along with rich interactions like dropdown selects and approvals.

“Creates isolated Git worktrees for each issue” is the line that makes the whole loop safe to run in parallel. Strip the rest of the sentence and you have a feature. Strip that phrase and you have a race condition waiting for a busy morning.

Pitfalls nobody warns you about

node_modules aren't free

Worktrees share .git/ but NOT the working tree — which means node_modulesis a fresh install in every worktree. If your repo has 10k dependencies, that's a significant per-run cost. Options: use pnpm (hardlinked store, much cheaper), cache an install across worktrees with a symlink, or accept the cost as the price of isolation.

Lock files race at install time

Two worktrees running npm install simultaneously will race over the global npm cache lock. npm handles this with retries, but slowly. pnpm and yarn berry have smarter cache locking. Or serialize installs at the agent level with a host-wide flock.

Branches are shared state

A worktree gives you filesystem isolation but NOT branch isolation if you're not careful. git worktree add without -b checks out an existing branch, and a branch can only be checked out in one worktree at a time. If run A is using feature/ENG-1234 and run B tries to check out the same branch, B fails. Always create a new branch per worktree — use -b feature/ENG-1234 or a uniquified branch name like agent/ENG-1234-20260410.

Cleanup on failure

If the agent crashes mid-run, the worktree is orphaned: it sits on disk, its branch is “checked out” from git's perspective, and a retry of the same issue fails because the branch is locked. Two defences: git worktree prune in a periodic sweep removes orphans whose directories have been deleted; and wrap every run in a try/finally that calls git worktree remove even on crash.

A minimal worktree-per-run shell recipe

#!/usr/bin/env bash
set -euo pipefail

ISSUE_ID="$1"          # e.g. ENG-1234
PROMPT="$2"            # the issue body

REPO=/srv/repo
WORKTREES=/srv/worktrees
WS="$WORKTREES/$ISSUE_ID-$(date +%s)"
BRANCH="agent/$ISSUE_ID"

# Make sure the base repo is current
git -C "$REPO" fetch --all --prune
git -C "$REPO" worktree prune  # clean up orphans

# Create the isolated workspace
git -C "$REPO" worktree add "$WS" -b "$BRANCH" origin/main

# Always clean up, even if claude crashes
trap 'git -C "$REPO" worktree remove --force "$WS" || true' EXIT

cd "$WS"
npm ci

claude --bare -p "$PROMPT" \
  --allowedTools "Bash(git:*),Bash(npm test),Read,Edit,Grep" \
  --max-turns 80 \
  --max-budget-usd 5 \
  --output-format stream-json \
  --verbose --include-partial-messages \
  | tee "claude-$ISSUE_ID.ndjson"

git push origin "$BRANCH"
gh pr create --title "[agent] $ISSUE_ID" --body "See run log."

The trap is the important line. Without it, the happy path cleans up and the crash path leaks.

What this gets you, concretely

Where this leaves you

You now know the isolation primitive. What you don't have yet is the rest of a background agent: the webhook handler that triggers each run, the OAuth flow for whichever issue tracker is the source of truth, the two-way sync that posts results back, the approval gate for destructive tool calls, the audit log of who ran what. Worktree is ~5% of that pie. The rest is months of glue nobody enjoys writing.

That's the whole background agent on Linear loop. Worktree is inside it. So is everything else.

isolation, scaled

Cyrus creates isolated git worktrees for each issue.

Plus the OAuth, the webhook signatures, the streaming events back to the issue tracker, the approvals, the audit log — all the parts of the loop that aren't fun to write. BYOK across Claude, Codex, Cursor, and Gemini. Community self-hosted is free forever.

Try Cyrus free →