security-guidance: Stop-hook LLM review broken on Windows when worktree has many untracked files (WinError 206)
Environment
- Claude Code CLI version: 2.1.152
- OS: Windows 11 Pro 10.0.22631
- Plugin version: security-guidance 2.0.0 (from
claude-plugins-official) - Provider: Anthropic 1P (subscription)
- Python: 3.x on PATH
Symptom
In a repo with a large tmp/ (or any large .gitignore'd / untracked directory) — in my case ~1300 untracked files, mostly PNG/JSON dumps — the Stop-hook LLM review never runs. PostToolUse pattern matching still works (so subprocess(..., shell=True) warnings fire), but the LLM diff review is silently skipped, so contextual findings (hardcoded secrets, SQL injection via f-string interpolation, etc.) are never reported back to Claude.
The plugin debug log shows the same failure on every Stop event:
[2026-05-27 09:58:35.886] Stop hook: review_set=1309 base=HEAD dirty_now=1309 changed_since=1309
[2026-05-27 09:58:35.993] git diff error: [WinError 206] The filename or extension is too long
[2026-05-27 09:58:35.993] Stop hook: diff against 71322f8c2287 failed — falling back to HEAD
[2026-05-27 09:58:36.101] git diff error: [WinError 206] The filename or extension is too long
[2026-05-27 09:58:36.101] Stop hook: no changes since baseline
Stop hook: no changes since baseline is then treated as a clean review, so nothing is sent to the LLM.
Reproduction (minimal)
- Windows 11 + Claude Code 2.1.152, security-guidance 2.0.0 installed.
- In any git repo, populate an untracked directory with ~1200+ files whose paths average ~50–70 chars (e.g.
mkdir tmp && python -c "import os; [open(f'tmp/file_{i:06}_with_a_reasonably_long_name.bin','w').close() for i in range(1300)]").tmp/does not need to be in.gitignore— being untracked is enough. - In a Claude Code session in that repo, ask Claude to
Writeany new file with a known-bad pattern (e.g. a Python file withSECRET_KEY = os.environ.get(\"SECRET_KEY\", \"dev-secret-123\")+ an f-string SQL query). - Let Claude finish the turn (Stop hook fires).
tail ~/.claude/security/log.txt→ seegit diff error: [WinError 206]andStop hook: no changes since baseline. The LLM review never ran; the secrets/SQLi findings are never surfaced.
Same issue reproduces with a fresh clone of any repo plus a synthetic large untracked dir — it's not project-specific.
Root cause (best guess from reading 2.0.0 source)
In hooks/gitutil.py, _temp_index() (~L91-141) does:
if untracked_paths is None:
add_args = [\".\"]
elif untracked_paths:
surviving = [p for p in untracked_paths
if os.path.lexists(os.path.join(cwd, p))]
add_args = [\"--\"] + surviving if surviving else None
...
subprocess.run(
[*GIT_CMD, \"add\", \"--intent-to-add\"] + add_args,
cwd=cwd, capture_output=True, text=True, timeout=10,
env=env,
)
When untracked_paths has ~1300 entries averaging ~60 chars, the full argv blows past Windows' ~32 767-char CreateProcess lpCommandLine limit and Python raises OSError: [WinError 206]. The exception bubbles out of _temp_index into get_git_diff() (L391-427), where the catch-all except (subprocess.TimeoutExpired, FileNotFoundError, OSError) logs it as git diff error: ... even though the failing call is actually git add --intent-to-add. The empty/None diff then flows down to the Stop hook's no changes since baseline short-circuit and review is skipped.
POSIX ARG_MAX is much higher (Linux ~2 MB, macOS ~256 KB), so the same untracked-count never hits this on those platforms — which is probably why this hasn't been reported before. On Windows, anything north of ~500 medium-length untracked paths starts being at risk; ours hit it cleanly.
Suggested fixes (ranked)
- Use
git add --pathspec-from-file=<tempfile> --pathspec-file-nul(git ≥ 2.25, Jan 2020 — well below the realistic minimum git version users have). Writes the path list to a temp file and passes a single short argv; no argv-length ceiling. This is the cleanest fix and also speeds up the call (kernel doesn't have to copy a 90 KB argv). - Batch the
git addcall. Chunksurvivinginto groups of e.g. 200 paths and callgit add --intent-to-add -- <chunk>in a loop, accumulating into the sameGIT_INDEX_FILE. Slower than (1) but zero new git-version assumption. - Detect the Windows argv ceiling and fall back to
git add -N .. Simplest, but reintroduces the whole-worktree stat that the targeted-add path was added to avoid (see the docstring on_temp_index), so this is a strict regression on big repos. Only useful as a last-resort safety net.
Either (1) or (2) on its own resolves the symptom. Worth pairing with: relabel the except OSError log line in get_git_diff to distinguish git add failures from real git diff failures, so the next person debugging this doesn't chase git diff like I did initially.
Why this matters
Pattern matching (layer 1) catches regex-recognizable surface (shell=True, yaml.load, etc.) — useful but narrow. The contextual LLM review (layer 2) is where the plugin earns its keep on findings like \"hardcoded fallback secret\" and \"f-string SQL where the variable is user input\". On any Windows project with a large tmp/, scratch dir, or untracked asset folder, layer 2 is currently a silent no-op and users have no signal that it's broken — the plugin reports no security issues found regardless.
Relevant log section (full)
[2026-05-27 09:57:53.776] Processing: hook_event=Stop, tool=
[2026-05-27 09:57:53.776] extensibility: loaded 5736 chars from C:\Projects\senseti-doker\.claude\claude-security-guidance.md
[2026-05-27 09:57:53.955] Stop hook: review_set=1308 base=HEAD dirty_now=1308 changed_since=1308
[2026-05-27 09:57:54.083] git diff error: [WinError 206] The filename or extension is too long
[2026-05-27 09:57:54.083] Stop hook: diff against 71322f8c2287 failed — falling back to HEAD
[2026-05-27 09:57:54.197] git diff error: [WinError 206] The filename or extension is too long
[2026-05-27 09:57:54.197] Stop hook: no changes since baseline
(Same three-line pattern repeats on every Stop hook in the session.)
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗