What an agent actually reaches
A security review is not one activity. It is several, and an agent is genuinely useful for some of them, marginal for others, and must not attempt the rest.
The distinction that matters most: Claude Code reads, and it requests. It does not attack. An audit that stays on the reading side is thorough, repeatable and safe to run against your own production system. One that drifts into probing is penetration testing, needs written authorisation, and should be done by someone who does it for a living.
Say this in the session, because the default behaviour is to be helpful:
This is a defensive audit of code and configuration we own.
Do not attempt exploitation, credential stuffing, brute force,
scanning of hosts, or any request designed to trigger a
vulnerability. Read the code, read the configuration, and make
ordinary requests to our own site only.
Report findings. Do not fix anything yet.
Scope before anything else
An audit with no scope produces a list of generic advice. Half an hour establishing what the system actually is makes everything after it specific.
Before auditing anything, inventory this project and report:
- Language, framework and major versions
- How authentication works, and who issues the session
- Where data lives, and how it is queried
- Every route or endpoint, and which require auth
- Third-party services, and what credentials each holds
- Where files are uploaded to, and what is done with them
- Every webhook receiver, and how each verifies its sender
- Admin surfaces, and how they are protected
- Where the app is deployed, and what the pipeline does
- Anything handling payment
For each: file paths. If you cannot determine one, say so.
The last line matters. An inventory with honest gaps is useful; one with confident guesses is worse than none, because everything downstream inherits the guess.
The workflow
Fifteen passes, in an order chosen so each one narrows the next:
Scope → Inventory → Secrets → Dependencies → Authentication
→ Authorisation → Input handling → Sessions → Headers
→ Browser exposure → Infrastructure → CI/CD → Logging
→ Deployment → Validation
Secrets come third for a reason: a leaked credential makes every other finding academic. Authorisation follows authentication because you cannot review who may do what until you know who the system thinks they are.
Secrets, including the ones in history
The most common real finding on a small codebase, and the easiest to check.
Search this repository for credentials in source, configuration,
build output, and committed artefacts. Include: API keys, access
tokens, private keys, database URLs with passwords, and any
hard-coded password including test ones.
Report file and line for each. Do not print the secret value —
print enough to locate it and no more.
That last instruction is not decoration. An audit report containing the actual keys is itself a disclosure, and it will end up pasted into an issue tracker.
Then the part people miss. Deleting a secret in a later commit does not remove it from history.
git grep -I --line-number -e 'BEGIN.*PRIVATE KEY' $(git rev-list --all) -- 2>/dev/null | head
git log -p -S'API_KEY' --oneline | head -50
A dedicated scanner is better than grep for this — gitleaks and trufflehog both scan history, and GitHub secret scanning runs continuously on repositories that enable it.
The rule that follows from a hit is unconditional: any secret that was ever committed is compromised and must be rotated. Rewriting history does not un-clone the repository, and it does not help if the commit was ever pushed.
Check the client bundle separately. A key that is safe on a server is public the moment it is bundled:
grep -rIn -E '(sk_live|api[_-]?key|secret)' dist/ build/ .next/static/ 2>/dev/null
Dependencies and supply chain
Now the top category in the OWASP Top 10:2025 to have moved: A03:2025 — Software Supply Chain Failures, expanded from what was previously the vulnerable-components category.
npm audit --audit-level=high # or: pnpm audit / yarn npm audit
pip-audit # Python
bundle audit # Ruby
What a scanner tells you and what it does not:
| It finds | It misses |
|---|---|
| Known CVEs in your lockfile | Vulnerabilities nobody has reported yet |
| Versions with published advisories | Whether your code reaches the vulnerable path |
| Transitive dependencies | A malicious package with no advisory |
| Typosquats you installed by accident |
The second column is where an agent adds something a scanner cannot. Ask whether the vulnerable function is actually called, and whether each dependency earns its place:
For each high-severity advisory from npm audit, find where the
affected package is used in our code and say whether the
vulnerable code path is reachable. If it is not reachable, say so
rather than recommending an upgrade we may not need.
Separately: list every direct dependency added in the last year,
what it does, and whether the standard library covers it.
Authentication
Read the implementation, not the login page. What to establish, in order of how much damage each causes:
- Password storage. A modern algorithm — argon2, bcrypt, scrypt. Not MD5, not SHA-256, not "encrypted".
- Reset tokens. Random, single-use, short-lived, invalidated on use, and not guessable from the user id.
- Rate limiting on login, registration and reset, and it applies per account as well as per address.
- Account enumeration. Do the responses for "no such user" and "wrong password" differ, in body, status, or timing?
- Multi-factor available for privileged accounts, and enabled on yours.
- Password rules that do not block passphrases. A 16-character maximum is a bug.
Authorisation, which is the one that matters
Authentication asks who you are. Authorisation asks what you may do. They get conflated constantly, and the second is where the expensive failures live: A01:2025 — Broken Access Control remains the top category in OWASP's list.
The check that finds most of it in a few minutes:
For every route that returns or modifies a record, show me the
line that verifies the current user may access THAT SPECIFIC
record — not just that they are logged in, and not just that
their role is correct.
List any route where you cannot find such a check.
Then verify by hand. Log in as one user, take an id from a URL or an API response, change it to another user's, and confirm you get a 403 rather than their data. That single test finds more real problems than any scanner, and it takes two minutes.
Two rules worth writing into your project's CLAUDE.md so they survive the audit: authorisation is checked server-side on every request, and hiding a control in the UI is not an authorisation check.
Input handling
Every value that came from outside is untrusted until it has been validated for its purpose and escaped for its destination.
| Where it lands | What to look for |
|---|---|
| SQL | String concatenation or interpolation anywhere near a query. Parameterise or use the ORM properly |
| HTML |
innerHTML, dangerouslySetInnerHTML, or a template filter that disables escaping |
| A shell | User input reaching exec, system, or a subprocess with shell=True
|
| A template | User input rendered as a template rather than into one |
| A file path | Path joins without normalisation, or anything that can climb out of its root |
| An outbound request | A user-supplied URL fetched by the server — now folded into A01 in the 2025 list |
Uploads get their own pass: type and size limited, renamed, stored outside the web root, and never executed. "We check the extension" is not a control.
Sessions
Short pass, mechanical, and easy to get right once. Cookies set HttpOnly, Secure and SameSite. The session identifier regenerates on login and on any privilege change. Sessions expire both on idle and absolutely. Logout invalidates server-side rather than only clearing the cookie. No identifiers in URLs. CSRF protection on every state-changing request that relies on cookie auth.
Headers and transport
Check the live response, not the config file — a header can be added or stripped by a proxy or a platform you do not control:
curl -sI https://example.com | grep -iE 'content-security-policy|strict-transport|x-content-type|referrer-policy|permissions-policy|x-frame'
A useful baseline is Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy, and frame protection through CSP frame-ancestors. MDN's CSP reference is the place to work out what yours should contain.
Two cautions. A CSP full of unsafe-inline and unsafe-eval is decoration; if you need them, write down why. And the right values depend on the application — an agent asked for "recommended headers" will produce a plausible generic set that breaks a third-party embed you actually need.
What ships to the browser
Everything in the bundle is public. The audit is: what is in there that should not be?
- Secrets, as above. Publishable keys are fine; secret keys are not.
- Source maps served in production — deliberate or accidental, but know which.
- Unsafe DOM writes from data that came from a URL, a query string, or an API.
- Every third-party script, named and justified. Each one can read the page.
- Tokens in
localStorage, which any successful XSS can read.
APIs and webhooks
Rate limiting on public endpoints. CORS restricted to known origins, never * on anything authenticated. Responses returning only the fields needed, rather than whole records the UI happens not to display. Errors generic to the caller and detailed only in server logs.
Webhooks deserve their own attention because the failure is silent:
For each webhook receiver: show the signature or shared-secret
verification, and show that the comparison is constant-time.
Then show what happens on a replayed request with an old
timestamp, and on a duplicate delivery.
A receiver that trusts its caller is an unauthenticated endpoint that mutates your data.
CI/CD and logging
The pipeline holds credentials with more power than most user accounts. Secrets in the platform's secret store rather than the config file; masked in logs; not exposed to builds from forked pull requests; deploy credentials scoped to what they deploy; branch protection on the production branch. GitHub's hardening guide is the reference if you are on Actions, and running Claude Code in CI covers the workflow side.
For logging, the question is what accidentally ends up in them: tokens, passwords, card numbers, personal data, full request bodies, stack traces returned to users. Note that OWASP renamed this category for 2025 to A09:2025 — Security Logging & Alerting Failures, emphasising the alerting half. Logs nobody reads and alerts nobody receives are not a control.
The audit prompt
One prompt, reusable, that produces evidence instead of advice:
Audit this project for security problems. Defensive review only —
read code and configuration, and make ordinary requests to our own
site. Do not attempt exploitation of any kind.
Rules:
1. Inspect before proposing anything. Change nothing yet.
2. Every finding needs evidence: file and line, or the actual
response. A finding you cannot evidence is marked UNVERIFIED.
3. Rank by real impact on this system, not by generic severity.
4. Do not report a missing nice-to-have as critical.
5. Do not print secret values.
6. Say explicitly what you could not check, and why.
For each finding use exactly this shape:
Finding:
Severity: critical | high | medium | low
Location: file:line, or the URL
Evidence: what you observed
Risk: what an attacker gains
Recommendation: the smallest change that fixes it
Validation: how I confirm the fix worked
Rule 6 is the one that turns a report into something you can act on. An audit that hides its own gaps reads as complete, and the reader assumes the silence means safety.
Recording a finding
The Validation line is what most reports omit and the only one that closes the loop. "Add CSRF protection" is a recommendation. "Submit the form without the token and confirm a 403" is a validation, and someone can run it.
Severity, kept simple enough that it gets applied consistently:
| Severity | Test |
|---|---|
| Critical | Data exposed now, or authentication bypassable now |
| High | Exploitable with modest effort, or one condition away from critical |
| Medium | Real weakness, needs another factor to be dangerous |
| Low | Hardening. Worth doing, not worth an incident |
Fix, then prove the fix
Find → Understand → Fix → Test → Rescan → Validate
Understand is the step that gets skipped, and skipping it is how a fix moves the bug rather than removing it. Ask for the mechanism before the patch: which line permits this, and why does the existing check not catch it?
Then fix one finding at a time, in its own commit, with a test that fails without the fix. And apply the discipline this site has learned the hard way: after the fix passes, reintroduce the problem and confirm the test goes red. A security test that has never failed is not evidence that the vulnerability is gone — it is evidence that the test runs. Four separate checks on this project reported success on exactly the condition they existed to catch, which is documented in the case study for this site.
What this does not replace
Stated plainly, because the gap between "we ran an audit" and "we are secure" is where people get hurt:
- Penetration testing. Actively attempting to break in, by someone qualified, with written authorisation. Nothing above does this.
- Threat modelling. Deciding what matters, to whom, and how much. That is a judgement about your business.
- Compliance auditing. PCI, SOC 2, HIPAA and the rest have specific evidentiary requirements a code review does not satisfy.
- Runtime and behavioural analysis. Race conditions, session handling under load, resource exhaustion.
- A specialist's judgement on anything holding payment data, health data, or personal data at scale.
What the workflow above genuinely does is remove the large, boring, avoidable class of problems — the committed .env, the missing object-level check, the endpoint that trusts its caller — so that a specialist's time is spent on the things only a specialist can find.
Where to go next
The condensed version of all of this is the free Claude Code security checklist, written so each item can be handed to a session directly. For the wider inspection this sits inside, see the website audit guide, and for building security in rather than retrofitting it, the complete website workflow. Organisation-level controls are covered in Claude Code for teams and enterprise, and the rules worth enforcing mechanically belong in hooks.
Sources and further reading
- OWASP Top 10:2025 — the current edition, with Software Supply Chain Failures new at A03
- OWASP Cheat Sheet Series — practical per-topic detail
- MDN: HTTP headers
- GitHub: secret scanning
- GitHub: hardening Actions