Worktree isolation refuses loops and process substitution that touch no path; error always says "without the redirect"

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

Summary

In a worktree-isolated session, Bash commands containing a loop (for/while/until) or process substitution (<(…)) are refused as "too complex to verify", even when the command performs no filesystem operation at all and therefore has nothing to escape with.

The accompanying error text also always ends with "without the redirect", including for commands that contain no redirect — which sends you looking for the wrong cause.

This is the isolation added in 2.1.222 ("Fixed worktree-isolated sessions and their subagents being able to run destructive git commands against the main checkout; isolation now applies to file edits and Bash in every session type"). I'm not arguing the feature is wrong — failing closed is correct for a security boundary. The report is that the refusal keys on syntactic shape rather than on any reachable path, so it rejects provably-inert commands.

Repro

Claude Code 2.1.228, macOS 15.6 (Darwin 25.6.0), zsh. Start a session in a git worktree (EnterWorktree, or claude --worktree), then run:

for f in one two; do echo "$f"; done

Expected: prints one / two. The command has no filesystem operand.

Actual:

This session is isolated in the worktree /path/to/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 /path/to/repo/.claude/worktrees/<name>
without the redirect.

Note the trailing "without the redirect" on a command with no redirect.

Measured boundary

Five probes in one session, same worktree:

| Construct | Example | Result |
| --- | --- | --- |
| ; / && chains | echo hi; pwd | allowed |
| Command substitution | echo "cwd is $(pwd)" | allowed |
| Redirect | echo hi > /dev/null | allowed |
| for loop | for f in one two; do echo "$f"; done | refused |
| Process substitution | diff <(echo a) <(echo a) | refused |

So $(…) — which can expand to anything — is allowed, while a for over two string literals is refused. That asymmetry is what makes the current rule feel shape-based rather than reach-based.

Impact

Loops are how you poll from a session, so this mostly bites background/CI monitoring inside a worktree:

# refused
until [ "$(gh api repos/O/R/commits/$SHA/status --jq .state)" != "pending" ]; do sleep 45; done

The documented alternative (Monitor with an until-loop) is refused for the same reason, so a worktree session has no in-session way to wait on an external condition. gh run watch works but only covers one provider. Workaround is to commit a wrapper script and invoke it as a single plain command — fine, but it means writing a file to poll a URL.

Suggested fixes (either would help; they're independent)

  1. Allow loops with no filesystem operand. If no word in the body resolves to a path — or more conservatively, if the body contains no redirect, no cd, and no known path-taking command — the loop can't leave the worktree. A for over literal words is the easy case.
  2. Fix the error text. Only append "without the redirect" when a redirect is present; otherwise name the construct that actually triggered the refusal ("loop", "process substitution"). The current message reliably misdirects — I spent a while auditing my commands for a redirect that wasn't there, and initially misdiagnosed this as a rule about compound commands, which it isn't.

Happy to test a fix against the probe table above.

View original on GitHub ↗