Repository navigation
fix(hooks): leave git unrewritten in Claude Code managed worktrees - #3879
Conversation
|
+1, run in the same issue |
|
+1 |
|
The
Reproduced just now, entering a hand-provisioned worktree that lives outside the repo entirely (
The refusal text names rtk directly - "this command runs rtk with a git command among its operands" - so the rewrite is landing before the guard reads it, exactly as you describe. But with this PR as drafted the A more robust test than the path shape: ask git. In a linked worktree Equal means main checkout, differing means linked worktree. Canonicalize before comparing, since both resolve relative to If you would rather not shell out to git in the hook's hot path: in a linked worktree |
|
Tested this branch on macOS 26 (arm64), Claude Code, Method: built
So the The Also, the branch currently has merge conflicts with |
ab2e8b4 to
d07e17d
Compare
|
Rebased onto current #3955 moved the shared hook decision into Gate on the rebased tree: fmt clean, clippy clean with
Happy to fold a fix for the first one into this PR if you'd prefer, otherwise I'm keeping it scoped to #3864. |
|
@b3hz4d thanks for the rebase! Re-tested d07e17d on macOS (arm64):
Still open (non-blocking): worktrees from cc @KuSh @aeppling: rebased, conflict-free and tested. Could one of you take a look? #3864 blocks every git command in Claude Code worktree sessions. |
KuSh
left a comment
There was a problem hiding this comment.
Round 1 — APPROVED, follow-ups filed
Claim: inside a Claude Code worktree at <repo>/.claude/worktrees/<name>, rtk hook claude leaves git unrewritten so Claude Code's worktree-isolation guard lets it run, while the rest of a chain is still rewritten.
Scope: accept. #4490 builds on this branch and covers sessions isolated outside .claude/worktrees/ (reproduced here with a WorktreeCreate hook, every git command still refused); this one lands first.
Ran: Claude Code 2.1.301 claude -p --worktree with the hook pointed at each build: 7 commands × develop / this PR merged on develop f16beca (+ fixup), and again by a second runner; a session isolated through a WorktreeCreate hook outside .claude/worktrees/; a plain session in a git worktree add checkout. Hook payload matrix: 22 commands × 9 cwd shapes × develop / PR head / PR merged. Invariants: non-default targets (subdirectory cwd, drive-letter path, trailing slash, custom worktree location), under the hook (real Claude Code), every decision. No output filter changes, so savings do not apply: in a managed worktree git output now goes unfiltered, where develop's rtk git … was refused anyway.
Pins: 8 decisions, 8 mutated, 3 survived the suite (.claude/<other> accepted as a worktree, user exclude_commands replaced instead of extended, only git status excluded). Each is killed by a test in the fixup.
Matrix: executed this round; pin check: executed this round
CI @4c4f055df: 12 green; the three non-Linux test jobs fail only a_clustered_h_is_answered_by_git_once, on git 2.56, exactly as develop f16beca's own CI does. #4578 fixes it; not this PR's code.
Blocks merge (0)
Filed as follow-ups
- #4591 Any
rtk …command with agit-named operand is refused in an isolated session (grep -n git README.md→rtk grep -n git README.md; alsortk bypass git …from rtk's own hints). Same guard rule, other command lines.
Checked and correct — no need to re-verify
- With develop's rtk,
git status,git log -1 --oneline,git -C <wt> statusandgit status && lsare all refused in a--worktreesession. With this PR they run, andgit status && lsbecomesgit status && rtk ls. Claude Code puts the worktree's absolute path in the payload'scwd. - Inside a managed worktree, every git spelling the rewriter would otherwise wrap is left alone: chains,
cd … &&, env assignments,env,time,timeout,nice,command, pipes,bash -c "git …". - Outside a worktree, with no
cwd, and at.claude/worktreesitself, nothing changes.git statusis stillrtk git status, and non-git commands are still rewritten inside the worktree. - A plain session running in a
git worktree addcheckout (no isolation) never hits the guard;rtk git statusruns there on develop. Matching the managed path rather than "any linked worktree" keeps git savings in that case. in_claude_worktreecovers subdirectories of the worktree and drive-letter paths with forward slashes.
Your questions
- "Happy to fold a fix for the first one into this PR if you'd prefer, otherwise I'm keeping it scoped to #3864." → drop:
tests_that_spawn_rtk_isolate_their_dataand the twogit_show_*tests pass on current develop, with this branch merged in. There is nothing to fold in.
Next
Fixup 4c4f055 pushed, code-reviewed. It merges current develop into the branch. Develop's 7cc2699 added a decide_from_verdict(command, verdict) call in the hook tests, which stopped the lib tests compiling against the new signature, so it now passes None. The fixup also adds tests for the three unpinned decisions, rewords the decide_for_agent_at comment to say a worktree placed elsewhere is not detected, and uses CLAUDE_DIR instead of the literal. Approving. Once #4578 is merged, this branch needs a rebase on develop (or develop merged in) so the three non-Linux test jobs go green before merge.
Threads: 0 open.
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>
Pin the worktree exclusion: another git verb, a non-worktree .claude directory, and the user's own exclude_commands kept alongside it. The doc comment now says a worktree placed outside .claude/worktrees is not detected, and the path check uses CLAUDE_DIR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4c4f055 to
ee2af0d
Compare
The editions link copied from the upstream README returned 404; both links now use the /docs/ paths. Also note rtk-ai/rtk#3879, which leaves git unrewritten in Claude Code worktrees and is not in a release as of v1.0.1. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Summary
rtk hook clauderefused every git command in a worktree-isolated Claude Code session. The refusal is Claude Code's own worktree-isolation guard: it evaluates the command after the hook has rewritten it, and it can only verify git when spelled plainly, so everyrtk git …was rejected as "cannot be shown not to be git".<repo>/.claude/worktrees/<name>. When the payloadcwdhas that shape, the Claude hook now addsgitto the exclude list so it passes through untouched while the rest of the chain is still rewritten (git status && cargo build→git status && rtk cargo build).Fixes #3864
Test plan
cargo fmt --all && cargo clippy --all-targets && cargo test --all(clippy clean, 3110 unit tests + integration suites green)target/debug/rtk hook claude.claude/worktrees/git statusrtk git statusgit status && cargo buildgit status && rtk cargo buildcargo testrtk cargo testrtk cargo testNot covered:
rtk git …/rtk proxy git …typed directly by the model is still refused by Claude Code's guard (the hook only leaves already-rtk commands alone); the model's plaingitretry now succeeds, which breaks the loop from the issue.🤖 Generated with Claude Code