Repository navigation
fix(hooks): detect linked worktrees by .git shape, not path convention - #4490
al-amin-chayan wants to merge 2 commits into
Conversation
Claude Code's worktree-isolation guard evaluates the command after the PreToolUse hook has rewritten it, and it can only verify git when it is spelled plainly. Every `rtk git …` the hook emitted was therefore refused as "cannot be shown not to be git", so no git command could run from a worktree-isolated session. The payload carries no isolation flag, but managed worktrees always live at `<repo>/.claude/worktrees/<name>`. When the payload cwd has that shape, the Claude hook adds `git` to the exclude list so it passes through untouched while the rest of the chain is still rewritten. Other hosts pass no cwd and are unchanged. Fixes rtk-ai#3864 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The cwd-based exclusion added in b3hz4d's rebase of rtk-ai#3879 matched only the <repo>/.claude/worktrees/<name> path shape Claude Code's own harness happens to use. Reviewers on that PR (pixelari, lgsurith) flagged the gap: a worktree hand-provisioned with `git worktree add` at any other location (for example this project's own .worktrees/<slug> convention) still got its git commands rewritten to `rtk git ...` and refused by Claude Code's worktree-isolation guard, exactly the rtk-ai#3864 loop. Replace the path-shape check with the signal pixelari proposed: git marks a linked worktree by making .git at its root a *file* containing `gitdir: ...`, rather than the directory the main checkout has. Walking up from cwd for the nearest .git entry and checking which kind it is needs no subprocess and covers every worktree shape uniformly. Rewrites the rtk-ai#3864 regression tests to lay out real .git fixtures with tempfile::tempdir() instead of asserting against a hardcoded path string, so they exercise the actual filesystem check. Fixes rtk-ai#3864 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
📊 Automated PR Analysis
SummaryReplaces the path-convention-based worktree detection ( Review Checklist
Linked issues: #3864 Analyzed automatically by wshm · This is an automated analysis, not a human review. |
|
Thank you for picking this up and for crediting the earlier work. I am closing it as not planned. Here is what I checked before deciding. The gap you describe is real. With Claude Code 2.1.302 and develop's rtk (#3879 merged), a session that enters a worktree made by The Measured on this head (a21875f) against develop e542dca, using real
The branch also conflicts with develop now: it was cut from #3879 before #3879 was rebased. I opened #4634 for the isolated-session-outside- |
Summary
Builds on #3879 (credit @b3hz4d for the rebase this is cherry-picked from, and @pixelari / @lgsurith for identifying the exact gap below) to close out #3864.
cwd-based exclusion only recognized<repo>/.claude/worktrees/<name>— the path shape Claude Code's own harness happens to use. @pixelari flagged on that PR that a worktree created by plaingit worktree addat any other location hits the identical rtk hook claude refuses every git command in a worktree, always the same isolation error #3864 refusal, since the check never fires for it. @lgsurith reproduced that gap on the rebased branch and suggested detecting a linked worktree by.gitbeing a file rather than matching a path convention.in_claude_worktree) with that generic signal (is_linked_git_worktree): git marks a linked worktree by making.gitat its root a file containinggitdir: <path>, instead of the directory the main checkout has. Walking up fromcwdfor the nearest.gitentry and checking which kind it is needs no subprocess, mirrors git's own upward search, and covers every worktree shape uniformly — including a convention like<repo>/.worktrees/<slug>that isn't Claude Code's own.#3864regression tests to lay out real.gitfixtures withtempfile::tempdir()(already a normal dependency) instead of asserting against a hardcoded path string, so they exercise the actual filesystem check rather than the string match it replaces.Fixes #3864
Test plan
parent() == None;std::fs::metadataresolves symlinks so a symlinked.gitfile is still detected correctly; main-checkout fixture uses a real.gitdirectory and is confirmed rejected)src/hooks/decision.rs:.git-file detection at an arbitrary (non-.claude/worktrees) path, from both the worktree root and a subdirectory; rejection of a real main-checkout.gitdirectory; rejection when no repository exists at all; end-to-enddecide_for_agent_atbehavior for both the linked-worktree and main-checkout fixturessrc/hooks/hook_cmd.rs'stest_claude_payload_cwd_in_worktree_skips_git_rewriteto use a real.git-file fixture instead of a path-string stand-incargo fmt --all && cargo clippy --all-targets && cargo test --all— I don't have a Rust toolchain on this machine and the fix is small enough that I didn't want to install one just to self-certify; happy to iterate on CI's output<repo>/.claude/worktrees/<name>git statusgit worktree add ../<name>git statusrtk git status(still refused by Claude Code's guard)git worktree add ../<name>git status && cargo buildgit status && rtk cargo buildgit statusNot covered (same as #3879):
rtk git …typed directly by the model is still refused by Claude Code's own guard; the model's plaingitretry now succeeds.🤖 Generated with Claude Code