Repository navigation
security-guidance: push-sweep re-reviews the same diff because commit review records reviewed SHAs only after the review completes #5425
Description
Summary
In security-guidance 2.0.7, the commit reviewer records reviewed SHAs only after the
review finishes, but the push-sweep's dedup reads that record at push time. When
git commit and git push happen back to back (the common agent flow), the push-sweep
starts before the record exists, so prefix_advanced stays 0 and the same diff is sent
through a second full agentic review.
Over 6 days on one machine this produced 67 redundant reviews out of 208 (32%), each
carrying the full unified diff in the prompt.
Evidence 1 — plugin debug log (~/.claude/security/log.txt)
A single commit, committed and pushed in the same flow:
[2026-08-15 22:14:26.082] Commit review: detected git commit in command
[2026-08-15 22:14:26.213] Commit review: 1/1 sha(s) resolved, 20 files <- review starts
[2026-08-15 22:15:21.115] Push sweep: range=1 prefix_advanced=0 base=<sha> tail=1
[2026-08-15 22:15:53.054] Commit review: no security issues found <- SHA recorded here
[2026-08-15 22:17:19.931] Push sweep: no new findings
The push-sweep at 22:15:21 re-reviewed the very commit whose review did not complete
until 22:15:53 — 32 s later.
Distribution across all push-sweeps that actually ran a review (n=49):
| pattern | count |
|---|---|
range=1 prefix_advanced=0 tail=1 |
37 |
other ranges, prefix_advanced=0 |
10 |
prefix_advanced>0 (dedup partially worked) |
2 |
Separately, Push sweep: every pushed commit already reviewed fired 10 times — dedup does
work when the push happens long after the commit, which is consistent with the race.
Evidence 2 — transcripts
Scanning ~/.claude/projects/**/*.jsonl for prompts starting with
Review this change for security vulnerabilities between 2026-08-13 and 2026-08-19:
- 208 occurrences, 141 unique prompt texts, 65 texts appearing 2–3 times
- every duplicate pair is in a different Agent SDK session file (0 duplicates within a
single session), i.e. these are genuinely separate review runs, not a logging artifact - example: one 222,835-char prompt at 15:31:10 / 15:31:53 / 15:47:06
- total 12,383,810 chars (~3.5M tokens); the duplicate share is ~1.1M tokens
Root cause
hooks/security_reminder_hook.py:
- ~line 1341:
concrete_guidance, vulns, _am = _agentic_review_with_race(...) - line 1372:
_append_reviewed_shas(repo_root, full_shas, vulns_found=len(vulns or []))
The append needs vulns, so it cannot run until the review returns. That review is long by
design — _agentic_review_with_race waits SG_AGENTIC_RACE_DELAY_S (default 180 s) before
starting the fallback leg, and agentic_review allows SG_AGENTIC_MAX_TURNS (default 18).
Meanwhile the push-sweep at line 1657 does reviewed = _load_reviewed_shas(repo_root) and
sees nothing.
There is no provisional record: grepping lines 1200–1372 shows _append_reviewed_shas only
at 1372.
Suggested fix
Write the SHAs before the review starts, then update the row after:
_append_reviewed_shas(repo_root, full_shas) # claim, vulns_found=0
concrete_guidance, vulns, _am = _agentic_review_with_race(...)
_append_reviewed_shas(repo_root, full_shas, vulns_found=len(vulns or [])) # dedup-by-sha
# keeps newest_append_reviewed_shas already dedups by SHA keeping the newest row, so the second call
overwrites the provisional one. If claiming a SHA that is never actually reviewed (crash
mid-review) is a concern, an in-flight marker the push-sweep also consults would be
equivalent and safer.
Secondary issue — reviewed-SHA state splits across git worktrees
hooks/diffstate.py:
def _reviewed_shas_path(repo_root):
gd = _git_dir(repo_root)
return os.path.join(gd, _REVIEWED_SHAS_BASENAME) if gd else None_git_dir resolves git rev-parse --git-dir, which in a linked worktree returns
<common>/.git/worktrees/<name> rather than the shared .git. Each worktree therefore
keeps its own sg-reviewed-shas, so a commit reviewed in one worktree is not known to
another. The docstring says the record is "per-clone", which suggests
--git-common-dir was intended. This is independent of the race above.
Environment
- security-guidance 2.0.7 (verified byte-identical to
mainfor
security_reminder_hook.pyandreview_api.py) - Claude Code on macOS
- repository uses multiple linked git worktrees
- no relevant env overrides at the time of measurement (defaults for
SG_PUSH_SWEEP,SG_AGENTIC_RACE_DELAY_S,DIFF_TOTAL_BYTES)
Workaround in use
SG_PUSH_SWEEP=0, which removes the duplicate at the cost of losing review coverage for
commits created outside Claude Code.
Activity
mbastawisy commentedon Sep 14, 2026
Confirming this from a second repo, and adding that it is not only the push sweep: the commit-to-commit reflog path races the same way. Plugin 2.0.8 (identical to
main), 23 commit reviews over four weeks, 5 overlapping pairs.Local reproduction from
~/.claude/security/log.txt:_append_reviewed_shasruns atsecurity_reminder_hook.py:1426, after the 1 to 4 minute agentic review at:1395; both dedup reads (:1116-1117reflog,:1725push sweep) happen before that. A second commit 26 s later re-swept the first two SHAs, so the two reviews overlapped (15 vs 9 files) rather than duplicated exactly.Aggravating factor worth noting for anyone measuring this: when
git commitruns inside a compound command with piped output,[branch sha]never reaches stdout, every commit logsstdout had no [branch sha], and dedup falls through to the 5-entry / 120 s reflog scan (gitutil.py:290). That makes the race window the normal path, not the fallback.Suggested shape. Simply moving
_append_reviewed_shasbefore the review would regress #5322 (a crashed review permanently silences those SHAs). A TTL-expiring in-flight sentinel avoids both, following the existingsg-hook-once-<id>idiom (same gitdir,O_CREAT|O_EXCL, opportunistic GC):diffstate.py:_mark_inflight(repo_root, shas)writessg-inflight-<sha>;_load_inflight_shas(repo_root)returns those younger than ~900 s and unlinks older ones.security_reminder_hook.py: call_mark_inflightright before the review starts (requires hoisting therev-parseloop at:1417-1425above it), and have both dedup reads use_load_reviewed_shas(root) | _load_inflight_shas(root).Trade-off: a genuinely failed review suppresses a duplicate pass for at most the TTL, after which the commit is eligible again. Files live in the common gitdir, so worktrees of one clone share the marker, which is the desired behaviour.
rjmurillo commentedon Sep 25, 2026
Independent confirmation on 2.0.8 (marketplace commit
55b58ec6e5649104f926ba7558b567dc8d33c5ff), plus a suggested dedupe key that also covers cases a SHA claim cannot.Measurement. I audited child review transcripts from one machine (2026-09-10 to 2026-09-20; 223 child sessions, 5,475,987 prompt bytes). I hashed each prompt's diff body with SHA-256.
Suggested key. The Stop hook already skips an unchanged diff with
reviewed_diff_hash(security_reminder_hook.py:2043-2046). Commit review and push sweep could use the same idea:_agentic_review_with_race, compute SHA-256 over the capped diff text that goes into the prompt. Scope it by repository identity (git rev-parse --git-common-dir) so all worktrees share it.A hash of the exact prompt input catches the race, the worktree split, and identical content under a new SHA (rebase, cherry-pick, or a no-op amend). It never skips a diff that differs by even one byte. The SHA claim proposed above still makes sense next to it, because it keeps the
sg-reviewed-shasrecord accurate.One more repeat, smaller. When pass 1 returns one or two candidates,
iter2_prompt(llm.py:1421) isuser_promptplus exclusions, sent to a fresh child session. Each iter2 session I measured re-created the cache for the whole diff on its first turn:cache_creation_input_tokenswas 5,351 to 16,240. Resuming the investigate session for pass 2 would read the diff from cache instead. It would also change the isolation between passes, so I note it only as an option.Checked and rejected: artifact references. I also tested replacing the inline diff with a content-addressed file reference that the reviewer fetches with a tool. With the plugin's own
AGENTIC_INVESTIGATE_SYSTEMandFINDINGS_SCHEMA, the referenced prompt used more input tokens in 5 of 6 measured cells (+3% to +44%). It usually added a fetch turn and saved nothing, because the reviewer has to read the diff anyway. Removing the duplicate review is where the savings are.docs(analysis): reject artifact references for security-guidance diff…