security-guidance: failed LLM review reports "no vulnerabilities found" (fail-quiet); resolved model is never logged

Status Open
Reported on v2.1.219
Maintainer reply None cached
Activity 2 comments · opened Jul 25, 2026

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_KEY unset, Max subscription)
  • OS: macOS (Darwin 25.5.0)

Repro

  1. Set an invalid model id, e.g. in ~/.claude/settings.json:

``json
"env": { "SECURITY_REVIEW_MODEL": "claude-opus-DOES-NOT-EXIST" }
``

  1. 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
```

  1. End the turn so the Stop hook fires.
  2. 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 of no 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.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗