Worktree isolation guard never expands `~`, refusing commands that provably stay inside the worktree
Worktree isolation guard never expands ~, refusing commands that provably stay inside the worktree
Edited after further bisection. The original report blamed "a tilde that passed through a shell variable". That was too narrow — a bare literal~is also refused when it reachesgit -C. The accurate rule is below, and it is simpler: the guard does not expand~at all.
Environment
- Claude Code 2.1.238
- macOS 26.6.1 (darwin)
- Shell: zsh
- Session entered a worktree via
EnterWorktreewithpath
Summary
In a session isolated to a git worktree, the Bash guard refuses commands whose
paths it cannot statically resolve. ~ is never expanded. $HOME always is.
Because ~ is left unresolved, a path that is plainly inside the worktree
looks unverifiable, and the command is refused. Substituting $HOME for ~ —
same path, same target, same semantics — makes every one of those commands pass.
There are two refusal messages, and they are two code paths noticing the same
unresolved ~:
This session is isolated in the worktree <path>, but this command is too
complex to verify that it stays inside the worktree. Refusing to run it — a
worktree-isolated session's git operations must target its own worktree.
Split it into plain, separate commands and run them from <path>.
This session is isolated in the worktree <path>, but this command points git
at a directory computed at runtime (-C ~/…), which can't be verified before it
runs. Refusing to run it — a worktree-isolated session's git operations must
target its own worktree. Run the equivalent from <path> without the redirect.
git -C is checked whenever its path argument contains ~, literal or not.
Other commands are checked only when the path reaches them through a variable.
Reproduction
git worktree add ~/src/worktrees/myrepo/probe
Then EnterWorktree with path: /Users/<you>/src/worktrees/myrepo/probe.
Inside the isolated session:
git -C ~/src/worktrees/myrepo/probe status --short # refused
git -C $HOME/src/worktrees/myrepo/probe status --short # passes
Both name the session's own worktree. Only the spelling of the home directory
differs.
Bisection
All rows run inside the isolated session unless noted. Same worktree, same
session, one variable changed at a time.
| command | result |
| --- | --- |
| full compound with W=~/…, not isolated | passes |
| the same command, isolated | refused ("too complex") |
| echo hi \| tr a-z A-Z | passes |
| echo hi > probe3.txt && cat probe3.txt | passes |
| W=/Users/<you>/…/probe; echo hi > "$W/probe4.txt" | passes |
| W=/Users/<you>/…/probe; git -C "$W" status \| head -3 | passes |
| W=$HOME/…/probe; git -C "$W" status \| head -3 | passes |
| git -C $HOME/…/probe status --short \| head -3 | passes |
| cat ~/…/probe/AGENTS.md \| head -1 | passes |
| W=~/…/probe; git -C "$W" status \| head -3 | refused ("too complex") |
| W=~/…/probe; cat "$W/probe3.txt" \| tr a-z A-Z | refused ("too complex") |
| git -C ~/…/probe status --short \| head -3 | refused ("computed at runtime") |
Pipes, output redirection, &&, variable assignment, git -C, and $HOME in
any position are all accepted. Only ~ is unresolvable, and only paths
carrying it are refused.
I did not probe loops, heredocs, or command substitution, so an unexpanded ~
is a confirmed trigger rather than provably the only one.
Impact
The refusal is easy to misread as a limit on command complexity — the first
message says so in as many words, and "split it into plain, separate commands"
is the advice it gives. Splitting does not remove the ~, so the split
commands are refused too. In my own logs I have 24 recorded instances of this
over 12 days, every one of them attributed to compound-command complexity
rather than to the path.
Common project conventions walk straight into it. A convention that puts
worktrees under ~/src/worktrees/<repo>/<branch>, combined with one that
prefers git -C <path> over cd, produces the refused form as the idiomatic
spelling:
W=~/src/worktrees/myrepo/branch; git -C "$W" log --oneline -5
Suggested fix
Expand a leading ~/ to the user's home directory during the guard's static
analysis, the same way the shell would, before deciding whether the path
escapes the worktree. ~/ is unambiguous and resolvable without executing
anything — the guard already does the equivalent for $HOME.
Two smaller fixes worth making regardless:
- Name the real blocker in the message. "This path uses
~, which cannot
be resolved before the command runs — use $HOME or an absolute path"
points at the fix. "Too complex to verify" and "split it into plain,
separate commands" point away from it.
- Drop "git operations" from the first message. It fires on commands with
no git in them:
``bash``
W=~/…/probe; cat "$W/probe3.txt" | tr a-z A-Z # refused
Workaround
Write $HOME instead of ~ in any path used inside a worktree-isolated
session. It resolves in every position tested: bare, in a variable, and as the
argument to git -C.