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/state and working files remain confined to the worker's directory
  • Branch isolation: Commits occur on the worktree's branch, leaving main untouched
  • 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 normalizeWorktreeCreateCWD function rejects payloads where the cwd parameter resembles JSON, eliminating the bug described in issue #128
  • Automatic pruning: WorktreeManager.CleanupStale removes abandoned worktrees older than 24 hours, preventing repository bloat
  • Idempotent cleanup: WorktreeRemove handlers safely ignore missing files, ensuring repeated cleanup calls do not error
  • Resource containment: All temporary files are scoped to /tmp and the specific worktree path, preventing cross-contamination between agents

Summary

  • Worktree hooks (WorktreeCreate and WorktreeRemove) in claude-code-harness automate the lifecycle of isolated Git worktrees for parallel agents
  • Hook registration occurs through .claude-plugin/hooks.json, delegating to bin/harness and specialized handlers in go/internal/hookhandler/
  • State management happens via hidden .claude/state directories containing worktree-info.json metadata files
  • Git isolation is provided by breezing.WorktreeManager, which executes git worktree add -b to 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →