[Bug] Worktree isolation blocks all Bash commands due to parser abort in bash-command classifier
Bug Description
Title: Worktree-isolated sessions refuse every Bash command, including pwd (regression in 2.1.227)
---
Since 2.1.227, any session or subagent with worktree isolation active refuses every Bash command — not just compound or git ones. Still broken on 2.1.231.
Repro (fresh 2.1.231 session, macOS 25.6.0 arm64, git repo):
1. EnterWorktree (name: anything) — worktree created at <repo>/.claude/worktrees/<name>
2. Run Bash: pwd
Actual:
This session is isolated in the worktree /Users/me/projects/<repo>/.claude/worktrees/<name>,
but this command is too complex to verify that it stays inside the worktree; break it into
plain, separate commands. Refusing to run it — a worktree-isolated session's git operations
must target its own worktree. Run the equivalent from <worktree> without the redirect.
Identical refusal for echo hi, ls, git status, /usr/bin/git status, /usr/bin/git diff --staged. Read/Grep/Glob still work, so the session looks alive but can't build, test, or commit.
Expected: pwd is a plain, single, non-git command and should run.
Diagnosis from the bundle: the isolation guard is fail-closed on parsed.kind !== "simple", and the bash-command classifier is returning non-simple for every input in these sessions — so the guard's first branch emits the "too complex" refusal unconditionally. A tree-sitter parse abort (kind: "too-complex", reason "Parser aborted") is the plausible path; in one --debug log of an affected session the parser did log tree-sitter: loaded from disk successfully, so the failure may be per-call rather than at init.
Scope / evidence from my local transcripts:
- 0 refusals across ~2000 Bash calls in isolated worktrees up to 2.1.220; 373 refusals on 2.1.227/2.1.228.
- Every affected session refuses from its first Bash call; one long-running session flipped from all-ok to all-refused exactly when 2.1.227 records begin.
- Only isolation-enabled sessions are hit (EnterWorktree, Agent isolation: "worktree"). A normal session with cwd inside the same worktree runs Bash fine.
- Ruled out a stale-binary artefact: reproduced in a brand-new process on 2.1.231.
Impact: worktree-isolated agents can't run anything — they burn a full session and report back "Bash is non-functional, I could not commit."
Environment Info
- Platform: darwin
- Terminal: WarpTerminal
- Version: 2.1.231
- Feedback ID: 32a38b4f-3bf8-4c47-9c76-ca0251f56cc3
Errors
[]Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Could not reproduce on v2.1.233 (macOS, darwin-arm64).
Steps tried:
git inita fresh repo with one commit; startclaudein it and accept the trust prompt.EnterWorktree(namerepro2), then run Bashpwd,echo hi,git status,/usr/bin/git status.claude -p) and with a subagent launched withisolation: "worktree".Observed: every plain command ran normally from inside
<repo>/.claude/worktrees/repro2— no "too complex to verify that it stays inside the worktree" refusal. Expected: the same. Checking the command classifier directly,pwd,echo hi,ls,git status,/usr/bin/git diff --staged,FOO=1 pwd, pipes,2>&1, and even the usualgit commit -m "$(cat <<'EOF' ... EOF)"all classify as plain and pass the worktree guard.Assessment: intended today, but the expectation is reasonable and the message is misleading. What actually happens: a worktree-isolated session (entered via
EnterWorktree,--worktree, or anisolation: "worktree"subagent) refuses, on purpose, any command whose shell syntax the safety analyzer can't fully resolve — things like$(...)command substitution,${VAR}expansions,for/whileloops, or a stray carriage-return character in the command — because such text could hide a git call that targets the shared checkout. Two things line up with your timeline: that check has existed for isolated subagents for a while, but starting around v2.1.222 it also applies to sessions that entered a worktree themselves, so a session that used to run those constructs freely now refuses them. It cannot, as far as we can tell, refuse a literalpwd; the "tree-sitter: loaded from disk" log line you quoted also does not exist in Claude Code, so some of the diagnosis in the report looks inferred rather than observed. We should make this clearer by having the refusal name the construct that tripped it (e.g. "contains$(...)command substitution") instead of the generic "too complex", and by mentioning the rule in the worktree docs.If you still see a literal
pwdrefused, please paste the exactcommandstring from the tool call in the transcript (.jsonlunder~/.claude/projects/) and the[worktree] blocked shell execline from a--debuglog — that would point at something environment-specific we haven't found.🤖 Generated with Claude Code
We weren't able to reproduce this. Could you provide steps to trigger the issue — what you ran, what happened, and what you expected? This issue will be closed automatically if there's no activity within 7 days.
Found a reproducible cause for the "guard rejects trivially simple commands" class of reports that does not reproduce on vanilla installs, which may explain the needs-repro state here: a third-party PreToolUse hook that rewrites the Bash command via
updatedInputbefore the worktree guard parses it.Setup: Claude Code 2.1.233 (macOS 15), session isolated in a native worktree under
.claude/worktrees/<name>, with the token-optimizer plugin (alexgreensh/token-optimizer 5.11.85) enabled. Its Bash PreToolUse hook rewrites whitelisted read-only commands (ls,find,grep,git status/log/diff/show/branch,tail,wc, test runners, …) into an output-compression wrapper:The worktree isolation guard then evaluates this rewritten command, classifies the for-loop as not statically verifiable (
kind !== "simple"), and refuses with "is too complex to verify that it stays inside the worktree; break it into plain, separate commands … without the redirect" — for what the model actually submitted as a barels sim/aend_sim/engineorgit status(relative path inside the worktree, no redirect, no pipe).Why it looks random: the plugin categorically excludes commands containing shell metacharacters (
;|&$(){}<>…) from rewriting. So compound commands with pipes/redirects reach the guard in their original, statically verifiable form and pass, while bare single commands get rewritten and are refused. In my session the plugin's own rewrite log (bash-rewrites.jsonl) correlated 1:1 with the refusals: every refused command was logged as rewritten; every passing command was absent from the log.So "it cannot refuse a literal
pwd" holds for vanilla installs (a literalpwd/echopassed for me too) — but any PreToolUse hook that wraps commands viaupdatedInput(compressors, sanitizers, loggers) turns every wrapped command into a guard refusal inside worktree-isolated sessions.Two suggestions:
ls.updatedInputrewrites are what the isolation guard evaluates, so hook authors know they must emit statically verifiable commands or skip rewriting in isolated sessions.Workaround for affected token-optimizer users:
"v5_bash_compress": falsein~/.claude/token-optimizer/config.json(or envTOKEN_OPTIMIZER_BASH_COMPRESS=0), or patch the hook to skip rewriting when the session cwd is under.claude/worktrees/. I've filed the plugin-side issue as well.