defending-code-reference-harness: Anthropic's harness to find and fix vulnerabilities with Claude Code, tested
Anthropic has published defending-code-reference-harness, a reference harness to find and fix vulnerabilities in your code with Claude Code. It is not a scanner you install and forget. It is the full workflow Anthropic has seen work with real security teams (threat model, scan, verification, triage and patch), packaged as Claude Code skills plus an autonomous pipeline that runs in isolated containers.
We cloned it, read it end to end and tested it in Claude Code. This article covers what it includes, how each stage works, how to follow its "day 1 to week 2" plan, what came out of running /vuln-scan and where its limits are. It is for anyone who already uses agents to write code and wants to use them to audit it too.
What defending-code-reference-harness is
It is the companion implementation to Anthropic's article Using LLMs to secure source code (also copied into docs/blog-post.md). It captures what Anthropic learned working with security teams at several organizations since launching Claude Mythos Preview.
Repository facts at the time of testing (commit d3bea6b, August 6, 2026; vuln-pipeline package 0.2.1):
- License: Apache-2.0, not MIT. You can use, modify and redistribute it, including at a company, but if you redistribute the code or a modified version you must keep the license and state your changes. Using it to audit your code puts no obligation on your code.
- Unmaintained: the README states the repository is not maintained and does not accept contributions. Treat it as a reference to build your own workflow, not a product that gets updates.
- Requirements: Python 3.11+, Claude Code and, for the autonomous pipeline, a Linux host with Docker (gVisor only runs on Linux x86_64 or aarch64).
- Model provider: it works with the Anthropic API, a Claude Code token (
claude setup-token) or Bedrock; the article also mentions Vertex and Azure.
If you want something managed, Anthropic points to its Claude Security product. This repository is the open version for teams that want to build and adapt their own pipeline.
The defender's loop: six stages
The whole repository revolves around a six-step loop. The first two are set up once; the other four repeat until scans stop surfacing new findings.

- Threat model: decide what counts as a vulnerability in *your* system.
- Sandbox: an isolated environment to build the target and fire proofs of concept.
- Discovery: surface candidates, as many as possible.
- Verification: confirm which ones are actually exploitable.
- Triage: remove duplicates and rank what to fix first.
- Patching: fix, check the fix and scan again.
The subtitle of the diagram is the point: discovery is easy to parallelize; the bottleneck is everything after it. A model can hand you a hundred candidates before lunch, and if half are noise your team will stop reading them.
What the repository includes
Three blocks, with different risk levels:
| Block | What it is | Runs target code? |
|---|---|---|
| Interactive skills | `/quickstart`, `/threat-model`, `/vuln-scan`, `/triage`, `/patch`, `/customize` in `.claude/skills/` | No (they only read and write files), except `/customize`, which edits the harness and runs validation commands |
| `harness/` (`vuln-pipeline`) | Autonomous recon → find → verify → report → patch pipeline for C/C++ memory bugs with ASAN | Yes: only inside gVisor |
| Detection & response | `/dnr-hunt` and `/dnr-respond` skills + `dnr_harness/` (`dnr-pipeline`) | The skills start the demo app on `127.0.0.1`; the pipeline runs in gVisor |
And five practice targets in targets/:
canary: anentry.cwith three planted bugs (heap overflow, stack overflow and use-after-free). Use it to check everything works in about 6 minutes.drlibs: thedr_wav.handdr_flac.haudio decoders pinned to a commit with two real CVEs (CVE-2026-29022 and CVE-2025-14369).htslib(SAM/BAM/CRAM genomics formats, CVE-2026-31962 through -31971) andalsa(alsa-lib topology, CVE-2026-25068): two more real C libraries, also pinned to vulnerable versions.dnrcanary: a vulnerable Flask app with an attack campaign hidden in a week of generated logs.
One detail worth knowing: .claude/settings.json adds an anthropic-cyber-runbook: skills header to Claude Code requests, and the pipeline adds anthropic-cyber-runbook: pipeline plus a User-Agent token. It is a declared marker (static strings, no content) so Anthropic can attribute usage; docs/pipeline.md explains how to remove it.
Day 1: threat model, scan and triage with the skills
The README lays out a plan by day. Day 1 uses only Claude Code skills and needs no sandbox, because these skills don't build or run anything: they read the code and write reports. Run them interactively and approve each tool use.
git clone https://github.com/anthropics/defending-code-reference-harness
cd defending-code-reference-harness
# Pin every subagent to the model you want
export CLAUDE_CODE_SUBAGENT_MODEL=<model-id>
claudeInside Claude Code:
> /quickstart
> /threat-model bootstrap targets/canary
> /vuln-scan targets/canary
> /triage targets/canary/VULN-FINDINGS.json
> /patch ./TRIAGE.json --repo targets/canaryWhat each one does:
/threat-model:bootstrapdrafts aTHREAT_MODEL.mdfrom the code, docs, git history and past CVEs (context, assets, entry points, trust boundaries and which bug classes matter).interviewwalks someone who knows the system through Shostack's four questions.bootstrap-then-interviewdoes both./vuln-scan: static scan. It splits the code into focus areas based on the threat model, spawns one review subagent per area in parallel and writesVULN-FINDINGS.jsonand.md. Its allowed tools are read-only (Read,Grep,Glob,rg,ls...) plusWritefor the report./triage: verifies each finding with several independent votes (3 by default,--votes 5for important batches), merges duplicates by root cause, re-derives severity from preconditions and assigns an owner.--autoskips the interview./patch: generates a candidate diff per finding and an independent reviewer agent assesses it. In static mode the diffs are inert: they are never applied automatically.
You end up with THREAT_MODEL.md, VULN-FINDINGS.{json,md}, TRIAGE.{json,md} and a PATCHES/ folder.
What happened when we ran /vuln-scan on the canary target
We set up a clean test project with the vuln-scan skill in .claude/skills/ and only the canary target files (entry.c, Dockerfile, config.yaml and its THREAT_MODEL.md), without the sample outputs the repository ships, so the agent couldn't copy them. We ran it non-interactively, limited to the read-only tools the skill declares:
claude -p "/vuln-scan targets/canary" --output-format json \
--permission-mode acceptEdits \
--allowedTools "Read,Glob,Grep,Write,Task,Bash(ls:*),Bash(wc:*),Bash(head:*),Bash(rg:*),Bash(grep:*)"Result: 49 seconds, 6 turns and 0.16 USD with our Claude Code install's default model. It found the three planted bugs at the exact lines:
| ID | Where | What | Confidence |
|---|---|---|---|
| F-001 | `entry.c:27` | `parse_alpha` copies an input-controlled length (0-255) into an 8-byte heap buffer | 0.95 |
| F-002 | `entry.c:40` | `parse_bravo` copies the whole input (up to 4095 bytes) into `char name[16]` | 0.95 |
| F-003 | `entry.c:60` | `parse_charlie` frees the record when the id is `0xff` but doesn't return, then writes to freed memory | 0.93 |
Three things we noticed:
- It spawned no subagents. The skill describes one subagent per focus area, but with a single 86-line C file the agent chose one sequential pass and scored confidence itself. That is reasonable on a tiny target; on a large one, check the transcript to make sure the work is really split.
- It corrected the threat model's line numbers. The bundled
THREAT_MODEL.mdcites the bugs at lines 25, 38 and 58; the agent flagged the mismatch and cited the lines it confirmed withgrep(27, 40 and 60). That is exactly the kind of error the repository tells you to check before you send a report to anyone. - It missed a low-severity finding. The sample run shipped with the repository (
targets/canary/example_skill_runs/) found five candidates, including an uninitialized read inparse_alphawhen the declared length is 0 (it leaks one heap byte to stdout). Ours didn't. This is the variance the repository itself describes: the same prompt on the same code doesn't always return the same findings, so run several passes and union the results.
We didn't run /triage on this output because on the canary it dismisses findings on purpose (the code declares itself as demo code), and we didn't run the autonomous pipeline because it needs gVisor on a Linux host with Docker. For triage and patching we relied on the bundled sample run, which goes through all four skills.
In that run, /triage --auto --votes 3 confirmed 4 of the 5 findings 3 votes to 0, absorbed a possible double free as a duplicate of the use-after-free and kept the uninitialized read at LOW with one precondition (the attacker must see stdout). Then /patch --top 3 produced this diff for the parse_alpha overflow:
size_t claimed = buf[0]; // length field from input, 0-255
unsigned char *out = malloc(8);
if (!out) return;
- // Bug: trusts claimed length without bounding to allocation size.
+ if (claimed > 8) claimed = 8;
+ if (claimed > len - 1) claimed = len - 1;
+
memcpy(out, buf + 1, claimed);The independent reviewer accepted it (style_score 8) and patch_result.json documents the variants checked and the bypass attempt. But look closely: with claimed = 0 the printf on the next line still reads out[0] uninitialized. The patch fixes the overflow, not the one-byte leak, because that LOW finding fell outside --top 3. That is why the repository insists a human reads every diff like any PR from outside.
Day 2: the autonomous pipeline with gVisor
Day 2 moves from skills to autonomous runs, where agents do build the target and throw malicious inputs at it. That is why everything runs inside gVisor containers (a user-space kernel between the agent and your machine) with internet egress limited to the Claude API through an allowlist proxy. Agent-spawning subcommands refuse to start outside that sandbox unless you pass --dangerously-no-sandbox.
# One-time setup: venv, gVisor, agent images and egress proxy
python3 -m venv .venv && .venv/bin/pip install -e .
./scripts/setup_sandbox.sh
# Auth: pick one
export ANTHROPIC_API_KEY=sk-ant-...
# export CLAUDE_CODE_OAUTH_TOKEN=<token from claude setup-token>
# export CLAUDE_CODE_USE_BEDROCK=1
export VULN_PIPELINE_MODEL=<model-id>
# Recon + find + verify + report on dr_libs
bin/vp-sandboxed run drlibs --runs 3 --parallel --stream --auto-focus --max-turns 100
# One candidate patch per finding
bin/vp-sandboxed patch results/drlibs/<timestamp>/
The stages, as implemented in harness/:
- Build: compiles the target with ASAN (the C/C++ memory error detector) into a Docker image. That image is the "trust anchor": every later container spins from it.
- Recon (optional,
--auto-focus): a lightweight agent reads the source with no network and proposes N distinct subsystems to attack separately, so parallel agents don't converge on the same bug. Ondrlibsit proposes about 14 areas. - Find: N parallel agents, each in its own container with
--network noneand 4 GB of memory. They read the source, craft malformed inputs and run the binary until an input crashes it 3 out of 3 times. They readfound_bugs.jsonllive to avoid repeating what a sibling already found. - Grade: a grader agent reproduces each crash in a fresh container the find agent never touched. Only
poc.bincrosses over. - Judge: compares each crash with those already reported and decides
NEW,DUP_BETTERorDUP_SKIP. - Report: one exploitability report per unique bug (primitive, reachability, escalation, constraints and severity).
- Patch (separate command): proposes the fix and runs it up a four-rung ladder: it builds, the original PoC stops crashing, the project's tests still pass, and a fresh find agent can't get around it in 50 turns.
With --stream, the first report (bug_00, the dr_wav out-of-bounds write) usually lands about 10 minutes in. The dr_flac CVE is DoS-class and the default bar drops it: you need --accept-dos and should expect 60-80 minutes to the first crash.
Days 3 to 5: port it to your stack with /customize
The pipeline ships configured for C/C++ memory bugs, but its shape is generic. Porting it to another language or vulnerability class means answering three questions:
| Question | C/C++ reference | Your target (examples) |
|---|---|---|
| What signals a finding? | ASAN crash signature | Exception, canary file, DNS callback |
| What does a proof of concept look like? | Crashing input file | HTTP request sequence, transaction list, test harness |
| How is the target built and run? | `Dockerfile` with clang + ASAN | Your language's build in a container |
The flow is to run the day 1 skills on your code first and hand their output to /customize, which creates targets/<your-service>/ and modifies the harness. Review its plan before approving it, because this skill does edit code and run commands.
> /threat-model bootstrap-then-interview ~/code/my-service
> /vuln-scan ~/code/my-service
> /triage ~/code/my-service/VULN-FINDINGS.json --repo ~/code/my-service
> /customize use ~/code/my-service/{THREAT_MODEL.md,VULN-FINDINGS.json} and ./TRIAGE.mdThen one smoke run before scaling up:
bin/vp-sandboxed run my-service --runs 1Week 2: the outer loop of scan, triage and patch
From here on it is about repetition: waves of runs, triage across all waves and patching the worst first.
bin/vp-sandboxed run my-service --runs 5 --parallel --stream --auto-focus> /triage results/my-service/ --repo ~/code/my-service --auto --votes 5
> /patch results/my-service/<timestamp>/The key idea is that iterating beats parallelizing. Launch ten agents at once and they converge on the same shallow bugs. Fix what the first pass finds and scan again, and the model can no longer re-find those, so it surfaces new and usually deeper ones. If you can't patch quickly, at least record known bugs in the target's known_bugs so later runs steer away from them.
Detection & response: when the attacker is already in your logs
Step 5 is optional and changes focus: instead of prevention, it assumes someone is already in. The dnrcanary target is a vulnerable web app with a week of generated logs containing a loud, failing scanner burst and a quiet campaign that succeeds.
pip install flask pyyaml
python3 targets/dnrcanary/generate_logs.py --seed 42> /dnr-hunt
> /dnr-respond INC-1/dnr-hunt requires a ledger row per query (hypothesis, query, result, next step) and only marks an incident confirmed_exploited when three legs hold: log evidence, the flaw located in source and a PoC fired against a local instance. /dnr-respond ends in a plan (blocks, accounts to suspend, credentials to rotate) that it never executes: that "attacker IP" might be a NAT gateway with 400 legitimate users behind it. You can then score the result against ground truth with targets/dnrcanary/grade.py.
Lessons from the repository you can apply without using it
docs/best-practices.md and the companion article stand on their own. The ones we found most useful:
- The threat model is the biggest lever against false positives. One team found that with a well-defined threat model, 90% of findings were exploitable. Another had a 40% "false positive" rate that turned out to be real bugs that didn't fit what the team considered a threat. Write down what is trusted (config, authenticated clients) and what isn't.
- Separate discovery from verification. Ask the finding agent to also filter and it self-censors real positives. An independent adversarial verifier roughly halves non-exploitable findings; also requiring a PoC brings them close to zero.
- Constraints go in code, not in the prompt. One team told the model it had no network access; it did, and the model fetched from GitHub anyway. Another saw an agent answer a GitHub issue mid-scan. Never leave
~/.aws,~/.sshor.envwithin the agent's reach. - Derive severity from preconditions, not category. Zero preconditions and unauthenticated remote: high. One or two, or authenticated: medium. Three or more, or local-only: low.
- Don't ask the model to compute CVSS. Have it write the vector and compute the score with a tool (Python's
cvsslibrary or the NVD calculator). - Ask for variants explicitly. By default the model only fixes the line you show it. Ask it to look for the same pattern at other call sites and the same bug class elsewhere.
- Turn findings into Semgrep or CodeQL rules so the pattern is caught in CI without spending tokens again.
- If you scan open source projects, treat every finding as a lead, not a report. Reproduce it against a released version, check that cited paths and lines exist, report through the private channel in its
SECURITY.mdand say AI was involved.
Common errors
run refuses to start outside the sandbox. That is by design. Run ./scripts/setup_sandbox.sh and always launch with bin/vp-sandboxed, not .venv/bin/vuln-pipeline directly.
gVisor can't manage cgroups inside a container or nested VM. The script detects this during verification and re-registers runsc with --ignore-cgroups; according to docs/agent-sandbox.md isolation is unaffected.
/triage dismisses every canary finding as a false positive. Also expected: entry.c declares itself as vulnerable demo code and /triage excludes bugs in test or fixture code. To see the full flow, use the fixture /triage .claude/skills/triage/fixtures/canary-findings.json --repo targets/canary or point it at your own code.
The dr_flac CVE never shows up. It is DoS-class and the default bar skips it. Add --accept-dos.
A find agent hangs. It doesn't block results from being written; kill it with docker rm -f find_drlibs_<N>.
Subagents use a different model. Set CLAUDE_CODE_SUBAGENT_MODEL before starting Claude Code; docs/troubleshooting.md covers it in detail.
FAQ
Do I need Docker to get started? No. The day 1 skills (/threat-model, /vuln-scan, /triage and /patch on static findings) only read and write files. Docker and gVisor are needed for the autonomous pipeline.
Does it work with languages other than C and C++? The skills are language-agnostic. The autonomous pipeline ships set up for C/C++ with ASAN and must be ported with /customize by answering the three questions in the table above.
Does it apply patches by itself? No. /patch diffs in static mode are inert, and in pipeline mode the check ladder verifies the crash is gone, not that the diff is ready to upstream. A human has to review it.
Is it the same as Claude Security? No. Claude Security is Anthropic's managed product. This repository is an open, unmaintained reference implementation for building your own pipeline.
What license does it use? Apache-2.0. You can use and modify it, including commercially, keeping the license if you redistribute it.
Conclusion
defending-code-reference-harness is not the tool you'll leave running in production; it is the map for building one: threat model first, noisy discovery, independent verification with a PoC, triage by preconditions and patches that get attacked again. Start with day 1 on your own code and a THREAT_MODEL.md; build the sandbox only once the volume of findings justifies it.
For a lighter review on every change, see the OWASP skill for Claude Code or Cloudflare's security-audit skill. And before you give your agents more tools, read how to prevent prompt injection in AI agents.
// source code
// implementation
More on AI agents

- Skills for your agents
- Claude Code

- Skills for your agents
- Claude Code

- Skills for your agents
- Claude Code