security-guidance: failed LLM review reports "no vulnerabilities found" (fail-quiet); resolved model is never logged
Summary
When the LLM review API call fails, the security-guidance plugin still emits the verdict line LLM code review: no vulnerabilities found and surfaces nothing to Claude. From a consumer's point of view (the Claude session, a human reading the log, or any downstream automation), a failed review is indistinguishable from a clean review.
There is a separate API call failed with status <code> line, so the log is not strictly silent — but the verdict line is what conveys the result, and it says the file is clean when in fact nothing was reviewed.
This matters because a realistic, non-exotic trigger exists: model retirement. Anyone who pinned SECURITY_REVIEW_MODEL to a model that later retires (Opus 4.1 is announced for retirement on 2026-08-05) will have their end-of-turn security review silently degrade into a no-op, while the log keeps reporting "no vulnerabilities found".
Environment
- Claude Code CLI: 2.1.219
- Plugin: security-guidance 2.0.6 (
claude-plugins-official) - Provider setup: 1P / Anthropic API via OAuth subscriber token (
ANTHROPIC_API_KEYunset, Max subscription) - OS: macOS (Darwin 25.5.0)
Repro
- Set an invalid model id, e.g. in
~/.claude/settings.json:
``json``
"env": { "SECURITY_REVIEW_MODEL": "claude-opus-DOES-NOT-EXIST" }
- Write a file with an obvious planted vulnerability to a git-visible path, e.g.
tests/fixture.py:
```python
import os, sqlite3
def find_user(conn, username):
cur = conn.cursor()
cur.execute(f"SELECT id, email FROM users WHERE username = '{username}'") # SQL injection
return cur.fetchall()
def archive_report(name):
return os.system(f"tar czf /tmp/{name}.tar.gz /var/reports/{name}") # command injection
```
- End the turn so the Stop hook fires.
- Read
~/.claude/security/log.txt.
Actual log
[2026-07-25 10:36:43.472] Stop hook: review_set=3 base=ab90842b9c1f dirty_now=14 changed_since=3
[2026-07-25 10:36:43.853] Stop hook: reviewing 2 changed files (standard diff)
[2026-07-25 10:36:44.860] API error: 404 - {"type":"error","error":{"type":"not_found_error","message":"model: claude-opus-DOES-NOT-EXIST"},"request_id":"req_011CdMvFi7qj6uqEE1Bt39nw"}
[2026-07-25 10:36:44.860] LLM code review: no vulnerabilities found <-- reports clean despite total failure
[2026-07-25 10:36:44.861] Stop hook: LLM reviews took 1.0s total
[2026-07-25 10:36:44.861] Stop hook: API call failed with status 404
For comparison, the same fixture with a valid model id produces:
[2026-07-25 10:15:35.144] LLM code review found 2 high/critical vulnerabilities
[2026-07-25 10:15:35.145] Stop hook: LLM reviews took 11.8s total
Note that in the failure case nothing was surfaced to the Claude session at all — behaviourally identical to a genuinely clean run.
Expected
When the review could not be performed, the plugin should not report a clean verdict. Some options:
- Emit a distinct verdict line, e.g.
LLM code review: FAILED (review not performed)instead ofno vulnerabilities found. - Surface the failure to the Claude session the same way findings are surfaced, so the model/user learns that the safety net did not run (fail-loud rather than fail-quiet).
Secondary request: log the model actually used
The plugin never writes the resolved model id to the log. grep over the entire ~/.claude/security/log.txt history returns zero model strings, and DEBUG_LOG_FILE points at the same file.
Combined with hooks/llm.py:131:
SECURITY_REVIEW_MODEL = os.environ.get("SECURITY_REVIEW_MODEL", "").strip() or "claude-opus-4-7"
…an env var that fails to reach the hook results in a silent fallback to claude-opus-4-7 with no way to tell after the fact which model reviewed the code. Verifying that a SECURITY_REVIEW_MODEL override had actually taken effect required running a negative control with a deliberately invalid model id, because every valid id produces byte-identical log output.
Logging the resolved model once per review (e.g. Stop hook: reviewing N changed files (model=<id>)) would make both the override and the fallback observable.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗