Worktree isolation guard never expands `~`, refusing commands that provably stay inside the worktree

Status Open
Reported on v2.1.238
Maintainer reply None cached
Activity 0 comments · opened Aug 21, 2026

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 reaches git -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 EnterWorktree with path

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.

View original on GitHub ↗