Worktree-isolation guard: string-executor check matches every argument position, case-insensitively
Worktree-isolation guard: string-executor check matches every argument, case-insensitively
Version: 2.1.236 (Claude Code)
Platform: Linux (CachyOS, kernel 7.2.0), zsh via the Bash tool
Related: #88776 — same guard, different branch. That issue covers commands
whose AST is not simple. Everything below has a perfectly simple AST.
Summary
While a session is isolated in a worktree, the guard refuses any command in
which any word matches one of a set of string-executing bash builtins. The
check is applied to every argument position, not just command position, and it
is case-insensitive. echo complete is refused.
The tokenizer itself is fine — comments are stripped, quotes resolved, word
boundaries respected. The bug is what the tokens are then compared against, and
where.
Minimal repro
From a session isolated in a worktree (EnterWorktree, or a subagent withisolation: "worktree"), run via the Bash tool:
echo complete # REFUSED
echo completely # runs
Refusal text:
This session is isolated in the worktree \<path\>, but this command runs a string through complete, which can't be verified to stay inside the worktree; run the command directly instead. Refusing to run it — a worktree-isolated session's git operations must target its own worktree.
No git, no path, no pipeline, two tokens.
Full matrix (all as echo <token>, all simple ASTs)
| token | result | note |
|---|---|---|
| eval | refused | bash builtin |
| source | refused | bash builtin |
| trap | refused | bash builtin |
| alias | refused | bash builtin |
| complete | refused | bash builtin |
| bind | refused | bash builtin |
| exec | runs | also invokes; inconsistent with the above |
| command | runs | also invokes; inconsistent |
| builtin | runs | also invokes; inconsistent |
| . | runs | source alias; inconsistent with source |
| bash sh env sudo awk ssh find xargs | run | all can execute strings |
So the list is six bash builtins, and it is neither complete nor consistent —. is source, and exec/command/builtin invoke too, yet all pass, while
external string-executors (bash -c, awk, ssh, xargs) pass as well.
Two distinct defects
1. Matched in argument position, not just command position.
echo complete # REFUSED -- 'complete' is an argument to echo
echo "complete" # REFUSED -- quoting does not help
echo 'complete' # REFUSED -- single quotes do not help either
Quotes are resolved before the comparison, so there is no way to pass one of
these six words as data.
2. Case-insensitive, although shell builtin names are case-sensitive.
echo COMPLETE # REFUSED, and the message echoes back "COMPLETE"
echo Alias # REFUSED, message echoes back "Alias"
COMPLETE and Alias are not bash builtins and could never execute anything.
The refusal message quoting the original casing back is a useful tell that the
comparison is lowercasing before matching.
Correctly handled (for contrast — the tokenizer is not the problem)
echo completely # runs -- word boundary respected, not substring
echo foo-complete-bar # runs -- single shell word, correctly not a match
echo trap_door # runs
echo hi # complete # runs -- comments stripped before matching
Real-world impact
Any CLI with a subcommand named after one of these six becomes unusable while
isolated. Concretely: a todo-graph tool where query.py complete <id> marks a
node done — a natural subcommand name that collides with a bash builtin. The
workaround was an alternate spelling of the same operation
(query.py update <id> --status done).
Because argument position is matched, this also blocks ordinary text: any
command echoing, grepping for, or passing the literal words complete,
source, alias, trap, bind or eval — all common English — is refused
regardless of context. Verified:
grep -rn "source" README.md # REFUSED
You cannot grep a repository for the word "source" while isolated. That is a
broad class of false positive across six very common English words.
Suggested fix
- Only test the word in command position, not every argument.
- Drop the case-insensitivity, or keep it only if the check is also restricted
to command position (where a case-insensitive filesystem could matter).
- Decide the list deliberately: either it is "builtins that defer a string"
(then . belongs and bind/alias/trap are arguably about registration,
not immediate execution), or it is "anything that can execute a string"
(then bash -c, awk, xargs, ssh belong). Currently it is neither.
Note that even with argument position fixed, this guard's stated purpose is
keeping git operations inside the worktree; a command with no git in it
arguably should not reach this branch at all — which is the point #88776 makes
about the too-complex bail ordering.