Worktree isolation refuses every pwsh/powershell/cmd invocation with any argument; git-as-path-segment also refused inside interpreter text

Status Open
Reported on v2.1.283
Maintainer reply None cached
Activity 2 comments · opened Sep 28, 2026

Preflight Checklist

  • [x] I have searched existing issues. The worktree guard has many reports (#84530, #95611, #97225, #95122, #86036, #84135, #84720) but none cover pwsh/powershell/cmd being refused as argv[0].
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code (2.1.283)

What's Wrong?

In a worktree-isolated session (after EnterWorktree), the Bash tool refuses every pwsh invocation that has at least one argument. No git token, no path, no denylisted shell word is needed to trigger it:

$ pwsh -NoProfile -Command 'Write-Output "ok"'
This session is isolated in the worktree /home/me/git/wt/dotfiles/test-run, but this
command runs pwsh in a plain command; what it reads or is handed as shell text cannot
be shown not to run git. Refusing to run it — a worktree-isolated session's git
operations must target its own worktree. Run the plain command from /home/me/git/wt/dotfiles/test-run.

Every variant is refused identically, with cwd already set to the worktree:

| Command | Result |
|---|---|
| pwsh -NoProfile ./test.ps1 (the repo's documented test command) | refused |
| cd <worktree> && pwsh -NoProfile ./test.ps1 | refused |
| pwsh -NoProfile -WorkingDirectory <worktree> -File <worktree>/test.ps1 | refused |
| pwsh -NoProfile -Command 'Write-Output "ok"' | refused |
| same from a general-purpose subagent | refused |
| same with dangerouslyDisableSandbox: true | refused |

The refusal text says "Run the plain command from <worktree>", which is exactly what was done. There is no rewrite that satisfies the guard.

For comparison, other interpreters are not treated this way in the same session:

| Command | Result |
|---|---|
| python3 -c 'print(1)' | allowed |
| python3 /tmp/probe.py | allowed |
| perl -e 'print "ok\n"' | allowed |
| bash -c 'echo ok' | allowed |
| python3 -c 'import os; print(os.path.exists("/home/me/git/dotfiles"))' | refused: "names git in a form too complex to verify" |

The last row is a second, separate false positive: the substring git inside interpreter text is refused even when it is only a path segment (~/git/...), which is a common place to keep repositories.

Why it happens (read out of the 2.1.283 binary)

The guard keeps a set of shells:

nL = new Set([...INe /* bash sh zsh dash ksh mksh ash hush rbash */,
              "fish","csh","tcsh","rsh", ...rDt /* busybox toybox wsl su runuser */,
              "cmd","powershell","pwsh"])
Zet = new Set([...nL, ...aS("export declare typeset local readonly")])

For a simple command whose argv[0] is in nL, this branch runs (minified names kept):

if (n.simple && nL.has(xe)) {
  ...
  return De && (nn === void 0 || EL(nn, n) || /^(?:su|runuser)$/.test(xe))
      || /^(?:cmd|powershell|pwsh)$/.test(xe) && Ie.length > 0     // <-- any argument at all
      || /^[-+]/.test(nn ?? "")
      || Pp(nn ?? "")
      || Rt.some((Wt) => ntt.test(Wt))
    ? vX(xe) : ...
}

and vX produces the message:

function vX(e) {
  let n = kc(e);
  if (_h.test(n)) return n1;                       // _h = /^git(?:\.exe|\.real|-[a-z][\w-]*)?$/i
  return Zet.has(n) || a5.has(n)
    ? `runs ${n} ${kp}; what it reads or is handed as shell text cannot be shown not to run git`
    : null;
}

So pwsh, powershell and cmd with any operand are refused unconditionally. bash -c '<text>' gets better treatment: its payload is re-parsed by cv(), which only refuses when the text fails to parse or matches /git/i. python, perl, ruby, node go through the g1 interpreter-model map and are allowed unless their inline script matches /git/i. pwsh -Command '<text>' and pwsh -File <path> have no equivalent path and fall straight into the refusal.

The git-substring rule is the same shape elsewhere:

Exn = /git/i;   // applied to the whole command text when the command is not "simple"
// in cv(): if (/git/i.test(n)) return `hands ${e} text naming git ...`

which is why a path like /home/me/git/dotfiles inside a python -c string is refused.

The docs (https://code.claude.com/docs/en/worktrees#how-claude-code-enforces-isolation) say "For PowerShell commands, Claude Code applies only the working-directory check", but that describes the PowerShell tool. pwsh launched from the Bash tool gets the full command-shape check and there is no way through it.

What Should Happen?

Any of these would fix it, most preferred first:

  1. Treat pwsh -File <path> / pwsh <path.ps1> / pwsh -Command <text> like bash -c: allow when the operand is a path inside the worktree (or when the text does not match /git/i), instead of refusing on Ie.length > 0. The binary already ships a PowerShell parser (CLAUDE_CODE_PWSH_PARSE_TIMEOUT_MS), so the -Command payload could be parsed the way cv() parses shell text.
  2. At minimum, honor -WorkingDirectory <worktree> / cwd = worktree for pwsh the way the working-directory check does for the PowerShell tool.
  3. For the git substring rule: match git as a command token (_h), not as a substring of a path. ~/git/<repo> is a common layout and turns this into a constant false positive.
  4. Make the refusal text accurate. "Run the plain command from <worktree>" is not actionable when that is already what was run.

Error Messages/Logs

This session is isolated in the worktree /home/me/git/wt/dotfiles/test-run-20260928, but this command runs pwsh in a plain command; what it reads or is handed as shell text cannot be shown not to run git. Refusing to run it — a worktree-isolated session's git operations must target its own worktree. Run the plain command from /home/me/git/wt/dotfiles/test-run-20260928.
This session is isolated in the worktree /home/me/git/wt/dotfiles/test-run-20260928, but this command names git in a form too complex to verify that it stays inside the worktree. Refusing to run it — a worktree-isolated session's git operations must target its own worktree. Split it into plain, separate commands and run them from /home/me/git/wt/dotfiles/test-run-20260928.

Steps to Reproduce

  1. In any git repo: git worktree add ../wt/probe -b probe origin/main
  2. In a Claude Code session, have Claude call EnterWorktree with path = that worktree.
  3. Ask Claude to run pwsh -NoProfile -Command 'Write-Output ok' → refused.
  4. Ask Claude to run python3 -c 'print(1)' → allowed.
  5. Ask Claude to run python3 -c 'print("/home/me/git/x")' → refused ("names git").

Workaround that works: ExitWorktree (keep), run pwsh -NoProfile -WorkingDirectory <worktree> -File <worktree>/test.ps1 from the main checkout, EnterWorktree again. The tests in question only create temp git repos under Pester's TestDrive, so nothing in either checkout is touched, but the guard cannot know that and offers no path to express it.

Claude Model

Claude Fable 5.1 (irrelevant — the refusal is a static check before the command runs)

Is this a regression?

Unknown. The interpreter denylist branch has been present since 2.1.222 per #84530; I have not tested earlier versions with pwsh.

Claude Code Version

2.1.283

Platform

CLI (native install, ~/.local/share/claude/versions/2.1.283)

Operating System

WSL2 (Ubuntu) on Windows 11, kernel 6.18.33.2-microsoft-standard-WSL2

Terminal/Shell

bash (Bash tool); pwsh 7.x installed in WSL

Additional Information

Related: #84530 (interpreter denylist, maintainer said "working as designed", later auto-closed by the stale bot), #95611 (fifteen shell words refused in any argv position), #97225 (commands that never leave the worktree refused). This report is the pwsh/powershell/cmd branch, which none of those cover, plus the git-as-path-segment substring match.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗