[Bug] Worktree isolation blocks all Bash commands due to parser abort in bash-command classifier

Status Open
Reported on v2.1.227
Maintainer reply ✓ Yes — bcherny
Activity 4 comments · opened Aug 13, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

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

[]

View original on GitHub ↗

3 Comments

bcherny collaborator · 15 days ago

Could not reproduce on v2.1.233 (macOS, darwin-arm64).

Steps tried:

  1. git init a fresh repo with one commit; start claude in it and accept the trust prompt.
  2. Ask Claude to call EnterWorktree (name repro2), then run Bash pwd, echo hi, git status, /usr/bin/git status.
  3. Repeat in print mode (claude -p) and with a subagent launched with isolation: "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 usual git 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 an isolation: "worktree" subagent) refuses, on purpose, any command whose shell syntax the safety analyzer can't fully resolve — things like $(...) command substitution, ${VAR} expansions, for/while loops, 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 literal pwd; 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 pwd refused, please paste the exact command string from the tool call in the transcript (.jsonl under ~/.claude/projects/) and the [worktree] blocked shell exec line from a --debug log — that would point at something environment-specific we haven't found.

🤖 Generated with Claude Code

github-actions[bot] · 15 days ago

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.

Dakaric · 14 days ago

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 updatedInput before 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:

for b in bash /bin/bash /usr/bin/bash /usr/local/bin/bash /opt/homebrew/bin/bash; do command -v "$b" >/dev/null 2>&1 && exec "$b" <launcher.sh> <compress.py> <original argv…>; done; <original command>

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 bare ls sim/aend_sim/engine or git 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 literal pwd/echo passed for me too) — but any PreToolUse hook that wraps commands via updatedInput (compressors, sanitizers, loggers) turns every wrapped command into a guard refusal inside worktree-isolated sessions.

Two suggestions:

  1. Have the refusal message echo the command string the guard actually evaluated. A hook rewrite would then be immediately visible instead of looking like the guard refusing ls.
  2. Document (in the worktrees doc) that updatedInput rewrites 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": false in ~/.claude/token-optimizer/config.json (or env TOKEN_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.

Showing cached comments. Read the full discussion on GitHub ↗