Skip to content

fix(hooks): create real git worktrees and clean them up in worktree hooks#74

Open
gepenniman wants to merge 2 commits into
rohitg00:mainfrom
gepenniman:fix/worktree-hooks
Open

fix(hooks): create real git worktrees and clean them up in worktree hooks#74
gepenniman wants to merge 2 commits into
rohitg00:mainfrom
gepenniman:fix/worktree-hooks

Conversation

@gepenniman

@gepenniman gepenniman commented Jul 6, 2026

Copy link
Copy Markdown

Fixes #73

Problem

scripts/worktree-create.js ends with console.log(data), echoing its stdin JSON on stdout. Claude Code treats WorktreeCreate stdout as the path of the worktree the hook created, so every isolated Agent spawn fails with WorktreeCreate hook returned a path that is not a directory. Its companion scripts/worktree-remove.js only logs, so once creation works, every spawn would leak a registered worktree and branch. Details, contract notes, and repro are in the linked issue.

Change

worktree-create.js now fulfills the contract it was accidentally claiming:

  • Runs git -C <input.cwd> worktree add -b agents/<name>-<timestamp> into $TMPDIR/pro-workflow/worktrees/, so the agent gets a real checkout of the session's HEAD.
  • Falls back to a plain directory when the session is not inside a git repo.
  • Keeps the existing logging: stderr status lines and the rolling worktrees.json ledger, now recording the real worktree path, branch, and source repo instead of "unknown".
  • Echoes exactly one line on stdout: the created path. Logging failures are swallowed so they can never block a spawn; if directory creation itself fails the hook exits 1.

worktree-remove.js now actually cleans up:

  • Best-effort git worktree remove --force on the payload's worktree_path, deletes the matching agents/* branch (never any other namespace), runs git worktree prune, and drops the ledger row.
  • Always exits 0 so cleanup problems can never block the harness, and captures each raw payload to worktree-remove.log for diagnosis if the harness schema ever changes.

No changes to hooks/hooks.json or any other script.

Testing

  • Live end-to-end on Claude Code 2.1.201 (macOS): spawned an agent with isolation: "worktree". Before: instant failure. After: agent lands in a working checkout (pwd equals git rev-parse --show-toplevel, tracked files present).
  • Piped synthetic payloads through both scripts (20-case suite): create yields a real worktree on a fresh agents/* branch when cwd is a repo, a plain directory otherwise, and survives malformed stdin; remove tears down the worktree, branch, and ledger entry, and exits 0 on missing or bogus paths.

Alternative

If you would rather not own worktree creation in a logging plugin, the smaller fix is deleting the WorktreeCreate and WorktreeRemove entries from hooks/hooks.json and letting the harness create worktrees itself (SubagentStart/SubagentStop already log spawn lifecycle). Happy to rework this PR that way instead.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved workspace creation to prefer creating an isolated git worktree when possible, otherwise falls back to a local directory.
    • Enhanced workspace persistence and emits only the created workspace path on success.
    • Improved workspace cleanup that best-effort removes worktrees, prunes related branches, and keeps cleanup from blocking execution.
  • Bug Fixes

    • More defensive handling of malformed input.
    • Safer path resolution and ownership checks to avoid deleting unrelated directories or affecting other repositories.

…ooks

WorktreeCreate previously logged tracking data but created nothing, which
broke every agent spawn because the harness expects the hook to own
worktree creation and echo the new path. It now creates a real git
worktree on an agents/<name>-<ts> branch under $TMPDIR/pro-workflow/
worktrees (plain directory fallback outside a repo) and records the
branch and source repo in worktrees.json.

WorktreeRemove previously only logged and echoed stdin, so every
isolated agent spawn leaked a registered worktree and branch. It now
best-effort runs git worktree remove --force, deletes the matching
agents/* branch (never any other namespace), prunes stale admin
entries, and drops the ledger row. It always exits 0 and still echoes
stdin, and it captures each raw payload to worktree-remove.log for
diagnosis if the harness schema changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f18f7519-5d81-456c-9aeb-a882f84a184a

📥 Commits

Reviewing files that changed from the base of the PR and between 4b1adba and ba27dba.

📒 Files selected for processing (2)
  • scripts/worktree-create.js
  • scripts/worktree-remove.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/worktree-remove.js

📝 Walkthrough

Walkthrough

The worktree-create and worktree-remove hook scripts were rewritten. worktree-create.js now creates an actual workspace directory, records derived metadata, and outputs only the created path. worktree-remove.js now performs best-effort cleanup of matching worktrees, branches, and ledger entries while keeping stdin handling non-fatal.

Changes

Worktree lifecycle hooks

Layer / File(s) Summary
Worktree creation and ledger persistence
scripts/worktree-create.js
Adds createWorktree to sanitize names, create unique temp directories, attempt git worktree add with fallback to a plain directory, hardens stdin JSON parsing, persists actual worktree_path/branch/repo to worktrees.json, and outputs only the created path on stdout.
Worktree cleanup and ledger removal
scripts/worktree-remove.js
Adds git/logging helper utilities and a cleanup flow that resolves worktree/branch/repo via fallbacks, deletes worktree directories only under the temp base, runs git worktree remove/prune, force-deletes matching agents/* branches, removes the ledger row, and echoes the original input while avoiding throws.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: creating and cleaning up real git worktrees in hooks.
Linked Issues check ✅ Passed The create hook now emits a real worktree path and records correct metadata, and the remove hook tears down the worktree, branch, and ledger as required.
Out of Scope Changes check ✅ Passed The sanitization, unique naming, ownership checks, and exit-handling refinements directly support the worktree hook fix and are in scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/worktree-create.js`:
- Around line 24-32: The fallback in worktree creation is too broad: after a
successful git repo check in worktreeCreate, a failure from git worktree add
should not be swallowed and converted into a plain directory. Update the
try/catch around execFileSync in worktree-create.js so that only the non-repo
case uses fs.mkdirSync(target) and returns a null repo, while git worktree
failures are surfaced or rethrown instead of returning { target, branch: null,
repo: null }. Use the existing worktreeCreate flow and the rev-parse/worktree
add calls to separate these cases clearly.
- Around line 19-22: The worktree naming logic in the script should produce
Git-safe branch and directory names and avoid collisions when multiple spawns
happen at the same time. Update the `rawName` sanitization used for `name` so it
cannot yield ref-invalid forms like leading dots, `..`, or repeated dots, and
make the `branch`/`target` suffix more collision-resistant than `Date.now()`
alone. Keep the fix localized around the `name`, `target`, and `branch`
generation so `git worktree add -b` in this flow can succeed without relying on
the fallback directory path.

In `@scripts/worktree-remove.js`:
- Around line 90-91: The worktree removal script is force-exiting after logging,
which can drop buffered stdout output from the hook harness. In the main flow
that prints the payload with console.log, remove the direct process.exit(0) call
and let the script terminate naturally, or set process.exitCode = 0 instead so
the output can flush reliably.
- Around line 48-72: The repo-level cleanup in worktree-remove.js is too
permissive because prune and branch deletion can still run for non-owned paths
once a repo is resolved. Update the cleanup flow around the existing ours,
entry, repo, and branch handling so any call to git worktree prune or git branch
-D only happens when the worktree is owned or has a ledger entry, and keep the
current agents/* branch guard intact. Use the existing worktree removal logic
and repo resolution block to gate all Git cleanup consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2111b21b-fc3c-4d6f-b689-abce36b2def7

📥 Commits

Reviewing files that changed from the base of the PR and between 5c313e2 and 4b1adba.

📒 Files selected for processing (2)
  • scripts/worktree-create.js
  • scripts/worktree-remove.js

Comment thread scripts/worktree-create.js Outdated
Comment thread scripts/worktree-create.js Outdated
Comment thread scripts/worktree-remove.js
Comment thread scripts/worktree-remove.js Outdated
worktree-create.js:
- Sanitize names so branch/dir suffixes can't produce git-ref-invalid
  forms (leading dot, "..", trailing ".lock").
- Add a random suffix alongside the timestamp so parallel spawns with
  the same name can't collide on the same branch/target.
- Stop conflating "not a git repo" with "git worktree add failed
  inside a confirmed repo" - the latter now logs to stderr instead of
  silently downgrading to an indistinguishable plain directory.

worktree-remove.js:
- Gate the prune and branch-delete git calls on the same ownership
  check used for directory removal (ours || entry), not on repo
  alone. repo can resolve via input.cwd, which may be the session's
  main repo rather than a worktree this hook created.
- Drop the trailing process.exit(0): process.stdout is written
  asynchronously when the destination is a pipe, and exit() doesn't
  wait for the flush, so the echoed payload could be truncated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WorktreeCreate hook fails every isolated agent spawn: echoes stdin JSON as the worktree path

1 participant