mcpbeat

Dnr Respond

anthropics/dnr-respond

Incident response workup for a lead in hand — an alert, an IOC, or a /dnr-hunt finding. Scopes the lead across the logs, verdicts whether the attack succeeded, quantifies blast radius, confirms root cause in source with a local PoC, and writes a proposed (never executed) containment/remediation/recovery plan. Use when asked to "respond to this alert", "work this incident", "how bad is this", or "run dnr-respond". /dnr-hunt is the no-alert entry to the same track.

4k tokens
context cost
the whole folder, loaded on every use
2
files
instructions only
0
copies elsewhere
how many repositories repackaged it
6909
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/anthropics/defending-code-reference-harness --skill dnr-respond

What comes with it

2 960 bytes besides the instruction
README.md

What it tells the agent to use

found in the instruction text
Bash runs shell commands — read the instruction before connecting
Read reads your files
Grep reads your files

The instruction itself

9 sections, as written by the author

dnr-respond

Lead-in-hand incident response: take one concrete starting point and answer

the four questions an incident commander needs, in order — **what happened,

did it succeed, how far did it go, and what do we do about it.** The output

is a verdict, a quantified blast radius, a root cause proven in source, and

a response plan that is *proposed, never executed*.

Invoke with /dnr-respond <lead> [--logs dir] [--repo path] [--fresh].

The lead ($1) can be:

  • an incident id from the hunt's INCIDENTS.json (e.g. INC-2) — the

/dnr-hunt handoff

  • an alert pasted from an alert queue (JSON line or prose)
  • an IOC: an IP, an account name, a route, a session identifier
  • free text ("a customer says their address was quoted in a phishing email")

Other arguments:

  • --logs = logs directory or dnr target directory (same resolution as

/dnr-hunt; defaults to targets/dnrcanary if it exists, else cwd).

  • --repo = application source (defaults to the target's app/).
  • --fresh = ignore any checkpoint in the run dir's .dnr-respond-state/

and mint a new run dir.

Hard rules (shared with /dnr-hunt — the track's contract)

  • Log evidence alone never confirms a vulnerability.

confirmed_exploited requires log evidence + the flaw in source + a PoC

fired locally this session. Otherwise the ceiling is suspected.

  • Query, don't read. Never Read or cat a log file; wc/head/

tail to profile, grep/awk/sort/uniq/python3 to interrogate.

Small files (alert queues, error logs under ~200 lines) may be read whole.

  • Propose, never execute. No blocking, disabling, rotating, or config

changes — in any environment, including the demo app. The plan names

actions; humans execute them.

  • Local only. PoCs fire at 127.0.0.1 against an instance you

started. Never touch a remote host.

  • Every number is a query result. "How many records" and "how many

customers" get answered by pipelines run this session, never estimated.

Run directory and checkpointing

First action — establish the run directory, the same way /dnr-hunt

does, but as a *consumer*. Let TARGET be the basename of the resolved

--logs/target path (default targets/dnrcanary) and SCOPE that path.

  • Lead is INC-n (the /dnr-hunt handoff, or a second lead from a hunt

you already started responding to): work the response into the investigation

that incident lives in. {RUN} is the newest results/TARGET/<timestamp>/

directory that contains an INCIDENTS.json — find it with Bash, e.g.

ls -1d results/TARGET/*/ | sort -r | while read d; do [ -f "$d/INCIDENTS.json" ] && echo "$d" && break; done.

Do not call rundir for an INC-n lead — rundir mints a fresh empty

dir once a prior /dnr-respond in that dir has completed, which would strand

the incident's INCIDENTS.json in the old dir. Only if no run dir holds an

INCIDENTS.json (you were handed INC-n but never ran the hunt) fall through

to the rundir line below.

  • Any other lead (alert, IOC, free text): Bash

python3 .claude/skills/_lib/checkpoint.py rundir results/TARGET --state .dnr-respond-state --scope "SCOPE"

(append --fresh if it's in $ARGUMENTS). This adopts the newest run dir for

the target if one exists, else mints a fresh results/TARGET/<timestamp>/.

That path is {RUN}; substitute the literal path everywhere below, and

double-quote it (and SCOPE) in Bash.

Checkpoint mechanics are identical to /dnr-hunt, state dir

{RUN}/.dnr-respond-state/, phases 0–4. When {RUN} already holds a

completed .dnr-respond-state from an earlier lead, load reports

complete and the rules below reset it cleanly — a second lead enriches the

same INCIDENTS.json and writes its own RESPONSE-<lead>.md. On start:

python3 .claude/skills/_lib/checkpoint.py load {RUN}/.dnr-respond-state

absent/complete/--freshreset and start at Phase 0; running with

phase_done == N → read phase0.jsonphaseN.json, resume at N+1. After

each phase: Write payload to {RUN}/.dnr-respond-state/_chunk.tmp, then

checkpoint.py save {RUN}/.dnr-respond-state <N> <name> --from {RUN}/.dnr-respond-state/_chunk.tmp.

Never write progress.json directly; never pass payload via heredoc/stdin.

After outputs: checkpoint.py done {RUN}/.dnr-respond-state 4.

Phase 0 — Parse the lead, locate inputs

  • Resolve --logs (dnr target config or bare directory) and inventory log

files (du, wc -l, head -2 each — identify formats and field

positions). The corpus is expected to exist already — the target README

covers generating it.

  • Parse the lead into starting entities:
  • INC-n → read that entry from {RUN}/INCIDENTS.json (the run dir

adopted above); its IPs, accounts, routes, and timeline rows are the

seed set.

  • alert line → source IP, rule, path, timestamp.
  • IOC → the entity itself.
  • free text → extract the observables (who reported, what data, when);

the seed set may start as just a route or a table name.

  • State the working question in one sentence (e.g. "did 203.0.113.66's

activity against /login succeed, and if not, what did?"). Everything in

Phase 1 serves that question.

Checkpoint phase0.json:

{"lead": ..., "lead_type": ..., "entities": {"ips": [], "accounts": [], "routes": [], "windows": []}, "question": ..., "logs_dir": ..., "repo": ..., "files": [...]}

Phase 1 — Scope: pull everything the entities touched

For each seed entity, extract its complete activity, then expand one ring:

  • Per IP: every request — time-ordered, with route, status, size, UA,

and authenticated user. Summarize phases of behavior, don't paste

hundreds of lines.

  • Per account: every login (from which IPs), every authenticated

request. Compare against that account's historical pattern in the same

corpus — new IP, new UA, new volume, new hours are all signal. (A new

IP *alone* is baseline noise — users roam; combine it with volume,

cadence, or what the session did next before treating it as a lead.)

  • Per route: who else hit it the same way in the window? One attacker

found via an alert may not be the only one.

  • Ring expansion (once): entities discovered above join the set —

the account an IP logged into, the second IP that touched the same

account, the route a session pivoted to. Expand once, deliberately;

/dnr-hunt's open-ended loop is the tool when scope keeps growing.

  • Correlate the error log and alert queue rows for all entities into

one timeline.

Checkpoint phase1.json:

{"activity": [{"entity", "summary", "first_seen", "last_seen", "requests", "key_lines": [...]}], "expanded_entities": [...], "timeline": [...]}

Phase 2 — Verdict and blast radius

Answer the working question with evidence on both sides:

  • Did it succeed? Status codes alone don't answer this — a 401 wall

says no; 200s with anomalous sizes, error-then-success progressions, and

data-shaped responses say yes. State the verdict and the discriminating

evidence.

  • Blast radius, quantified. What data was reached: which

routes/tables, how many records, over what time span, belonging to how

many distinct users/customers. Each figure comes from a pipeline (count

the exfil-page sizes, count the distinct ids walked, join walked ids to

owners via the app's own data if available locally — running the

target's deterministic seed_command early, before Phase 3, is fine).

  • Containment status check (read-only). Is the activity ongoing at the

end of the corpus window, or did it stop? When was the last attacker

action?

  • If the verdict is "did not succeed", the workup doesn't end — say what

the attacker was *trying* and why it failed; that feeds the plan's

hardening section and the ruled_out record.

Checkpoint phase2.json:

{"verdict": ..., "succeeded": bool, "discriminators": [...], "blast_radius": {"data": ..., "records": N, "distinct_victims": N, "window": ...}, "ongoing": bool, "last_activity": ...}

Phase 3 — Root cause and PoC

Same bar as /dnr-hunt Phase 3–4:

  • Read the implicated handler(s) in --repo; name the flaw (or the

defense) at file/function precision; check sibling routes for the same

flaw class.

  • Seed and start the app locally per target config: seed_command /

app_command are target-relative — run them with the target

directory as cwd; launch in the background via a single command that

prints the PID (the same python3 -c "...subprocess.Popen..." pattern

/dnr-hunt Phase 4 shows, with the port override env var if the default

port is busy). Fire the minimal PoC, record command + response

excerpt, fire the negative PoC against defended routes, then

kill <pid> (never by port or pattern).

  • No runnable app → verdict ceiling is suspected, stated explicitly.

Checkpoint phase3.json:

{"root_cause": {"file", "function", "mechanism"}, "poc": {"command", "status", "response_excerpt"}, "negative_poc": {...}, "siblings_checked": [...]}

Phase 4 — The response plan (PROPOSED, NEVER EXECUTED) and outputs

Write the plan to {RUN}/RESPONSE.md — or {RUN}/RESPONSE-<lead>.md (e.g.

RESPONSE-INC-2.md) if a RESPONSE.md from a different lead already

exists in the run dir; never silently overwrite another incident's plan.

Sections, every action carrying what / why / risk-if-wrong:

  • Verdict & summary — three sentences: what happened, did it succeed,

how far it went.

  • Timeline — the merged entity timeline from Phase 1.
  • Containment (proposed) — block/deny candidates (IPs, sessions,

tokens), accounts to suspend, routes to rate-limit or disable. For each:

the evidence line that justifies it and what breaks if the call is wrong

(e.g. blocking a NAT IP locks out legitimate users behind it).

  • Eradication & remediation (proposed) — the code fix, described at

file/function level, handed off as a /triage-compatible vuln entry;

interim mitigations if the fix will take time.

  • Recovery (proposed) — exactly which credentials rotate (the queried

list, not "all users"), which customers get notified (the distinct-victim

list from Phase 2), what monitoring confirms the attacker is out.

  • Detection engineering (proposed) — the rules that would have caught

this earlier, derived from what you actually used to find it: the

discriminating queries from Phase 2 turned into alert logic (e.g.

error-rate per source on a route; sequential-id access cadence;

account-from-new-IP at volume). Note which existing alerts fired,

which were noise, and what that says about the queue.

  • Open questions — what this corpus cannot answer (earlier history,

other log sources, whether stolen data was used elsewhere).

Then update {RUN}/INCIDENTS.json (same schema as /dnr-hunt; create it if

absent): add or enrich this incident — verdict, quantified impact, root

cause, PoC, timeline. Extra keys are fine — graders match on content, not

structure. Negative outcomes go to ruled_out.

Mark complete (checkpoint.py done {RUN}/.dnr-respond-state 4) and point the

user onward (substitute the literal {RUN} path):

  • for the fix, from inside {RUN}: /triage INCIDENTS.json --repo <repo>

then /patch TRIAGE.json --repo <repo> — both write to the directory

they're invoked from, so running them there lands the verdicts and

patches beside the incidents

  • on a dnr target: python <target>/grade.py {RUN}/INCIDENTS.json to

self-score (the user runs this)

  • a human reviews and executes (or rejects) each proposed action; nothing

in RESPONSE.md has been done

Customizing beyond the demo

  • Feed it your real alert queue one alert at a time — the lead parser

takes any JSON or prose alert; the method doesn't change.

  • Auth logs, CDN/WAF exports, and EDR threat events slot into Phase 1 as

additional per-entity activity sources; keep the single merged timeline.

  • The Phase 4 plan template is the part most worth adapting to your org:

map containment to your actual controls (WAF rules, IdP session

revocation, feature flags) — still as proposals routed to the owning

team, never as actions the agent takes.

How to use it

Copy the folder

Take anthropics/dnr-respond from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.