Skip to content

Isolation & Worktree

When multiple agents work on the same codebase simultaneously, they need isolation to avoid stepping on each other’s changes. Claude Code uses git worktrees as its primary isolation mechanism — each agent gets its own working copy of the repository, sharing the same git history but with independent file systems.

The alternatives and why they were rejected:

ApproachProblem
Same directoryFile conflicts between concurrent agents
Full cloneDisk space, slow, separate git history
Docker/containerHeavy, complex setup, not universally available
Git worktree✅ Lightweight, shared history, independent files

Git worktrees are a native git feature (git worktree add) that creates a new working directory linked to the same repository. Agents can independently modify files, create branches, and commit without affecting each other.

Agent definitions support two isolation modes, declared in BaseAgentDefinition:

src/tools/AgentTool/loadAgentsDir.ts
isolation?: 'worktree' | 'remote'
ModeAvailabilityDescription
worktreeAll usersLocal git worktree on disk
remoteAnthropic internal onlyRemote execution in Claude Code Remote (CCR)

The remote mode is gated at parse time:

// src/tools/AgentTool/loadAgentsDir.ts — AgentJsonSchema
isolation: (process.env.USER_TYPE === 'ant'
? z.enum(['worktree', 'remote'])
: z.enum(['worktree'])
).optional(),

External users can only use worktree. Attempting to set remote in an agent definition will fail validation.

sequenceDiagram
participant L as Leader Agent
participant W as Worktree System
participant G as Git
participant A as Agent (in worktree)
L->>W: Spawn agent with isolation: 'worktree'
W->>G: git worktree add <path> <branch>
G-->>W: Worktree created at <path>
W->>A: Start agent with cwd = worktree path
Note over A: Agent works independently<br/>in its own working copy
A->>A: Read, Edit, Bash, Commit...
A-->>L: Report results
L->>W: Cleanup / Kill agent
W->>G: git worktree remove --force <path>
G-->>W: Worktree removed

Each team member can have its own worktree, stored in the team configuration:

// TeamMember in teamHelpers.ts
type TeamMember = {
agentId: string
worktreePath?: string // Path to this member's worktree
// ...
}

When a team is disbanded or a member is killed, the worktree must be cleaned up. The destroyWorktree() function handles this carefully:

// src/utils/swarm/teamHelpers.ts — destroyWorktree (conceptual)
async function destroyWorktree(worktreePath: string): Promise<void> {
// 1. Read .git file to find the main repository
// (worktree .git is a file, not a directory: "gitdir: /path/to/main/.git/worktrees/...")
const gitContent = await readFile(join(worktreePath, '.git'), 'utf-8')
// 2. Extract main repo path from gitdir reference
const mainRepoPath = resolveMainRepo(gitContent)
// 3. Use git worktree remove for clean removal
try {
await exec(`git worktree remove --force ${worktreePath}`, { cwd: mainRepoPath })
} catch {
// 4. Fallback: force remove the directory
await exec(`rm -rf ${worktreePath}`)
}
}

When the fork mechanism creates a child in an isolated worktree, a special notice is injected into the child’s context:

src/tools/AgentTool/forkSubagent.ts
export function buildWorktreeNotice(parentCwd: string, worktreeCwd: string): string {
return `You've inherited the conversation context above from a parent agent
working in ${parentCwd}. You are operating in an isolated git worktree at
${worktreeCwd} — same repository, same relative file structure, separate
working copy.
Paths in the inherited context refer to the parent's working directory;
translate them to your worktree root.
Re-read files before editing if the parent may have modified them since
they appear in the context.
Your changes stay in this worktree and will not affect the parent's files.`
}

This notice addresses three critical issues:

  1. Path translation — inherited paths point to the parent’s directory, not the worktree
  2. Stale context — files read in the parent’s context may have changed
  3. Isolation guarantee — changes won’t affect the parent’s working copy

When teams are cleaned up, all worktrees must be destroyed before directories are removed:

flowchart TD
A[cleanupTeamDirectories<br/>teamName] --> B[Read team config.json]
B --> C[For each member<br/>with worktreePath]
C --> D[destroyWorktree<br/>member.worktreePath]
D --> E{Success?}
E -->|yes| F[Continue]
E -->|no| G[Log error, continue]
F --> H[Remove task directories<br/>for each member]
H --> I[Delete team config file]

Session-level cleanup (cleanupSessionTeams) runs when a session ends, ensuring no orphaned worktrees remain:

// Cleanup finds all team files for the session and destroys their worktrees
async function cleanupSessionTeams(sessionId: string): Promise<void> {
const teamFiles = await findTeamFilesForSession(sessionId)
for (const teamFile of teamFiles) {
await cleanupTeamDirectories(teamFile.name)
}
}

The worktree system also supports standalone sessions (not just team members). getCurrentWorktreeSession() in src/utils/worktree.ts tracks worktree metadata for the current session:

// Used in prompts.ts for the statusLine agent input
type WorktreeSession = {
name: string // Worktree name/slug
path: string // Full path to worktree
branch?: string // Git branch
original_cwd: string // Directory before entering worktree
original_branch?: string
}

This metadata flows into the status line configuration, letting users see which worktree they’re in and quickly navigate back.

Worktree paths are validated to prevent path traversal:

  • Paths must be absolute
  • Paths cannot escape the project root
  • The .git file in worktrees is read to validate it points to a legitimate repository

The validateMemoryPath() function in src/memdir/paths.ts shows the same pattern of path validation used throughout the system — rejecting relative paths, root paths, null bytes, and UNC paths.