How Worktree Hooks Enable Isolated Branch-Based Development in Claude-Code-Harness
Claude-Code-Harness implements two agent hooks—WorktreeCreate and WorktreeRemove—to spin up independent Git worktrees for each parallel worker, ensuring complete filesystem and branch isolation.
The claude-code-harness repository solves the problem of parallel agent collisions by using Git worktrees as sandboxed environments. When agents run with branch-based isolation enabled, the harness intercepts lifecycle events through registered hooks, creating dedicated workspaces that prevent file conflicts and keep unmerged changes confined to individual branches.
Hook Registration and Event Flow
The worktree system begins with declarative registration in .claude-plugin/hooks.json. This configuration binds the WorktreeCreate and WorktreeRemove events to the bin/harness binary, which delegates to specific subcommands.
When Claude emits a WorktreeCreate event during agent initialization, the runtime invokes _ hook worktree-create. The corresponding JSON payload contains three critical fields:
{
"session_id": "session-abcde-123",
"cwd": "/tmp/harness-worktrees/12345",
"hook_event_name": "WorktreeCreate"
}
The hook handler validates this input and returns a decision JSON: {"decision":"approve","reason":"WorktreeCreate: initialized worktree state"}.
Creating Isolated Worktrees with WorktreeCreate
The WorktreeCreate hook initializes the sandbox environment through two parallel implementations: a Go handler for production use and a shell script for lightweight environments.
Validating the Working Directory
In go/internal/hookhandler/worktree_create.go, the hookhandler.HandleWorktreeCreate function parses the incoming payload and immediately validates the cwd parameter. The helper function looksLikeHookDecisionJSON prevents a critical bug (fixed in issue #128) where malformed JSON could be interpreted as a directory path.
// normalizeWorktreeCreateCWD guarantees that decision JSON
// cannot be treated as a directory path
func normalizeWorktreeCreateCWD(cwd string) (string, error) {
if looksLikeHookDecisionJSON(cwd) {
return "", fmt.Errorf("cwd appears to be JSON, not a path")
}
return cwd, nil
}
Persisting State Metadata
After validation, the handler creates a hidden state directory at $CWD/.claude/state and writes worktree-info.json. This file tracks the worker ID, timestamp, and working directory, ensuring the harness can correlate filesystem paths with specific agent sessions.
The shell implementation in scripts/hook-handlers/worktree-create.sh mirrors this logic for environments without the Go binary:
#!/bin/bash
# scripts/hook-handlers/worktree-create.sh
if [ -z "$INPUT" ]; then
echo '{"decision":"approve","reason":"WorktreeCreate: no payload"}'
exit 0
fi
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""')
CWD=$(echo "$INPUT" | jq -r '.cwd // ""')
# Reject a cwd that looks like a decision JSON
if [[ "$CWD" =~ ^\{.*\"decision\".*\"reason\".*\}$ ]]; then
echo '{"decision":"approve","reason":"WorktreeCreate: invalid cwd"}'
exit 0
fi
# Initialise state dir and info file
WORKTREE_STATE_DIR="${CWD}/.claude/state"
mkdir -p "$WORKTREE_STATE_DIR"
cat > "${WORKTREE_STATE_DIR}/worktree-info.json" <<EOF
{"worker_id":"$SESSION_ID","created_at":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","cwd":"$CWD"}
EOF
echo '{"decision":"approve","reason":"WorktreeCreate: initialized worktree state"}'
Managing Worktree Lifecycles with WorktreeManager
Behind the hooks, the breezing.WorktreeManager in go/internal/breezing/worktree.go handles the actual Git operations. When a worker requests isolation, the manager executes:
git worktree add -b <branch> <path> HEAD
This creates a linked working tree with its own branch, allowing the agent to commit changes without affecting the main checkout. The manager records the path, branch, task ID, and owning agent ID in an internal registry.
Programmatic usage follows this pattern:
wm := breezing.NewWorktreeManager("/path/to/repo")
path, err := wm.Create("task-123", "") // creates .harness-worktrees/task-123
if err != nil { log.Fatal(err) }
wm.AssignAgent(path, "worker-xyz") // associate the agent
// ... run the agent inside `path` ...
wm.MarkInactive(path) // when the agent stops
wm.CleanupStale() // removes worktrees older than 24h
Cleaning Up Resources with WorktreeRemove
When a worker terminates, the WorktreeRemove hook fires. The Go handler in go/internal/hookhandler/worktree_remove.go removes temporary Codex prompt files from /tmp, cleans harness logs, and deletes the worktree-info.json marker file.
The shell counterpart in scripts/hook-handlers/worktree-remove.sh performs identical cleanup:
#!/bin/bash
# scripts/hook-handlers/worktree-remove.sh
# ... read INPUT, extract SESSION_ID and CWD ...
# Remove temporary Codex prompt files and harness logs
rm -f /tmp/codex-prompt-*.md /tmp/harness-codex-*.log
# Delete the worktree-specific info file
if [ -n "$CWD" ] && [ -f "$CWD/.claude/state/worktree-info.json" ]; then
rm -f "$CWD/.claude/state/worktree-info.json"
fi
echo '{"decision":"approve","reason":"WorktreeRemove: cleaned up worktree resources"}'
Enabling Isolation Mode in Agent Configuration
Agents opt into branch-based isolation by declaring isolation: "worktree" in their skill definitions. When this flag is set, the harness supplies each agent with a unique worktree path from WorktreeManager rather than the main repository checkout.
This configuration ensures:
- Filesystem isolation: Edits in
.claude/stateand working files remain confined to the worker's directory - Branch isolation: Commits occur on the worktree's branch, leaving
mainuntouched - Parallel safety: Multiple workers can modify the same filenames without conflict
Trigger isolation manually via CLI:
claude agents --isolation=worktree --task-id=12345
Safety Guarantees and Error Handling
The worktree hook system includes several defensive mechanisms to prevent resource leaks and security issues:
- Input sanitization: The
normalizeWorktreeCreateCWDfunction rejects payloads where the cwd parameter resembles JSON, eliminating the bug described in issue #128 - Automatic pruning:
WorktreeManager.CleanupStaleremoves abandoned worktrees older than 24 hours, preventing repository bloat - Idempotent cleanup:
WorktreeRemovehandlers safely ignore missing files, ensuring repeated cleanup calls do not error - Resource containment: All temporary files are scoped to
/tmpand the specific worktree path, preventing cross-contamination between agents
Summary
- Worktree hooks (
WorktreeCreateandWorktreeRemove) inclaude-code-harnessautomate the lifecycle of isolated Git worktrees for parallel agents - Hook registration occurs through
.claude-plugin/hooks.json, delegating tobin/harnessand specialized handlers ingo/internal/hookhandler/ - State management happens via hidden
.claude/statedirectories containingworktree-info.jsonmetadata files - Git isolation is provided by
breezing.WorktreeManager, which executesgit worktree add -bto create branch-specific workspaces - Cleanup automation removes temporary Codex files and state markers when workers terminate, with stale worktree pruning handled by
CleanupStale
Frequently Asked Questions
How do I enable worktree isolation for a specific Claude agent?
Set the isolation field to "worktree" in the agent's skill definition or launch the agent with the --isolation=worktree flag. According to the claude-code-harness source code, this triggers the WorktreeCreate hook during agent initialization, which allocates a separate Git worktree via the breezing.WorktreeManager.
What happens if the WorktreeCreate hook receives malformed JSON?
The Go implementation in go/internal/hookhandler/worktree_create.go includes the looksLikeHookDecisionJSON validation function. If the cwd parameter appears to be JSON rather than a filesystem path (a bug fixed in issue #128), the handler rejects the input and returns an approval response with reason "WorktreeCreate: invalid cwd", preventing the creation of directories with malformed names.
Where does the harness store metadata about active worktrees?
Each worktree contains a hidden state directory at .claude/state/worktree-info.json created by both the Go handler and the shell script in scripts/hook-handlers/worktree-create.sh. This JSON file stores the worker ID, creation timestamp, and absolute path, allowing the WorktreeManager to track which agents own specific branches.
Can I use the worktree hooks without the Go binary installed?
Yes. The repository provides POSIX-compliant shell implementations in scripts/hook-handlers/worktree-create.sh and scripts/hook-handlers/worktree-remove.sh. These scripts handle the same JSON parsing, validation, and state directory creation as the Go handlers, making the isolation system available in environments where the compiled bin/harness binary is not present.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →