Max Narditdata & ai systems
← Articles
11 mindev

Attack surface reduction with Claude Code: Anthropic's three passes on my own site, and a fourth they don't ask

The skill is not out, the method rebuilds in an afternoon, and the hard part is choosing where to point it. Here the most useful result was a broken verification file, and the exposure that mattered lived in a deploy setting my three passes never opened.

Contents

On October 20, Anthropic’s Frontier Red Team published a method that puts a different question to Claude. It asks not where the bug is but which code an attacker can reach that nothing needs to be reachable. They ran it over 25 open-source projects, it came back with 493 issues, and the case they lead with is OpenSSH, where about 4,500 lines of experimental post-quantum code were reachable before authentication in a default install.

I reran the discovery half of the method on the one public system I can test without asking anyone: this site. Three passes, each in a fresh context, findings traced against a local production build. It took under 25 minutes of wall clock. The most useful thing it found was a bug that had nothing to do with attack surface, and the worst exposure I had all month sat somewhere none of the three passes look. Both say the method finds what it is pointed at, and the pointing is your job.

What is Anthropic’s attack surface reduction method for Claude?

Three questions, asked in three passes, each starting with an empty context window apart from a short handoff from the pass before. The first looks for code the default configuration lets anyone reach that no intended feature uses. The second looks for code that runs before a hard gate, such as authentication or a protocol state transition, in any configuration. The third looks for option pairs that silently switch a security control off or weaken it. What keeps this from being a list of guesses is the confirmation step:

In every pass, Claude creates an inventory of the entry points from the
code and confirms each finding with a dynamic trace of the program. For
default configuration findings, it designs and implements a gate and
ensures that this causes no change in the program’s default behavior. It
does so by running the project's own test suite (along with a black box
regression suite) against the baseline; it requires modified builds to
have identical results.

Any gate that would change a non-default configuration is set aside for a human. That is the right instinct: close what the default never uses, and leave to the owner anything somebody might.

What did Anthropic’s attack surface passes find in OpenSSH, and what did the post leave out?

The headline numbers are 493 issues across 25 projects and over five million lines of code, from an inventory of more than 17,000 configuration options, 3,842 of them security relevant. The most interesting class came out of the third pass:

Claude found 162 security sensitive configuration combinations that were
accepted both without an error or warning and without any documentation
of the expected behavior of their interaction. In 94 of those cases, the
combination silently turned off a security control.

One pattern showed up in four projects: a deny rule written as a hostname, with reverse DNS lookups switched off. The server then compares a hostname pattern against an IP address, the rule can never match, and the deny list fails open. Another conflict let a server that required client certificates accept anyone, and it had been in the code for 21 years.

OpenSSH is the case study. In 10.4, sshd parsed whatever key type the client sent, and for certificates verified the CA signature, before it checked whether that key type was allowed. ML-DSA-44 was not enabled by default and the code was reachable anyway, even with the algorithm explicitly excluded. Anthropic puts it at about 16% of OpenSSH’s own pre-authentication code in a default sshd.

The 10.5 release moved the check before parsing. Claude then found two gaps in that check: the advertised algorithm and the parsed key type could differ, and certificate CA keys still went through full verification first. The fix that shipped in 10.6 puts the checks inside the parser and removed roughly a fifth of the code an unauthenticated client could previously reach. Note what it did not do: delete the ML-DSA code. It made that code unreachable to someone who has not been let in.

What the post leaves out matters as much. There is no false positive rate for the 493, no count of findings maintainers accepted outside OpenSSH, and no cost figure, although the authors say Claude does this economically. For the gated cohort they say outright that none of the issues would have been immediately exploitable on their own. That is the method, not a weakness of it: you shrink the surface before anyone writes the exploit, so the number to watch is code an outsider can no longer reach, not vulnerabilities found.

Is Anthropic’s attack surface reduction skill published?

Not with the October 20 post. It describes the three passes combined into one reusable Claude skill and says the team is integrating it into OSS-Scanner, but it links no repository and publishes no prompts. So what follows is a rebuild of the method from its description, and the wording of each pass is mine, not Anthropic’s. The rebuild is not hard, because the method is mostly discipline: separate contexts, a running build, a trace per finding, the project’s own tests as the baseline.

How do you run attack surface reduction in Claude Code?

Build the real thing locally and point every pass at it. For a Next.js site that is a worktree at the commit you want to test, a production build, and a server bound to loopback on a spare port (by default next start listens on every interface):

shell
git worktree add --detach ../asr HEAD
cd ../asr && npm ci && npm run build
npx next start -H 127.0.0.1 -p 3130

Then run the project’s own tests against that server before any pass, so there is a baseline to compare a cut against. Here that was the markdown and MCP endpoint test scripts the repository already has; both passed. The layout test could not run in that environment, so I say so here rather than drop it quietly.

Each pass goes to its own Claude Code subagent, so no pass inherits another’s conclusions; I ran the three in parallel. All three prompts share the same rules: read the worktree, change nothing, send requests only to localhost, count a finding only if a curl trace shows the path is reached, and for every finding name what you would cut and which test or request shows the default behaviour survives. The pass one question, trimmed from the prompt it ran with:

Which code is reachable in the DEFAULT configuration (no special env
vars, no flags, a normal production build) that does NOT need to be
reachable for the site to do its job? "Does not need to be reachable" =
a route, method, parameter, header path, rewrite, debug branch, or file
that is served but serves no purpose for those jobs.

Two details in that prompt do most of the work. It states the site’s job, including the endpoints that are public on purpose, so the pass does not flag deliberate machine-readable surfaces as waste. And it tells the pass I would rather remove code than add guards, which changes what a proposed fix looks like.

My run stopped short of Anthropic’s in one place: it covered discovery and proposals. No cut was applied during the passes, so the regression half of Anthropic’s loop, modified build against baseline, has not run yet for these findings.

What does attack surface reduction pass 1 find on a small Next.js site?

Pass one took five and a half minutes, including the test baseline, and confirmed six findings plus one dead-code item. In kind they are aliases nothing links to, handling the framework already does, redirects kept for an old setup, response metadata nobody needs, and configuration that no longer does anything. Most proposed fixes are deletions; one is a single configuration line. The pass also listed the false positives it threw out, which for an MCP endpoint is a long list of things the spec requires.

None of those six were fixed when the run ended, so I describe them by kind only. The finding I can name was fixed the same morning, and it was not a security issue. A path rewrite meant to send bare URLs to the default language was also catching two files that have to sit at the site root: the Google Search Console verification file and the IndexNow key. Both returned 404 in production, so search console re-verification and IndexNow key validation would have failed, quietly.

Anthropic’s post does not mention this side effect. A pass that has to prove what is reachable ends up holding a map of the server, and a map shows missing roads as clearly as extra ones.

Attack surface reduction pass 2 without a login: what can an anonymous request make the server do?

This site has no authentication gate anywhere, nothing that checks a secret or takes a webhook. So pass two turned into the question under it: what can an anonymous request make the server do? It took three minutes and came back with four findings, all one shape: a handler accepting more input than its job needs. None reached anything private. Two of them could only be confirmed to the end by causing the side effect, so their checks stopped at the local build on purpose.

Why did attack surface reduction pass 3 find so little on a site without controls?

On this site no option pair switched off a protection, because there was almost nothing to switch. A pair can only switch a protection off if there are protections to switch, and a site with no auth and no cookies gives the pass very little to bite on. In two and a quarter minutes it found one confirmed metadata inconsistency, one gap whose impact it could not confirm, and one latent case.

The third pass is for things that have controls to switch off: a reverse proxy, an SSH daemon, the access rules of a VPN. The hostname deny rule and the client certificate case in Anthropic’s post are exactly that kind of target.

The fourth attack surface reduction question: what does the deploy publish that the code never references?

The worst exposure I found this month had perfectly fine code. It sat in a static site I deploy, in two settings that were each reasonable. The site had no build step, which plain HTML does not need, so the hosting platform served the repository root as the site. And the agents I run on it keep their working notes in that same repository, which is private. Together, internal files were reachable at guessable URLs, and stayed that way for months. A deploy review exposed the mismatch. The fix was an allowlist of what deploys, then a check from outside that every internal path returns 404.

In shape that is a pass three finding, an option pair that silently drops a protection, except the protection was “this repository is private”. My three passes built their inventories from the code, and a pass that inventories from the code can only find what the code names. The fourth question inventories from the repository and asks the running build which of those files it serves. It needs no model at all. Against the local production build, request every tracked file at its own path:

shell
git ls-files -z | grep -zv '^public/' | while IFS= read -r -d '' f; do
  code=$(curl -gs -o /dev/null -w '%{http_code}' "http://127.0.0.1:3130/$f")
  echo "$code $f"
done > probe.txt
cut -d' ' -f1 probe.txt | sort | uniq -c   # status counts
grep -v '^404' probe.txt                   # everything that did not 404

The -g matters: without it curl treats the brackets in a Next.js route folder name as a glob and silently skips the file, which my first run of this loop did for 34 of them. Then do the same for the public folder, minus its prefix (Next serves public/x.png at /x.png), and flag every file no source file refers to. A 200 is only a lead: compare the body with the file before you call it served.

On this site the corrected loop took seven seconds. Of 679 tracked files outside the public folder, none returned 200. Of 345 files in the public folder, four are blocked on purpose, six images in old article folders are referenced by no source file, 89 are referenced, and 246 were font slices my reference check missed because the stylesheet that names them sits inside the public folder itself. The grep is a heuristic; the curl is the evidence.

The rerun also showed something new. Seventeen requests got a server error where a 404 belonged. Nothing was served, but a reachability change made earlier that day had opened them, which is the argument for rerunning the passes after every cut, not only before.

Next does not serve the repository root as a directory: it serves its routes, its build assets and the public folder, and what the routes return is up to the code. The site where it went wrong had no framework at all. The check costs seconds, and nothing in a plain static deploy will ever prompt you to run it.

Attack surface reduction vs bug hunting: why is closing the path better than finding the bug?

My read is that removing reachable code is a more correct fix than finding the bug in it. A found bug is one fix for one flaw, and the code that held it stays reachable for the next one. A closed path takes every future flaw in that code with it, including the ones nobody will ever report. In the OpenSSH case nobody had to find a memory bug in the ML-DSA parser for the fix to be worth shipping.

Anthropic’s version mostly gates rather than deletes, and between those two I lean to deleting wherever the default is the only configuration anyone runs. A gate is a check, and a check binds only over the paths it covers: the 10.5 gate looked at the advertised algorithm while the parser trusted the key blob, and the code behind it stayed one mismatch away. Deleted code has no second road in. The cost is optionality. When someone wants the experimental path back, it comes back through version control, not through a flag. So I would still gate where somebody runs a non-default setup, which is exactly the case Anthropic sets aside for a human.

It also changes the conversation with whoever reviews the change. A report that says “here is a bug” asks them to verify the bug. A patch that says “this code is unreachable in your default config, and your test suite gives the same results” gives them the change and its evidence together.

Walls are a different tool for a different question: a sandbox like srt limits what a process can touch, while this method shrinks what an outsider can reach in the first place. And in several of the escapes I have looked at, the way out was never code, it was a service or credential the agent had been given, or here, a repository handed to a web server whole.

Should you run attack surface reduction against a live service or a local build?

If you run this tomorrow, run the passes against a local production build, not against your live service. A scan of your own production looks exactly like an attack. When I checked live sites for leaked paths this week, the platform’s bot protection put the requests behind a challenge and the uptime monitor reported the site down until it lifted. And some findings can only be confirmed by doing the thing they warn about, which you do not want to do in production.

A local build reproduces what the application does without touching production. It does not reproduce the host’s caching, and it does not isolate outbound calls on its own, so keep a pass from triggering anything that leaves the machine. Use production for the one or two requests that confirm a finding is live, and then fix it before you write about it.

01 / Discussion

No comment section here — all discussions happen on X.

Max Nardit@mnardit

02 / More articles