Skip to content

fix(hooks): detect linked worktrees by .git shape, not path convention - #4490

Closed
al-amin-chayan wants to merge 2 commits into
rtk-ai:developfrom
al-amin-chayan:fix/3864-generic-worktree-detection
Closed

al-amin-chayan wants to merge 2 commits into
rtk-ai:developfrom
al-amin-chayan:fix/3864-generic-worktree-detection

Conversation

@al-amin-chayan

@al-amin-chayan al-amin-chayan commented Oct 9, 2026 •

Copy link
Copy Markdown

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.

  • fix(hooks): leave git unrewritten in Claude Code managed worktrees #3879's 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 plain git worktree add at 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 .git being a file rather than matching a path convention.
  • This PR replaces the path-shape check (in_claude_worktree) with that generic signal (is_linked_git_worktree): git marks a linked worktree by making .git at its root a file containing gitdir: <path>, instead of the directory the main checkout has. Walking up from cwd for the nearest .git entry 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.
  • Rewrote the #3864 regression tests to lay out real .git fixtures with tempfile::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

  • Hand-reviewed the diff for correctness (upward directory walk terminates on parent() == None; std::fs::metadata resolves symlinks so a symlinked .git file is still detected correctly; main-checkout fixture uses a real .git directory and is confirmed rejected)
  • New/updated unit tests in 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 .git directory; rejection when no repository exists at all; end-to-end decide_for_agent_at behavior for both the linked-worktree and main-checkout fixtures
  • Updated src/hooks/hook_cmd.rs's test_claude_payload_cwd_in_worktree_skips_git_rewrite to use a real .git-file fixture instead of a path-string stand-in
  • cargo 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
cwd command before this PR after
<repo>/.claude/worktrees/<name> git status passthrough (per #3879) passthrough
worktree from git worktree add ../<name> git status rtk git status (still refused by Claude Code's guard) passthrough
worktree from git worktree add ../<name> git status && cargo build both rewritten git status && rtk cargo build
main checkout git status rewritten rewritten (unaffected)

Not covered (same as #3879): rtk git … typed directly by the model is still refused by Claude Code's own guard; the model's plain git retry now succeeds.

🤖 Generated with Claude Code

b3hz4d and others added 2 commits October 9, 2026 12:07
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>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ b3hz4d
❌ alaminchayan
You have signed the CLA already but the status is still pending? Let us recheck it.

@rtk-wshm-sync-bot rtk-wshm-sync-bot Bot added bug Something isn't working git hooks worktree labels Oct 9, 2026
@rtk-wshm-sync-bot

Copy link
Copy Markdown

wshm · Automated triage by AI

📊 Automated PR Analysis

🐛 Type bug-fix
🟡 Risk medium

Summary

Replaces the path-convention-based worktree detection (in_claude_worktree) with a generic filesystem check (is_linked_git_worktree) that walks up from cwd looking for a .git file (linked worktree) versus directory (main checkout), fixing #3864's regression for worktrees not created at Claude Code's own .claude/worktrees/<name> path. It threads an optional cwd parameter through the hook decision pipeline and rewrites the associated regression tests to use real temp-directory .git fix…

Review Checklist

  • Tests present
  • Breaking change
  • Docs updated

Linked issues: #3864


Analyzed automatically by wshm · This is an automated analysis, not a human review.

@KuSh

KuSh commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

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 git worktree add ../repo-worktrees/feat through EnterWorktree(path: …) has every git command refused with the #3864 text. With this branch the same session runs git status and git log.

The .git file signal has a cost, though. Claude Code sends the same PreToolUse payload (same fields, same cwd) whether the session is isolated in that worktree or just started there. A plain session in a linked worktree never meets the guard, and rtk git … runs there on develop. With this branch, git output is no longer filtered in any linked worktree, isolated or not. The same goes for every submodule checkout, since a submodule's .git is also a gitdir: file (git log -3 from vendor/lib passes through unrewritten). Plain sessions in a hand-made worktree are a common setup, so this would take git savings away from far more sessions than the ones it fixes.

Measured on this head (a21875f) against develop e542dca, using real claude -p sessions with each build as the hook:

cwd / session develop this PR
git worktree add checkout, EnterWorktree(path) refused runs, unfiltered
same checkout, plain session started there rtk git … runs runs, unfiltered
submodule checkout rtk git … unfiltered
main checkout rtk git … rtk git …

The branch also conflicts with develop now: it was cut from #3879 before #3879 was rebased.

I opened #4634 for the isolated-session-outside-.claude/worktrees/ case. Until it is fixed, [hooks] exclude_commands covers it. The issue also lists what a fix would need: either Claude Code exposing isolation in the hook payload, or a way to tell rtk which worktree locations it uses.

@KuSh KuSh closed this Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working git hooks worktree

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

5 participants