The claude code security review reads the commits on your branch, hunts for exploitable vulnerabilities, verifies each candidate in its own sub-agent and reports the survivors as a markdown table of severity, category, exploit scenario and fix. On 2026-09-04 we put six known vulnerabilities into a 34-line diff and ran it against Claude Code 2.1.260. Sonnet reported four in one run and five in the next. Haiku reported all six. And in five separate runs it reported nothing at all, successfully, with exit code 0 — which is the finding that matters most if you are about to put this in CI.
Key takeaways
- It reviews commits against
origin/HEAD, not your working tree. No resolvableorigin/HEADmeans an empty diff, an empty report and exit 0. - The prompt is a real document in the binary: six vulnerability families, a three-step sub-agent fan-out, and a cutoff that drops any finding scored below 8 out of 10 for confidence.
- 17 hard exclusions and 12 precedents shape it, including "secrets or credentials stored on disk" and every denial-of-service class.
- Against six planted vulnerabilities: Sonnet 4 and 5, Haiku 6 — and the two Sonnet dropped are the two the exclusion list discourages.
- Zero false positives on the deliberately-safe route in all three runs.
The short answer
Run it on a branch that is pushed and tracking, after the commits exist, and read the empty report as a question rather than an all-clear. It is the second of two review commands and the narrower one: /code-review reads your working tree and reports anything, while the claude code security review reads your commits and reports almost nothing. It is a genuinely strong reviewer of injection, traversal and authorization bugs in a diff, and it is deliberately silent about several categories you probably still care about. Anthropic's commands reference lists it and the CLI reference covers the flags around it; nothing in either tells you what it refuses to report, which is the half this article measures.
git remote set-head origin --auto # or the report will be empty claude -p "/security-review" --output-format json # → 226s, $0.9652, 7 sub-agents, 4 findings on a 34-line diff

What the claude code security review reads
Not what you have open. The command's prompt embeds four shell expansions, and they all point at the same range:
GIT STATUS: !`git status` FILES MODIFIED: !`git diff --name-only origin/HEAD...` COMMITS: !`git log --no-decorate origin/HEAD...` DIFF CONTENT: !`git diff origin/HEAD...`
origin/HEAD... is the range from the merge base with your default branch to your current commit. So uncommitted work is invisible to it — the opposite of the /code-review command, which reads the diff in your working tree. Commit first, or you are reviewing an empty change.
It is also a bundled plugin command, not a skill and not a plain built-in: the registration carries pluginName:"security-review" and pluginCommand:"security-review", and the prompt ships as a markdown file whose frontmatter declares its allowed tools. Those tools are worth reading, because they are the guarantee that a security review cannot touch your repository:
allowed-tools: Bash(git diff *), Bash(git status *), Bash(git log *), Bash(git show *), Bash(git remote show *), PowerShell(…the same five…), Read, Glob, Grep, LS, Task
Five read-only git queries, four read-only file tools and Task for the fan-out. No write, no edit, no arbitrary shell. If you are hardening a machine that runs this unattended, that list pairs with how Claude Code permissions resolve.
The prompt behind it
The instructions are static text compiled into the binary, and they read like a brief written by someone who has been on the receiving end of a noisy scanner. The objective is stated as "HIGH-CONFIDENCE security vulnerabilities that could have real exploitation potential", with three critical instructions: minimise false positives, avoid noise, focus on impact. It names five categories to examine — input validation, authentication and authorization, crypto and secrets management, injection and code execution, and data exposure — with concrete members under each, from SQL injection and path traversal to JWT flaws and certificate-validation bypasses. The families map closely onto the OWASP Top Ten, with the exclusions below deciding which members of each actually reach you.
Then it prescribes the method, and this is the part that explains the bill:
1. Use a sub-task to identify vulnerabilities. … 2. Then for each vulnerability identified by the above sub-task, create a new sub-task to filter out false-positives. Launch these sub-tasks as parallel sub-tasks. … 3. Filter out any vulnerabilities where the sub-task reported a confidence less than 8. Your final reply must contain the markdown report and nothing else.
One finder, then one verifier per candidate, in parallel, then a hard cutoff at 8 out of 10. Our runs spawned 7, 9 and 6 sub-agents respectively, which is the finder plus one per candidate — so the sub-agent count is a rough census of how many things it considered before deciding what to tell you. Those are ordinary Claude Code subagents, spawned by the command rather than declared in your settings, and they are where the bill goes. The confidence scale is defined twice in the prompt, once as a 0.7–1.0 float ("below 0.7: don't report") and once as the 1–10 integer the filter uses. Both survive into the report: our Haiku run printed Confidence: 10/10 on each finding.
The exclusion list
The false-positive filter carries 17 numbered hard exclusions and 12 numbered precedents. Several will change how you read an empty report:
| Excluded outright | Why it matters |
|---|---|
| Denial of service, resource exhaustion, memory or CPU limits | Four separate clauses cover this. It is not a DoS reviewer |
| Secrets or credentials stored on disk "if they are otherwise secured" | Committed keys are somebody else's job by design |
| Vulnerabilities from outdated third-party libraries | Dependency scanning is out of scope |
| Missing permission checks in client-side JS or TS | Assumed to be enforced server-side |
| Anything in a documentation or markdown file | Including a real secret in a README |
| Findings in files that are only tests | Test-only code is skipped |
| Theoretical race conditions and timing attacks | Only concrete ones count |
| Regex injection, regex DoS, log spoofing, missing audit logs | Four more explicit non-vulnerabilities |
The precedents narrow it further: environment variables and CLI flags are trusted inputs, UUIDs are assumed unguessable, React and Angular are assumed XSS-safe unless dangerouslySetInnerHTML or equivalent appears, and "only include MEDIUM findings if they are obvious and concrete issues."
Six vulnerabilities, three runs
Our corpus is a 34-line diff adding five endpoints to a small Express API. Six vulnerabilities, one deliberately-safe decoy route, and no comment anywhere hinting at any of it.
| # | Line | The vulnerability |
|---|---|---|
| 1 | 23–28 | SQL built by concatenating a query parameter |
| 2 | 30–33 | Path traversal: a route param joined onto a storage directory |
| 3 | 11 | A live-looking API key committed in source |
| 4 | 35–38 | Destructive admin route gated by session but not by role |
| 5 | 40–44 | Shell command interpolating a request body field |
| 6 | 46–48 | Unsalted MD5 for a stored password |
| — | 17–21 | Safe decoy: parameterised query, no user-controlled path |
Three runs of the claude code security review, one process each:
| Run | Model | Time | Cost | Sub-agents | Reported | Planted found |
|---|---|---|---|---|---|---|
| 1 | sonnet | 226s | $0.9652 | 7 | 4 | 1, 2, 4, 5 |
| 2 | sonnet | 215s | $0.8963 | 9 | 6 | 1, 2, 3, 4, 5 |
| 3 | haiku | 342s | $0.3446 | 6 | 6 | all six |
Every run found the four that have an attack path you can write down: the injection, the traversal, the missing role check and the shell interpolation. Every one came back with a severity, a category slug such as sql_injection or path_traversal, a described exploit and a fix that referenced the parameterised pattern already in the same file.
No run flagged the decoy. Three runs, zero false positives on the route that is genuinely fine — which, given that it sits between two vulnerable routes and calls the same database helper, is a harder test than it looks.
Run 2 reported one thing we had not planted: the listing endpoint scopes results by an owner taken from the query string rather than from the session, which is a real access-control bug in code we wrote to hold a different one.
The stronger model reported less
The two vulnerabilities Sonnet dropped are not random. The committed API key is covered by two separate clauses — the objective's own exclusion of "secrets or sensitive data stored on disk (these are handled by other processes)" and hard exclusion 2. Sonnet skipped it in run 1 and reported it in run 2. Haiku reported it both times.
The unsalted MD5 password hash is a stranger case. Weak cryptographic algorithms are in the categories the prompt tells it to examine, so nothing excludes it — but hashPassword is exported and never called anywhere in the diff, so there is no data path from a request to it, and the prompt demands better than 80% confidence in actual exploitability. Sonnet dropped it twice. Haiku reported it twice, at 10/10.
Read together, those are one result: the more capable model applies the false-positive filter more faithfully, and applying that filter faithfully costs you findings. That is a defensible design — a scanner nobody reads is worse than no scanner — but it means the model you run this on changes what you are told, in a direction most people will guess backwards.
The silent failure that returns nothing
This is the one to fix before you use the command for anything.
In five runs, /security-review returned an empty report having spent nothing. Zero turns, $0.0000, a zero-length result, subtype: "success", exit code 0, and the whole thing over in about a second. Three of those were accidental — we ran the command in scratch repositories before adding a remote — and two were deliberate reproductions afterwards:
| Repository shape | Turns | Cost | Result | Exit |
|---|---|---|---|---|
| No remote at all | 0 | $0.0000 | empty | 0 |
Remote present, origin/HEAD unresolvable | 0 | $0.0000 | empty | 0 |
The mechanism is in the prompt. Every one of its four git expansions is written against origin/HEAD..., and if that ref does not resolve, they expand to nothing — so the model is handed a review request with no diff attached and answers with an empty report, exactly as instructed to reply with "the markdown report and nothing else".
The second shape is the dangerous one, because it is what a CI checkout usually looks like. A git clone --single-branch, a shallow fetch, or a checkout action that fetches one ref will all leave you with an origin remote and no origin/HEAD. We reproduced it by cloning single-branch and deleting the symbolic ref:
git remote set-head origin --delete
git rev-parse origin/HEAD
# → fatal: ambiguous argument 'origin/HEAD': unknown revision
claude -p "/security-review" --output-format json
# → {"num_turns":0,"total_cost_usd":0,"subtype":"success","result":""}
The fix is one line before the review: git remote set-head origin --auto. And the CI rule that follows from it: never gate on the exit code. Fail the job when the report is empty and the diff is not, and assert that total_cost_usd is greater than zero — a review that cost nothing did nothing. There is a real error path in the binary for a non-repository working directory, which prints a message telling you to cd into the repository first; there is no equivalent message for this case.
Where it stops and code review starts
The two commands look interchangeable and are not, so we ran each against the other's corpus.
/security-review on the correctness diff — six real bugs, no vulnerabilities — returned exactly one finding in 177 seconds for $0.5242, and it reframed the one it took: the inverted refund window came back as business_logic_bypass, High severity, on the grounds that it authorizes refunds outside the intended window. It ignored the crash, the missing await, the null dereference, the duplicate function and the quadratic loop. That is correct behaviour and a clean demonstration of scope: it is not a bug finder that also knows about security, it is a security reviewer that will take a logic bug when the logic is an authorization decision.
The reverse direction is stranger, and it is measured in the code review command article: pointed at this same vulnerable diff, the general-purpose /code-review reported more than the security command did — every one of the six, in a third of the time and a fifth of the cost. Nothing about that makes it the better security tool. It has no exclusion list and no verification pass, so its output is a longer list at a uniform, unstated confidence, and the work of deciding what is real moves back to you. Run code review on the diff before you open the PR, and security review on the branch before you ship it. For the broader threat picture around AI-assisted development, the risks in AI-generated code covers what neither command is looking at.
What did not work
Our first corpus labelled every vulnerability in a comment — // V1: SQL built by concatenation from a query parameter and so on down the file. The review found them, which proves nothing at all. Every number in this article comes from the rebuilt corpus, which carries no comments. This is the easiest way to fool yourself when benchmarking a security tool and it took a full round of runs to notice.
We could not verify the confidence filter directly. The prompt says findings below 8 are dropped, and Haiku printed Confidence: 10/10 on everything it kept, but a dropped finding leaves no trace in the output — so we can report the rule and the sub-agent count, and we cannot tell you how many candidates the filter actually removed. Inferring it from 9 sub-agents, 6 findings is arithmetic, not evidence, and we are not publishing it as a number.
One run's own timing is unreliable. The result envelope's duration_ms came back as 17 seconds for a run the shell clocked at 204. Every duration in this article is wall-clock, measured outside the process. We do not know why the envelope disagrees.
We did not test a real pull request. Everything here is local branches against local bare repositories. --comment-style posting, GitHub Actions integration and the behaviour on a fork PR are all untested by us, and we make no claim about them.
Best practices
- Treat the claude code security review as the last gate, not the only one. It reads one diff; a scanner reads the repository.
- Run
git remote set-head origin --autofirst, in CI unconditionally. It is the difference between a review and a silent no-op. - Commit before you review. The command cannot see your working tree.
- Run it twice on a branch that matters. Our two Sonnet runs differed by two findings on an identical diff.
- Keep a secret scanner. Committed credentials are excluded by the prompt in two places; assume this command will not tell you.
- Assert on cost, not exit code.
total_cost_usd == 0means the review never happened. - Read the severity, then re-read the exploit scenario. Every finding we got came with a described attack path, and that paragraph is what tells you whether it applies to your deployment.
Common mistakes
- Treating an empty report as a pass. DoS, dependencies, on-disk secrets, docs and test files are all out of scope by design.
- Running it on an unpushed branch with no upstream. Zero turns, zero cost, zero findings, exit 0.
- Expecting it to review your uncommitted changes. That is the other command.
- Assuming the strongest model finds the most. Ours found the fewest, because it honoured the false-positive filter.
- Using it instead of a scanner. It is a reviewer of one diff, not a census of your repository — and it says so by only ever reading
origin/HEAD....
Conclusion
Use /security-review as the last gate before a branch ships, with origin/HEAD resolvable and the commits already made, and treat its report as high-precision rather than complete — it caught every injection, traversal and authorization bug we planted, and it stayed quiet about a committed key and a weak hash because its own instructions tell it to. Budget a dollar and four minutes for a small diff. Then wire the CI check on the presence of a report rather than the exit code, because the failure mode that costs you is not a wrong finding, it is the empty one that passes.
Frequently asked questions
What does /security-review actually check?
Why did my Claude Code security review return nothing?
Does /security-review report hardcoded secrets?
Is /security-review a skill?
How much does a security review cost and how long does it take?
Muhammad Kashif
Founder and editor of Devventa, covering AI coding assistants, Next.js and the modern AI development stack.



