fix(hooks): refuse Agent worktree-isolation calls from nested worktrees#483
fix(hooks): refuse Agent worktree-isolation calls from nested worktrees#483zackees wants to merge 1 commit into
Conversation
When the Claude Code Agent tool is invoked with `isolation: "worktree"`, the harness creates the new worktree relative to the caller's current working directory. A sub-agent already running inside `.claude/worktrees/<parent>/` therefore lands its child worktree at `.claude/worktrees/<parent>/.claude/worktrees/<child>/`, and the next level deepens further. This recursive nesting was the root trigger of issue #481 — Windows MAX_PATH `fatal error C1081: 'file name too long'` in cc-rs build scripts, with the cc-rs PATH/env dumps overflowing the 1M context window before truncation (PR #482) could even matter. We can't change the harness from inside the harness, but we can short- circuit the bad case: a new PreToolUse hook (`worktree_guard.py`, matcher `Agent`) inspects the event and denies any Agent invocation that has `isolation: "worktree"` AND a cwd already inside a `.claude/worktrees/...` path. The deny message names the fix: omit `isolation: "worktree"` so the sub-agent shares the parent worktree, or spawn the agent from the repo root. Includes 10 unit tests covering path-segment detection (forward/back slashes, Windows drive letters, multi-level nesting), the deny matrix (Agent + worktree + nested = deny; everything else = allow), and defensive handling of malformed tool_input payloads. Refs #481. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 18 minutes and 18 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
Closing — the local-side workaround was reverted out of the repo (see commits resetting CLAUDE.md, .claude/settings.json, ci/hooks/README.md back to pre-#483 state). The harness-side defect is still tracked in #485; if/when an upstream fix lands or a different mitigation approach is chosen, a fresh PR will supersede this branch. |
…485) (#891) Claude Code's Agent tool with isolation="worktree" creates the new git worktree relative to the calling agent's cwd. When the calling agent is itself running inside a worktree the harness previously created, the new worktree lands at: .claude/worktrees/<parent>/.claude/worktrees/<child>/ Each sub-agent compounds the nesting. On Windows that pushes paths past MAX_PATH (260), breaking cargo build scripts (`cc-rs` for `bzip2-sys` / `libz-sys`) with `fatal error C1081: 'file name too long'`. The retry loop floods stderr with kilobyte PATH dumps that can overflow the session's context window (see #481/#482). PR #483 proposed the local mitigation but never merged. This adds it. The new `ci/hooks/worktree_guard.py` is a PreToolUse hook scoped to the `Agent` matcher. It denies any Agent call that combines `isolation: "worktree"` with a session cwd already inside `.claude/worktrees/<name>/`. The deny message tells the user how to proceed (re-spawn without isolation, or run from the main checkout) and points at #485 so they can track the upstream harness fix. Registered in `.claude/settings.json` with the same guarded-existence shape every other hook uses: `[ -f ci/hooks/worktree_guard.py ] && uv run --no-project --script ci/hooks/worktree_guard.py || exit 0` so orphan-branch worktrees that legitimately don't have the script cannot wedge the harness. 24 unit tests in `ci/hooks/test_worktree_guard.py` cover: - path normalization (Windows backslashes -> POSIX) - worktree-segment detection at any depth, including stacked cases - isolation-field parsing (case-insensitive, whitespace-tolerant) - session-cwd lookup precedence (payload > env > getcwd()) - main() end-to-end on the deny path AND the allow paths Once the upstream harness anchors worktree paths at the repo root (`git rev-parse --show-toplevel`), this hook can retire and #485 can close. Until then it shipping unblocks the user's nested-agent sessions on Windows. Closes #485
Summary
ci/hooks/worktree_guard.py, a newPreToolUsehook (matcher:Agent) that denies Agent invocations withisolation: "worktree"when the calling agent's cwd is already inside a.claude/worktrees/...path..claude/settings.jsonalongside the existing shelltool_guard.py.ci/hooks/test_worktree_guard.py(10 unit tests) and documents the new hook inci/hooks/README.md+CLAUDE.md.Why
PR #482 truncated the symptom (hook stderr overflowing context). This PR removes the cause.
The Claude Code Agent tool, when invoked with
isolation: "worktree", creates the new worktree relative to the caller's cwd. A sub-agent already running inside.claude/worktrees/<parent>/therefore lands its child worktree at.claude/worktrees/<parent>/.claude/worktrees/<child>/, and the next level deepens further. Localgit worktree listactually shows a 4-deep stack:That path is past Windows MAX_PATH for many cargo build-script tempfiles. It produced the
fatal error C1081: 'file name too long'that triggered cc-rs's PATH-dumping retry loop in issue #481, which in turn overwhelmed the 1M context window before truncation could help.We can't change the harness from inside the harness, but we can short-circuit the bad case at the PreToolUse boundary:
tool_name == "Agent"ANDtool_input.isolation == "worktree"ANDos.getcwd()already contains.claude/worktrees/<x>/→ deny, with a message naming the fix (omit
isolation: "worktree"so the sub-agent shares the parent worktree, or spawn from the repo root instead of from inside a worktree).Top-level Agent invocations (from the repo root) are still allowed; non-worktree Agent calls are still allowed; non-Agent tools (
Bash,Edit, …) are never inspected.Out of scope (separate follow-up)
This is a fbuild-local workaround, not a fix in Claude Code itself. The harness should ideally always anchor new worktrees at the repo root regardless of the calling agent's cwd. Tracked upstream-side in #485 — the hook can be retired once that lands.
Test plan
uv run -m unittest discover -s ci/hooks -p 'test_*.py'→ 35/35 hook tests pass (25 prior + 10 new).Agent+isolation: "worktree"+ nested cwd → emits thepermissionDecision: "deny"JSON.Bashtool with nested cwd → empty stdout, exit 0 (non-Agent tools are never blocked).tool_input(None, str, int, list) → returns False rather than crashing.Refs #481, follow-up to #482, tracked upstream-side in #485.