Skip to content

Claude Code Production EngineeringStep 2 of 8

Claude Code Website Security Audit: Complete Workflow

An agent reads code, configuration and dependencies in full, reaches a deployed site only through its own requests, and must not attempt exploitation at all. This is the workflow that stays on the right side of that line.

Claude Code Guides: Claude Code Website Security Audit. A shield with review layers inside it and an open lock beneath, marking what the review does not reach.

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.

Seven layers of a security review and how far an agent reaches into each. It reads source code, configuration and dependencies in full, and reaches the deployed site only through requests it makes. It cannot observe running behaviour such as race conditions, must not attempt adversarial testing or exploitation, and cannot decide what threat modelling considers important.
The top four are where it earns its place. The bottom three are not a gap in the tooling — they are a different job.

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

Read these next

See how this fits into Claude Code Production Engineering

Continue your learning path

  1. Next

    W3C is explicit that tools cannot do it all. This is the division of labour: what a scanner answers, what reading com...

  2. Then

    Mention @claude and it implements changes. Give it a prompt and it runs on every pull request without anyone asking. ...

  3. Going deeper

    Everything else you write for Claude Code is a request. Hooks run. That difference decides which of your rules belong...

The whole sequence: Claude Code Production Engineering

Free download

The CLAUDE.md Starter Kit, free

Four working CLAUDE.md files you can drop into a project today, plus the one-page checklist for what belongs in one and how to tell whether yours is actually working.

  • CLAUDE.md for a static marketing site
  • CLAUDE.md for a web application, with security and migration rules
  • CLAUDE.md for a Shopify theme, including the gotchas that cost hours
  • CLAUDE.md for a shared package in a monorepo
  • A one-page checklist, and how to test the file is actually working
What are you working on?

The download appears here as soon as you submit. I will also email you when there is a new guide worth reading. No fixed schedule, no selling your address, unsubscribe from any email. See the privacy policy.