Worktree isolation: string-executing builtins (eval, enable) are matched in argument position — `echo eval` is refused
Summary
In a worktree-isolated session, a command is refused when a string-executing shell builtin (eval, enable, …) appears anywhere on the command line, including as a plain argument to another command. The classifier appears to scan tokens rather than resolve the command being invoked.
echo eval is not an invocation of eval. It writes four characters to stdout.
This is adjacent to #85931 (loops and process substitution refused on syntactic shape) but a distinct mechanism: that one keys on shape, this one keys on a token in a non-command position.
Repro
Claude Code 2.1.237, macOS (Darwin 25.5.0), zsh. Start a session in a git worktree, then:
echo eval
Expected: prints eval.
Actual:
This session is isolated in the worktree /path/to/repo/.claude/worktrees/<name>, but this
command runs a string through eval, 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. Run the equivalent from
/path/to/repo/.claude/worktrees/<name> without the redirect.
echo enable fails identically, naming enable. Both are provably inert: no path operand, no redirect, no subshell.
Why it bites in practice
enable is an ordinary English word and a subcommand of the claude CLI itself. In a worktree session, claude plugin enable <name> --scope user is refused — a command that touches no repository state at all, whose only sin is that its subcommand shares a name with a bash builtin. The workaround an agent found was to use claude plugin install, which happens to install-and-enable.
The trailing "without the redirect" is also wrong here, as #85931 already notes for its own case: there is no redirect.
Suggested fix
Resolve the command in command position rather than scanning every token. eval/enable/source as an argument to echo, grep, printf or a CLI subcommand is not an invocation of them.
Related
- #85931 — same guard, refusal keyed on syntactic shape rather than a reachable path
- #86340 — same guard, parser abort blocking all Bash
- #84258 — same guard, read-only
git -Cagainst the main checkout - #87967 — the documented
!workaround hits the identical block
All four and this one share a shape: the isolation boundary is right, and the classifier is broader than the boundary. Failing closed is correct for a security feature; the reports are that provably-inert commands land on the closed side.