[BUG] Windows Git Bash: static analysis false-positives on read-only cd-compound commands cause constant, unsuppressable permission prompts (since 2.1.232 / auto-mode rollout)

Status Fixed / completed
Reported on v2.1.232
Maintainer reply None cached
Activity 10 comments · opened Aug 14, 2026 · closed Aug 20, 2026

Environment

  • Claude Code 2.1.232 (native install), Windows 11 Pro, Git Bash shell
  • Observed independently on two separate machines (different users, different hardware, same day)
  • Onset correlates exactly with 2.1.232 arriving via the native launcher's startup self-update (version files on disk: 2.1.229 → 13 Aug 08:30, 2.1.231 → 13 Aug 09:41, 2.1.232 → 14 Aug 06:01 local; prompts began within hours on both machines), compounding with the 14 Aug auto-mode default rollout.

What happens

On Windows with Git Bash, any Bash command of the shape cd "<absolute path>" && <anything not fully literal> now fails static analysis and falls through to a manual "Do you want to proceed?" prompt. We have collected four distinct reason wordings verbatim:

  1. > Contains simple_expansion On Windows with Git Bash, the final working directory of this cd-compound cannot be statically determined, so relative write targets cannot be checked for Cygwin-emulated symlinks and this request cannot be delegated to the auto-approval classifier.
  2. > Redirect target contains $(cmd) output — path is runtime-determined On Windows with Git Bash, [same continuation]
  3. > Contains brace with quote character (expansion obfuscation) On Windows with Git Bash, [same continuation]
  4. > Compound command contains cd with write operation - manual approval required to prevent path resolution bypass On Windows with Git Bash, [same continuation]

The clearest false positive

Reason 4 above was produced by this command, which contains no write operation of any kind — two sed -n reads and a pipe:

cd "C:/Users/<user>/<project>" && sed -n '/pattern-a/,/pattern-b/p' static/app.js | sed -n 1,60p

Other innocuous shapes reliably prompting:

# read-only loop → "Contains simple_expansion"
cd "C:/Users/<user>/<project>" && for v in 1.60.0 1.58.0; do echo "== $v"; uv run --with "pkg==$v" python -c "..." 2>&1 | tail -2; done

# write to a FULLY ABSOLUTE literal temp path via variable → "Redirect target contains $(cmd) output"
cd "C:/Users/<user>/<project>" && S="C:/Users/<user>/AppData/Local/Temp/claude/<session>/scratchpad" && tool > "$S/out.json" 2>&1

# heredoc script to an absolute literal path → "expansion obfuscation"
cd "C:/Users/<user>/<project>" && cat > "C:/.../scratchpad/probe.py" <<'PY'
...
PY

Note the cd target is always a quoted absolute path, so the "final working directory cannot be statically determined" claim is itself questionable for these commands.

Why the friction is severe

  • Subagents and workflows on Windows generate cd "<abs>" && ... compounds constantly (it is the natural way to scope a command to a project). Users are seeing dozens of prompts per hour during normal agentic work.
  • These prompts never offer "don't ask again" — presumably because a rule can't be derived from a command the analyser couldn't parse — so the friction cannot be reduced from the prompt itself. Hand-written Bash(cmd *) allow rules in settings help, but only apply to sessions started after they are saved.
  • Because this lands together with the auto-mode default rollout, every Windows + Git Bash user receives the behaviour change at once, with no version pinning available on the native installer (it self-updates at launch even with "autoUpdates": false).
  • Related existing reports: #43713 (expansions defeat auto-allow), #46783 (Windows "always allow" writes backslash rules that never match), #86478 / #86451 (auto-mode rollout overriding chosen permission modes).

Expected behaviour

  1. A command whose parts are all read-only (sed -n, grep, head, tail, pipes, no redirects) should pass static analysis regardless of cd, or at minimum remain delegable to the auto-approval classifier.
  2. A command containing no redirect and no write verb should never be described to the user as containing a "write operation".
  3. cd to a quoted absolute path should establish the working directory for the analysis of the rest of the compound.
  4. Where the analyser can identify the offending sub-part (it already prints "The following part requires approval: …"), the prompt should offer a scoped "don't ask again".

Happy to provide more captures — we are logging every distinct wording.

View original on GitHub ↗

9 Comments

Aura-Intel · 16 days ago

Two additions from continued logging on the same environment (Windows 11, Git Bash, 2.1.232, auto mode):

A fifth reason wording

Produced by a workflow clearing a stale index.lock and making a WIP commit inside a git worktree:

cd "/c/Users/<user>/<project>" && rm "repo/.git/worktrees/<wt>/index.lock" && cd repo-worktree && git add -A && git commit -m "..." && git log --oneline -1 && git status --porcelain | head -3
This command changes directory before running git, which can execute untrusted hooks from the target directory. Approve only if you trust it. On Windows with Git Bash, the final working directory of this cd-compound cannot be statically determined, so relative write targets cannot be checked for Cygwin-emulated symlinks and this request cannot be delegated to the auto-approval classifier.

This caution class is at least defensible (it is state-changing), but note the same boilerplate continuation is appended regardless of fit. Side observation: the stale lock itself was plausibly left by an earlier git command interrupted by one of these prompts, so the friction is starting to generate its own cleanup work.

The prompt garbles the command it quotes

A read-only pipeline (grep/sed/echo, no writes) was prompted, and the dialog's own echo of the command replaced a pipe with a comma. Sent:

sed -n "$(grep -n 'def _rel' tasks.py | cut -d: -f1),+16p" tasks.py

Rendered in the approval dialog:

sed -n "$(grep -n 'def _rel' tasks.py, cut -d: -f1),+16p" tasks.py

(| cut became , cut.) If the analysis path sees what the dialog shows, the analyser is judging a corrupted parse of the command, which would explain some of the misclassifications above (e.g. read-only pipelines labelled as containing write operations).

Aura-Intel · 16 days ago

Continued logging, same environment (Windows 11, Git Bash, 2.1.232, auto mode). Now at seven distinct reason wordings in ~12 hours across two machines. Two new ones, both verifiably false positives:

Wording 6: Windows 8.3 short paths read as bash tilde expansion

Tilde in assignment value — bash may expand at assignment time On Windows with Git Bash, the final working directory of this cd-compound cannot be statically determined, [same continuation]

Produced by:

SCR="C:/Users/ABCDEF~1/AppData/Local/Temp/claude/<session>/scratchpad"; cd "C:/Users/<user>/<project>" && cp static/a.css "$SCR/a.css.FIXED" && npm run build 2>&1 | tail -1

The "tilde" is ABCDEF~1DOS 8.3 short-name notation inside a double-quoted literal. Bash performs no tilde expansion there (quoted, and not path-leading); the analyser is misreading a Windows filename convention. Since %TEMP% commonly expands to an 8.3-form path on Windows, any command referencing the temp directory can trip this.

Wording 7: a concrete symlink claim that is false on disk

Path traverses a Cygwin-emulated symlink (Git Bash follows it, Node does not) — manual approval required

Produced by a read-only test invocation:

cd "C:/Users/<user>/<project>/scripts" && uv run --extra dev pytest -q -n auto -m "not serial" $(ls tests/test_*.py | tr '\n' ' ') 2>&1 | tail -8

Unlike the others, this asserts a concrete filesystem fact, so we checked it: a recursive PowerShell scan of the entire traversed tree (hidden files included, .venv included) for any ReparsePoint attribute returned zero results. The directory sits inside a git worktree whose .git is the standard plain pointer file, not a link. There is no symlink, Cygwin-emulated or otherwise, anywhere on the path. The stated reason is fabricated.

Pattern across all seven

The boilerplate continuation ("final working directory of this cd-compound cannot be statically determined…") is appended to every variant regardless of fit — including commands with no writes, no relative paths, and (per above) no symlinks. Combined with the pipe-to-comma mangling shown in the previous comment, the picture is consistent: the analyser is producing confident, specific, and checkably wrong justifications, and each one costs the user a manual approval.

stock-ds · 16 days ago

Also started getting these messages seemingly every few minutes

Tilde in assignment value — bash may expand at assignment time On Windows with Git Bash, the final working directory of this cd-compound cannot be statically determined, so relative write targets cannot be checked for Cygwin-emulated symlinks and this request cannot be delegated to the auto-approval classifier.
ally2sandeep · 16 days ago

Confirming independently on a third machine — Windows 11, Git Bash, native install. 2.1.232 landed via self-update at 13 Aug 18:36 local (on-disk version files: 2.1.229, 2.1.231, then 2.1.232); prompts began the same evening on a repo that had run unattended for months without them. Same wordings as reported here, including reason 4 on a pure read.

Two things to add.

1. A second trigger in the same code path: command size

Separate from the cd-compound cases, a command that the rules would auto-approve is downgraded to ask when it is over 10,000 characters or names more than 8 path-like arguments:

Command exceeds the maximum analyzable length; its full text cannot be security-scanned and requires human review.

In the shipped 2.1.232 bundle these are dge=1e4 and HGa=8. Neither the message nor the constants appear in 2.1.229 or 2.1.231.

Two real cases that tripped it here:

  • a subagent appending ~4KB of Python to a test file with cat >> tests/foo.py <<'PY' … PY
  • python scripts/run_pg_tests.py with 9 test files listed

Same practical effect as the cd-compound false positives: unsuppressable, and permissions.allow does not override it (consistent with #86630 — the downgrade happens after the allow decision).

2. Impact: it blocks unattended runs, it is not only an annoyance

We run multi-hour autonomous sessions — an orchestrator plus subagents and workflows — on a solo-maintained repo with ~2,100 merged PRs. Interactively a prompt costs two seconds. Unattended it halts the run indefinitely, because there is nobody present to click. With permissions.allow unable to override the gate, there is currently no configuration that keeps a Windows autonomous session running; the options are rewriting every command shape our agents emit, or pinning to 2.1.231 and losing the symlink fix.

Worth noting how easily tool-generated commands hit this: subagents routinely prefix with cd "<absolute path>" && … defensively, because a fresh agent cannot be certain of its working directory, especially with git worktrees in play. Nothing in our own prompts or skills teaches that shape — I checked — so it is a model default that recurs with every new agent rather than something a project can prompt its way out of.

Mitigations that would help most, in order

  1. Treat cd to a quoted absolute literal as statically determinable. It is — the ambiguity only exists for computed or expanded targets.
  2. Don't classify read-only commands (sed -n, grep, head, find) as write operations.
  3. Let an explicit permissions.allow match win, or provide a per-project opt-out, so unattended sessions have a supported configuration.
swsenterprises · 16 days ago

Adding a fifth distinct wording, plus a negative control that narrows the scope to Git Bash specifically rather than Windows generally.

5. Contains ansi_c_string

Same continuation as the four above:

Contains ansi_c_string On Windows with Git Bash, the final working directory of this cd-compound cannot be statically determined, so relative write targets cannot be checked for Cygwin-emulated symlinks and this request cannot be delegated to the auto-approval classifier.

Produced by this command:

f=C:/Users/<user>/<project>/src/Component.tsx; tr -d '\r' < "$f" > "$f.lf" && mv "$f.lf" "$f" && echo "CR count now: $(grep -c $'\r' "$f" || echo 0)"; git -C C:/Users/<user>/<project> diff --numstat -- src/Component.tsx

Worth flagging on its own: this command contains no cd token anywhere, yet the reason text asserts "this cd-compound". So the message is misleading about which construct actually tripped the analyser — here it appears to be the ANSI-C quoting ($'\r') and/or the variable-expanded redirect target (> "$f.lf"), neither of which involves cd. Anyone grepping their logs for cd will under-count how often this fires.

Negative control: Git Bash specific, not Windows generally

Same account, same 2.1.232, two Windows 11 machines:

| Machine | bash.exe resolves to | Affected |
|---|---|---|
| A | C:\Program Files\Git\bin\bash.exe (Git for Windows) | yes, constantly |
| B | C:\Windows\System32\bash.exe (WSL shim) | no, never |

Machine B never routes shell through Git Bash, so the check cannot fire there at any version. Flagging it because a second machine looks like a clean control and silently is not one — we used machine B to rule out a version regression and lost several hours to that before checking which bash.exe each one resolved.

Server-side feature flags were byte-identical across both machines (tengu_permission_friction, tengu_harbor_permissions, tengu_classifier_disabled_surfaces, tengu_auto_mode_default_on), so this is not a per-machine flag rollout.

Scale, for severity triage

On machine A, 2,118 of 4,519 Bash calls over five days (46.9%) match the cd "<abs>" && ... shape alone; including variable-expanded redirect targets and ANSI-C quoting takes it to roughly 51%. In a worktree-per-session workflow every command is naturally path-scoped, so about every other tool call now prompts.

Also confirming independently, consistent with #86630: permissions.allow entries do not suppress these. The check runs before allow-rule matching, so no allowlist can address it — including an exact-prefix rule for the leading command.

aclark-aquaveo · 16 days ago

Independent confirmation on a fourth machine, plus one reason wording that does not appear anywhere in this thread yet.

Environment

  • Claude Code 2.1.232 (native install, self-updated), Windows 11 Pro 10.0.26200
  • Git Bash as the Bash tool shell: MINGW64_NT-10.0-26200, bash 5.2.37, git 2.51.2.windows.1
  • permissions.defaultMode: auto, 32 permissions.allow rules, no ask/deny, no enterprise managed-settings.json
  • Onset matches the pattern others report. On-disk version files: 2.1.229 → 12 Aug 14:59, 2.1.231 → 13 Aug 02:56, 2.1.232 → 14 Aug 08:38 local, and claude.exe mtime is 08:38 the same morning. Prompting began that morning across all sessions, on repos that had run prompt-free for months. ~/.claude/settings.json was last modified three days earlier, so no config change is involved.
  • Binary size 307,186,848 → 319,026,336 bytes across 2.1.231 → 2.1.232.

Reproducing the string bisect from #86631 on this machine, as an independent check:

2.1.229: "Cygwin-emulated symlinks" absent
2.1.231: "Cygwin-emulated symlinks" absent
2.1.232: "Cygwin-emulated symlinks" present

Wording 8: Contains shell syntax (string) that cannot be statically analyzed

Verbatim, with the same continuation as the others:

Contains shell syntax (string) that cannot be statically analyzed On Windows with Git Bash, the final working directory of this cd-compound cannot be statically determined, so relative write targets cannot be checked for Cygwin-emulated symlinks and this request cannot be delegated to the auto-approval classifier.

Grepping this thread's body and all comments, shell syntax (string) does not yet appear, so this looks like an eighth distinct wording alongside simple_expansion, ansi_c_string, the untrusted-hooks/git one, the tilde-in-assignment one, and the analyzable-length one.

To be precise about provenance: the wording above is quoted verbatim from the affected user, but they did not capture the paired command, so I cannot attribute it to a specific invocation with certainty. Commands of this shape from the same session are the plausible source — a cd-compound whose later segments contain ordinary single-quoted literals:

cd /c/git/<project>; tr -d '\r' < /tmp/list.txt | xargs -d '\n' git rm --quiet -f --

Nothing there writes through a symlink, and the quoted literals are '\r' and '\n'.

The trigger is the dominant agent shell idiom, and users cannot mitigate it

Worth stating explicitly, because it bears on severity. The Bash tool's own description tells the model:

Working directory persists between calls, but prefer absolute paths — cd in a compound command can trigger a permission prompt.

In practice the model emits cd <project> && ... anyway, and reliably so after anything perturbs the shell's working directory — one cd into a git worktree that is not followed by a cd back, and every subsequent command gets a defensive cd <project>; prefix for the rest of the session. That happened in one session here and produced roughly 40 consecutive manual approvals, every one of them a read-only or repo-local command in a repo the user owns.

Two consequences:

  1. The false-positive rate is not a function of unusual user behaviour. It is a function of how the model writes shell, which the user does not control and cannot configure.
  2. The obvious user-side workaround does not work. Per #86630, the new gate runs after the permission decision and can downgrade allowask, so adding a permissions.allow rule for the offending shape does not suppress the prompt. That leaves affected Windows users with no configuration-level mitigation at all — only rolling back, or hoping the agent stops using cd.

The only mitigation that worked here was agent-side: dropping cd-compounds entirely in favour of git -C <abs path> and the already-persistent working directory. That is a fine long-term habit, but it is not something a user can switch on, and the model has no way to know it is needed until the prompts start.

I would add that the changelog entry quoted in #86631 describes a real bypass worth fixing, and nothing here argues against hardening it. The issue is the blast radius: gating on "there is a cd and something non-literal somewhere in this command" catches essentially all agent-issued shell on Git Bash, including pure reads, while a symlink-free repo has nothing for the check to protect.

joao-petreche · 16 days ago

Additional data points from a Windows 11 + Git Bash setup running several concurrent Claude Code sessions; the matrix below was measured with a positive control in a session running v2.1.232:

2x2 matrix (same session, same day):

| form | scratchpad dir | normal repo dir |
|---|---|---|
| t="C:/abs/path" && echo x > "$t/f.txt" (variable-assigned absolute, no cd) | no prompt | no prompt |
| cd "$dir" && echo y > f.txt (cd-compound + relative target) | prompt | prompt |

The positive control matters: the clean runs are a measurement, not an absence — the same probe discriminates. Notably, in a write target, plain $var expansion of an inline-assigned absolute is resolved statically by the checker (no prompt, top row). This refines the earlier suggestion that the ambiguity covers all "computed or expanded" targets: it is not variable expansion per se that trips the gate. Note the scope — an expanded target of cd itself was not clean in our testing, so the discriminator appears to be cd-compound with a relative/expanded working directory, not the expansion.

Verbatim dialog observed (on a read-only measurement command; the tree contains no symlinks — core.symlinks=false and find -type l returns nothing):

On Windows with Git Bash, the final working directory of this cd-compound cannot be statically determined, so relative write targets cannot be checked for Cygwin-emulated symlinks and this request cannot be delegated to the auto-approval classifier.

Variants observed the same day, on other command shapes: "Contains simple_expansion", "Contains brace with quote character (expansion obfuscation)", "This Bash command contains multiple operations".

Consistent with the separate ansi_c_string report: there the trigger cannot be the variable-expanded redirect target alone, since that form is clean in our matrix — which points at the ANSI-C quoting or the command substitution instead.

Impact: on the auto-mode rollout day this produced dozens of prompts/hour across three working sessions. Consistent with #86630, where permissions.allow rules reportedly do not suppress these prompts.

Workaround that removes most prompts by form (may help others while this is triaged): create probe/temp files via the editor's file-write tool instead of bash heredocs, use absolute literal or variable-assigned-absolute write targets, and avoid cd-compounds with relative writes. The residual cases are probes that genuinely need a relative cwd.

joao-petreche · 16 days ago

Follow-up from the same setup as my earlier comment (Windows 11, Git Bash, v2.1.232), with three additional measurements that may save others some diagnosis time. All measured today, with a human watching the screen in each run, and no PreToolUse hook matching Bash configured in the measuring session (checked — so nothing could have suppressed or injected a prompt decision).

1. A cd that resolves to the current working directory is a no-op and does not prompt — measured, both combinations.

| probe (cwd = the target dir itself) | result |
|---|---|
| cd "<current dir>" && git status | no prompt |
| cd "<current dir>" && echo x > file | no prompt (file written) |
| positive control: cd "<other dir>" && <relative write> — same session | prompt |

This matches the documented behavior (the permissions doc notes the no-op exemption for the cd+git case) and extends it: it also held for the cd+redirect combination here. The practical consequence for agent workflows: the defensive cd <project> && ... prefix that models habitually emit is harmless when the session is already in that directory — the prompts come only from cd into a different directory. If your agent's project directory matches its cwd, that entire class of prompt disappears on its own.

**2. The analyzer reads the content of string literals: a commit message that merely quoted commands triggered the gate.**

A cat notes.md >> LOG.md && git add ... && git commit -m "<message>" && git push — a routine append plus commit, with no executable cd anywhere in the command — prompted with:

Unrecognized redirect shape On Windows with Git Bash, the final working directory of this cd-compound cannot be statically determined, [same continuation as the others]

In this case the only cd-compounds present were inside the quoted commit message text, where the message happened to describe shell commands — yet the reason names "this cd-compound". To be precise about attribution: the executable part did contain the ordinary >> LOG.md append, so the "redirect shape" half of the wording may refer to that; but the cd-compound the reason names does not exist in executable position — it only occurs as quoted prose. At minimum the reason misattributes the construct (consistent with the misleading-reason reports in #76718); at worst the analyzer is parsing string-literal content as syntax.

A later commit of the same shape prompted with "Contains shell syntax (command) that cannot be statically analyzed" (a sibling of the shell syntax (string) wording reported above). That second case is reported for the wording only, and to be exact about it: that command did contain command substitution, so "shell syntax (command)" is an accurate reason there — it is listed because the wording itself appears to be new, not as a misattribution.

Practical tip either way: commit messages that quote trigger-shaped commands currently coincide with prompts — check the executable part of your command before assuming the screen points at a real hazard. "Unrecognized redirect shape" does not appear anywhere in this thread yet, so this looks like a ninth distinct wording, and this one comes with its paired command.

3. Small documented escape hatch worth knowing: a command whose only redirect target is /dev/null does not prompt (per the permissions doc) — so read-only patterns like grep -r pattern . 2>/dev/null are safe even in compound form.

ally2sandeep · 16 days ago

looks like an update is released an hour ago to address all the issues listed here. I am testing and will update either way. Wish me Luck

https://github.com/anthropics/claude-code/releases/tag/v2.1.233#:~:text=Windows%3A%20fixed%20auto,a%20later%20release

Showing cached comments. Read the full discussion on GitHub ↗