security-guidance: Stop-hook LLM review broken on Windows when worktree has many untracked files (WinError 206)

Resolved 💬 1 comment Opened May 27, 2026 by Black-coffe Closed May 29, 2026

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)

  1. Windows 11 + Claude Code 2.1.152, security-guidance 2.0.0 installed.
  2. 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.
  3. In a Claude Code session in that repo, ask Claude to Write any new file with a known-bad pattern (e.g. a Python file with SECRET_KEY = os.environ.get(\"SECRET_KEY\", \"dev-secret-123\") + an f-string SQL query).
  4. Let Claude finish the turn (Stop hook fires).
  5. tail ~/.claude/security/log.txt → see git diff error: [WinError 206] and Stop 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)

  1. 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).
  2. Batch the git add call. Chunk surviving into groups of e.g. 200 paths and call git add --intent-to-add -- <chunk> in a loop, accumulating into the same GIT_INDEX_FILE. Slower than (1) but zero new git-version assumption.
  3. 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.)

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗