Skip to content

fix(hooks): leave git unrewritten in Claude Code managed worktrees - #3879

Merged
KuSh merged 2 commits into
rtk-ai:developfrom
b3hz4d:fix/worktree-git-rewrite-3864
Oct 17, 2026
Merged

KuSh merged 2 commits into
rtk-ai:developfrom
b3hz4d:fix/worktree-git-rewrite-3864

Conversation

@b3hz4d

@b3hz4d b3hz4d commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • rtk hook claude refused 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 every rtk git … was rejected as "cannot be shown not to be git".
  • The PreToolUse payload carries no isolation flag, but Claude Code's managed worktrees always live at <repo>/.claude/worktrees/<name>. When the payload cwd has that shape, the Claude hook now adds git to 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).
  • Other hosts (Gemini, Cursor, Copilot, Droid, Vibe) pass no cwd and are unchanged.

Fixes #3864

Test plan

  • cargo fmt --all && cargo clippy --all-targets && cargo test --all (clippy clean, 3110 unit tests + integration suites green)
  • New unit tests: path-shape detection, git excluded / others rewritten inside a worktree, payload-level skip
  • Manual: piped real PreToolUse payloads into target/debug/rtk hook claude
Command cwd under .claude/worktrees/ cwd elsewhere
git status passthrough rtk git status
git status && cargo build git status && rtk cargo build both rewritten
cargo test rtk cargo test rtk cargo test

Not 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 plain git retry now succeeds, which breaks the loop from the issue.

🤖 Generated with Claude Code

@CLAassistant

CLAassistant commented Sep 5, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@Andre1Becker

Copy link
Copy Markdown

+1, run in the same issue

@Ilya1vanov

Copy link
Copy Markdown

+1

@pixelari

Copy link
Copy Markdown

The cwd path-shape test will miss a large class of worktrees.

<repo>/.claude/worktrees/<name> only covers worktrees the harness provisions itself. A worktree created with plain git worktree add at an arbitrary path gets isolated too, and hits exactly the refusal from #3864.

Reproduced just now, entering a hand-provisioned worktree that lives outside the repo entirely (~/workspace/<project>-worktrees/<branch>; .claude/worktrees/ is empty):

command result
git status --porcelain refused
rtk git status --porcelain refused
/usr/bin/git status --porcelain runs

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 cwd check would never fire for that worktree, and the loop stays.

A more robust test than the path shape: ask git. In a linked worktree --git-dir and --git-common-dir differ; in the main checkout they are identical. From that same worktree:

$ git rev-parse --git-dir --git-common-dir
/repo/.git/worktrees/<name>
/repo/.git

Equal means main checkout, differing means linked worktree. Canonicalize before comparing, since both resolve relative to cwd. That covers harness-managed and hand-provisioned worktrees alike, and does not depend on a path convention that can change.

If you would rather not shell out to git in the hook's hot path: in a linked worktree .git is a file containing gitdir: ..., not a directory. Same signal, no subprocess - verified in the same worktree above.

@lgsurith

lgsurith commented Oct 6, 2026

Copy link
Copy Markdown

Tested this branch on macOS 26 (arm64), Claude Code, rtk hook claude as the PreToolUse Bash hook. The bug is still present on the latest release (0.51.0), so this PR is still needed.

Method: built ab2e8b43 with cargo build, then piped real PreToolUse payloads (tool_input.command + cwd) into both the released binary and this build, comparing the rewritten command.

cwd command 0.51.0 (release) this PR
<repo>/.claude/worktrees/<name> git status / git log -n 2 / git branch -m a b rtk git … → refused by the isolation guard passthrough ✅
<repo>/.claude/worktrees/<name> git -C <worktree> status rtk git -C … passthrough ✅
<repo>/.claude/worktrees/<name> git status && cargo build both rewritten git status && rtk cargo build ✅
<repo>/.claude/worktrees/<name> rtk git status (typed directly) passthrough (still refused) passthrough (still refused) — as noted in the PR
worktree from git worktree add ../<name> git status rtk git status rtk git status ⚠️
main checkout git status, cargo test rewritten rewritten ✅ (savings unaffected)

So the .claude/worktrees/ case works as described, with no regressions outside worktrees.

The ⚠️ row confirms the earlier review comment: a worktree outside .claude/worktrees/ is still rewritten. In a session that enters it via EnterWorktree(path: …), that leads to the same refusal. +1 to detecting a linked worktree by .git being a file (gitdir: …) instead of matching the path; no subprocess needed, and it covers both kinds.

Also, the branch currently has merge conflicts with develop. If you're short on time, I'm happy to rebase it and add the .git-file check, either as a PR into your branch or as a follow-up that credits this one.

@b3hz4d

b3hz4d commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current develop and force-pushed as a single commit (d07e17d).

#3955 moved the shared hook decision into src/hooks/decision.rs since I cut this branch, so I re-applied the fix there: decide_for_agent_at(cmd, verdict, cwd) excludes git when cwd is under <repo>/.claude/worktrees/<name>, and the Claude hook passes the payload's cwd through. Every other host passes None and is unchanged. I dropped the hand-written CHANGELOG entry since release-please generates it from the commit message now.

Gate on the rebased tree: fmt clean, clippy clean with -D warnings, all suites green except three tests that fail identically on a pristine develop checkout in files this PR doesn't touch:

  • core::test_isolation::tests_that_spawn_rtk_isolate_their_data flags tests/glab_argv_test.rs for using CARGO_BIN_EXE_rtk directly.
  • git_show_cluster_flag_value_is_not_mistaken_for_the_blob_object and git_show_rename_limit_clusters_under_diffs_grammar_not_logs in tests/guard_integration_test.rs.

Happy to fold a fix for the first one into this PR if you'd prefer, otherwise I'm keeping it scoped to #3864.

@lgsurith

lgsurith commented Oct 6, 2026

Copy link
Copy Markdown

@b3hz4d thanks for the rebase! Re-tested d07e17d on macOS (arm64):

Still open (non-blocking): worktrees from git worktree add outside .claude/worktrees/ are still rewritten, since the check is path-based. I'm happy to send a follow-up after this lands that detects them by .git being a file.

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 KuSh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 a git-named operand is refused in an isolated session (grep -n git README.md → rtk grep -n git README.md; also rtk 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> status and git status && ls are all refused in a --worktree session. With this PR they run, and git status && ls becomes git status && rtk ls. Claude Code puts the worktree's absolute path in the payload's cwd.
  • 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/worktrees itself, nothing changes. git status is still rtk git status, and non-git commands are still rewritten inside the worktree.
  • A plain session running in a git worktree add checkout (no isolation) never hits the guard; rtk git status runs there on develop. Matching the managed path rather than "any linked worktree" keeps git savings in that case.
  • in_claude_worktree covers 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_data and the two git_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.

b3hz4d and others added 2 commits October 17, 2026 11:32
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>
@KuSh
KuSh force-pushed the fix/worktree-git-rewrite-3864 branch from 4c4f055 to ee2af0d Compare October 17, 2026 09:35
@KuSh
KuSh merged commit e542dca into rtk-ai:develop Oct 17, 2026
15 checks passed
@rtk-release-bot rtk-release-bot Bot mentioned this pull request Oct 17, 2026
ryanportfolio added a commit to ryanportfolio/Harness-Firmware that referenced this pull request Oct 17, 2026
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>
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.

rtk hook claude refuses every git command in a worktree, always the same isolation error

7 participants