Git Worktrees
Per-task git worktree isolation gives every active task its own checkout on its own branch, so parallel agents never trample each other's working tree. Claiming a task materializes a worktree; finishing it auto-commits, merges back to trunk, and prunes the directory.
Typical flow
- Set
MULTITERMINAL_WORKTREE_MODE=on(a startup env var — see below) and launch MultiTerminal. - Claim / activate a task → you get an isolated checkout on branch
task/<id>under.claude/worktrees/. - Do the work there — your edits never touch the main checkout or any other task's tree.
- Mark the task done → MultiTerminal auto-commits, merges the branch back to trunk, and prunes the worktree directory.
On This Page
Why worktrees
A git worktree is a second working directory backed by the same repository — a separate checkout on a separate branch that shares the one .git store. MultiTerminal uses one worktree per active task so that:
- Two agents working two tasks in the same repo never see each other's uncommitted edits.
- Each task's commits land on a dedicated
task/<id>branch, keeping trunk clean until the task is done. - A helper joining a task gets its own branch and worktree, so helper work integrates deliberately rather than racing the assignee's tree.
Worktrees live inside the project at .claude/worktrees/ (gitignored). Keeping them under the main checkout matters for Claude Code's permission scope, harness cwd pinning, and the EnterWorktree enter-existing form (see below).
Turning it on (WORKTREE_MODE)
Worktree isolation is opt-in, gated by an environment variable. With it unset, MultiTerminal behaves exactly as before — everyone works directly in the main checkout.
# Enable per-task worktree isolation
MULTITERMINAL_WORKTREE_MODE=on
MULTITERMINAL_WORKTREE_MODE is a process environment variable read once at startup (WorktreeConfig resolves it on first access and caches the result). Set it before launching MultiTerminal — e.g. as a Windows user/system environment variable, or in the shell that starts the app — and restart the app for a change to take effect. Only the literal value on (case-insensitive) enables it; unset, blank, 1, true, and yes all leave it off.
When the gate is on, the worktree lifecycle hooks fire automatically at task-activate and task-done time. When it is off, no worktrees are created and the teardown steps are skipped.
Auto-create on claim
Setting a task active (claiming it, or activating an already-claimed task) materializes a worktree for the acting agent. The operation is idempotent: if that agent already has an active worktree on disk for the task, the existing one is returned unchanged — no second checkout, no git work.
- The assignee gets the canonical worktree on branch
task/<id>, forked from trunk. - Each helper gets their own worktree on
task/<id>--<slug>, forked from the canonical branch's tip (the canonical branch is created first if it does not yet exist).
Fresh worktrees contain only tracked files. A few build-required artifacts in this repo are vendored and gitignored (notably tools/rg.exe), so MultiTerminal seeds the known vendored directories from the main checkout into each new worktree — otherwise a post-build copy step would fail.
Trunk safety
The canonical branch is always rooted explicitly at trunk, never at "whatever HEAD points to." If the main checkout is on a detached HEAD or sitting on another task/ branch, worktree creation fails loudly rather than silently forking the new branch from the wrong commit.
Branch & path naming
The first 8 characters of the task id (<idShort>) drive both the directory name and the branch name.
| Role | Branch | Worktree path | Forked from |
|---|---|---|---|
| Assignee (canonical) | task/<idShort> |
.claude/worktrees/<idShort>/ |
trunk |
| Helper | task/<idShort>--<slug> |
.claude/worktrees/<idShort>--<slug>/ |
canonical branch tip |
Records are persisted in the task_worktrees table, keyed by the composite (task_id, agent_name). A record is active while the directory exists on disk and pruned after removal; pruned rows are retained for audit/history.
Two agents on one task
Say task c0ce8037 is assigned to Alice and Bob joins as a helper with slug tests:
- Alice (assignee) works in
.claude/worktrees/c0ce8037/on branchtask/c0ce8037— the canonical branch, forked from trunk. - Bob (helper) works in
.claude/worktrees/c0ce8037--tests/on branchtask/c0ce8037--tests— forked from the canonical branch tip.
Each has its own checkout, so neither sees the other's uncommitted edits. On done, Bob's branch is merged into Alice's canonical branch first, then the canonical branch lands on trunk.
Teardown on done (commit → merge → prune)
Moving a task to done runs a multi-phase teardown so nothing the agents wrote is lost and trunk ends up carrying every agent's work:
- Auto-commit — each worktree's changes are committed on its own branch. MultiTerminal stages the exact file list from
git status --porcelain(never a blanketgit add -A), honors pre-commit hooks (never--no-verify), and writes a single commit with the task title, the implementation summary, and aCo-Authored-Bytrailer for the agent. A clean worktree is a no-op. - Integrate helper branches — each
task/<id>--<slug>branch is merged into the canonicaltask/<id>branch (inside the still-checked-out canonical worktree). A conflict aborts the merge cleanly and halts teardown so nothing is dropped. - Prune — every worktree directory for the task is removed and its record marked
pruned. Prune must run before the trunk merge: git refuses to merge a branch while it is checked out anywhere. - Merge to trunk — the canonical
task/<id>branch is merged into the main checkout's trunk withgit merge --no-edit(fast-forward or merge commit), then the branch is deleted.
Merge guards (fail closed)
The trunk merge refuses rather than guessing when it cannot be sure it is landing in the right place:
- The expected trunk is resolved from the project's configured
git_default_branch, then the remote'sorigin/HEAD, then the sole non-task local branch only if it bears a conventional name (main,master,develop,trunk). If none resolve, the merge refuses. - If the main checkout's branch is not the expected trunk, the merge refuses with a "check out trunk and re-mark done" message.
- If the main checkout has uncommitted tracked changes (beyond MT's own
.claude/project.jsonbookkeeping), the merge refuses — genuine user work is never auto-committed. - A merge conflict triggers
git merge --abort; the main checkout stays clean and the task branch is left alive for manual resolution.
Where your shell ends up
On Claude Code 2.1.157 and later, MultiTerminal uses the EnterWorktree / ExitWorktree harness operations to move a session's working directory into and out of a task worktree cleanly (rather than a raw cd, which the cwd-pinned harness does not honor). The session-start auto-cd flow enters the active task's worktree so a resumed shell lands in the right checkout.
This is also why worktrees must live under .claude/worktrees/: the EnterWorktree(path=...) enter-existing form requires the target be a worktree under that directory for cwd-pinned-at-launch agents (MT terminals).
HUD git panel rebind
The HUD git panel always shows the repository state of the active task's worktree, and rebinds itself when the active task changes. Three events drive this:
TaskActiveChanged— fires after the active task swaps. The HUD git renderer resolves the new task's worktree path agent-aware (a helper rebinds to its owntask/<id>--<slug>worktree, not the assignee's canonical one).WorktreePruning— fires synchronously just before the directory is removed at task-done. Subscribers drop live git handles, and a control message tells any agent whose cwd is inside the worktree tocdout before the rmdir.WorktreeReady— fires after both prune and the post-prune trunk merge complete. The HUD git renderer releases its cached service for the pruned path and rebinds to the post-merge repo root.
The contract between the last two: pruning says "drop your handles, the directory is about to disappear"; ready says "the post-merge state is durable, refresh your view."
Querying the active worktree
A long-running shell can switch tasks, so the launch-time worktree path goes stale. Agents query the live path instead via the get_active_worktree MCP tool, which is backed by:
GET http://localhost:5050/api/worktrees/active/{agentName}
The endpoint always returns HTTP 200. A null worktreePath means "no active worktree" (no active task, worktree mode off, or project unregistered) and is used as the no-op signal in the auto-cd protocol. A representative response:
{
"agentName": "Alice",
"taskId": "c0ce8037",
"taskTitle": "Add dark mode",
"worktreePath": "H:\\DevLaptop\\...\\.claude\\worktrees\\c0ce8037",
"repoRoot": "H:\\DevLaptop\\...\\MultiTerminal",
"branchName": "task/c0ce8037"
}
This is a read-or-create endpoint: if the agent's active task is eligible but its worktree was never materialized, the call backfills it (idempotent, bounded to that agent's one legitimate worktree).
Failure handling
- Partial prune (Windows): when an agent's terminal has its cwd inside a worktree,
git worktree removewipes contents and unregisters the worktree but cannot rmdir the directory (the OS holds an open handle). MultiTerminal detects this by re-queryinggit worktree list; if the path is gone from git's view it treats the removal as a success and leaves only an empty shell, which the janitor sweeps later. A strand signal is surfaced to the activity feed so it is observable in real time. - Stranded directories:
GET /api/worktrees/strandedreports de-registered-but-on-disk worktree shells. Its payload carries an explicitstatus(ok/partial/unavailable) so a caller can never mistake "couldn't tell" for "none." - Dirty main checkout / conflicts: see the merge guards above — these refuse and leave state recoverable rather than forcing a merge.