Claude Code security review: an audit that only reports real vulnerabilities
Ask Claude Code for a security review of your app and you'll get a list. Some of it is real. Much of it is generic advice: "consider rate limiting", "validate all input", "use HTTPS". A security audit with Claude Code is only useful when every finding comes with an attack path you can test, and that takes a routine, not a one-line prompt.
This post walks through the routine we use on our own code: scope the review, map where untrusted data enters, check authorization by hand, try to disprove each finding, and report only what survives. Everything here works as a plain prompt. At the end there's a shortcut if you'd rather not type it every time.
Why AI security reviews get noisy
- Pattern matching without data flow. A string-built SQL query is only an injection if user input reaches it. Flagging every
execute(call buries the one that matters. - Missing the defense upstream. The ORM parameterizes, the template engine auto-escapes, a validator rejects the payload two files earlier. Without following the data, the review can't see that.
- Ignoring access control. The most common serious bug in web apps is "user A can read user B's data", and grep can't find it. It needs someone to ask, route by route, who is allowed to call this.
- No proof. A finding nobody tried to break is a guess.
1. Pick the scope
Say exactly what to review. For a pull request, review the diff but follow data into the unchanged code it touches:
gh pr diff 123
git diff main...feature
For a whole repo, don't ask for "everything". Ask for the riskiest areas first: authentication, payments, file uploads, admin panels, and anything that takes a URL or a file name from the user. Then ask Claude to say what it covered and what it didn't. A review that admits it skipped the reporting module is more useful than one that implies it read it all.
Claude Code also has a built-in /security-review command for the pending changes on your current branch. It's a good quick pass before you push. The workflow below is for when you want a deeper audit of a specific area, with your own rules about what counts as a finding.
2. Map entry points, then trace sinks backwards
Start with where untrusted data comes in: HTTP routes, GraphQL resolvers, webhooks, queue consumers, CLI arguments, file and archive parsers. For each one, note who can call it (anonymous, logged-in user, admin), which auth middleware runs, and what it reads: body, query, headers, cookies, path params, uploads.
Then search for dangerous sinks and trace each one back to a source. A short list goes a long way:
- Injection: queries built with string concatenation or f-strings,
raw(, shell calls such assubprocess(..., shell=True)orchild_process.exec. - Path traversal:
path.joinoros.path.joinwith user input, zip and tar extraction. - SSRF: any HTTP client fetching a URL the user supplied: webhook testers, image proxies, importers, PDF renderers.
- XSS:
innerHTML,dangerouslySetInnerHTML,v-html,|safe,mark_safe. - Unsafe deserialization:
pickle,yaml.loadwithout a safe loader, PHPunserialize. - Crypto misuse: MD5 or SHA-1 for passwords,
Math.randomfor tokens, JWTs decoded without verifying the signature,==to compare secrets.
Only a sink with a path from a source is a candidate. Everything else is noise.
3. Check authorization route by route
This is the step most reviews skip and the one that finds the worst bugs. For every route that reads or changes data, ask:
- Is authentication actually enforced? Middleware that is imported but not applied to the router doesn't count.
- Is the object scoped to the caller?
Invoice.findById(req.params.id)with no owner or tenant check is an IDOR: change the id, read someone else's invoice. - Can a normal user set a field they shouldn't, like
roleorisAdmin, by adding it to the request body (mass assignment)? - Do state-changing requests with cookie auth have CSRF protection?
- Are login, password reset and OTP endpoints rate limited, and are reset tokens single-use with an expiry?
Tell Claude to go through these one route at a time and to name the file and line where each check happens. "Auth is handled by middleware" is not an answer. "requireUser is applied in src/routes/index.ts:12 to every route under /api" is.
4. Try to disprove every finding
Before anything goes into the report, ask Claude to argue against it:
- Write the exact request an attacker would send, and the code path it takes, file by file.
- Look for a defense on that path: validation, type coercion, a parameterizing ORM, framework escaping, an allowlist.
- Where it's cheap, prove it locally: a unit test, a
curlagainst your dev server, or a one-line script calling the function with the payload.
Anything that doesn't survive is dropped. Anything that couldn't be fully confirmed is labeled Plausible, with a note on what would confirm it. This one step is what turns a list of warnings into a list of bugs. Keep the testing on your own code and your own dev environment; there's no need to point anything at production.
5. Report severity first, with the fix
Ask for a one-line summary, then findings from most to least severe, each in the same shape:
3 findings (1 Critical, 1 High, 1 Medium) in auth, uploads and the image proxy.
### 1. [Critical] Any user can read any invoice (IDOR)
src/routes/invoices.ts:27
GET /invoices/:id loads the invoice by id without checking it belongs to
req.user.orgId.
Attack: log in as any customer, request /invoices/1..N, get other
customers' invoices.
Fix: Invoice.findOne({ id, orgId: req.user.orgId }), return 404 when null.
Confirmed: integration test with two users, second user got 200.
A simple severity scale keeps everyone honest. Critical: unauthenticated or cross-tenant data access, or code execution. High: needs a normal account, or exposes sensitive data. Medium: needs unusual conditions or has limited impact. Low: defense in depth.
End with a short "checked and fine" section naming the risky areas that held up. It tells you what was actually covered, and it stops the next review from redoing the same work.
What a code audit won't catch
Reading application code covers one layer. Three others need their own pass:
- Leaked secrets. A key deleted in a later commit is still in git history. Run a scanner such as
gitleaksover the whole history, and rotate anything real before cleaning history. - Dependencies. Run your ecosystem's audit (
npm audit,pip-audit,cargo audit,govulncheck), then check whether the vulnerable function is actually reachable before upgrading. Avoidnpm audit fix --force; it can jump major versions without review. - Configuration. Security headers, CORS, cookie flags, Docker users, CI token permissions. A wildcard CORS policy with credentials can undo careful code.
And if you don't know where to start, a short threat model helps: list what an attacker would want, where data crosses trust boundaries, and the top five risks. Then point the code audit at those first.
Make it a skill
The routine above is long to type, and it only works if every step happens every time. That's what Claude Code skills are for: write it once as a SKILL.md and Claude loads it whenever you say "security review this" or "is this endpoint safe?". Our guide on how to write a Claude Code skill shows the format. The same verify-before-reporting idea drives our code review workflow, which pairs well with this one.
Or use the Security Audit Pack below: five ready skills for code audits, secret scanning, dependency triage, config hardening and threat models, built around exactly these steps.