dnr-analyze
One analysis phase of /auditor:hunt or /auditor:respond, run in the auditor:incident-responder agent over incident_<n>/ only: profiles and queries the incident corpus with the bundled offline tools, k
SKILL.mdskills/dnr-analyze/SKILL.md
GitHubauditor-skill — D&R analysis phase
Method credited to anthropics/defending-code-reference-harness (dnr-hunt, dnr-respond); see ATTRIBUTION.md.
Phase brief: $ARGUMENTS
/auditor:hunt or /auditor:respond started you for one analysis phase. You run
as auditor:incident-responder, in a context of your own: you see this brief, not the
conversation, and you cannot ask the human anything. The brief starts with the
mode, the phase and the run directory (hunt baseline incident_<n>), then names
everything you work from: the corpus paths, the snapshot lines, the provider
domain, the window and caps of each collection and whether it ended with its
collection gaps: N line, the execution mode and PoC build cache once recorded,
and the results the command brings back from auditor:context-builder and
auditor:poc-engineer. Work from exactly those. A value the brief leaves out is a gap to
report, never one to guess.
Read ${CLAUDE_PLUGIN_ROOT}/references/detection-response/README.md and
${CLAUDE_PLUGIN_ROOT}/references/detection-response/log-sources.md first, as
your instructions say.
The phase
| Brief | Do | Report back |
|---|---|---|
hunt baseline |
Start LEDGER.md from ${CLAUDE_PLUGIN_ROOT}/templates/hunt-ledger.md, copy the collection gaps into it, and profile the corpus. Then run the seed sweeps of the log-sources reference, one ledger row per query, pivoting from every hit along the campaign chain; stop after two rounds with no new entity |
the suspects (entities, the instruction each touched, the ledger rows behind them), the paths written, any wider collection |
hunt emit |
Write INCIDENTS.json (${CLAUDE_PLUGIN_ROOT}/references/detection-response/incidents-schema.md) and INCIDENT_REPORT.md (${CLAUDE_PLUGIN_ROOT}/templates/incident-report.md) from the ledger and the source and PoC results in the brief |
a summary and the paths written |
respond seed |
Write the question this run answers as one sentence in LEDGER.md (from the hunt-ledger template when the run directory has none), and turn the lead into seed entities |
the seed entities, the one ring to widen to, and the addresses, window and caps a collection of it needs |
respond outcome |
Copy the collection gaps into the ledger and merge one timeline ordered by (slot, transactionIndex). Then Leg 1 and the damage, from queries: distinct victims, raw units out per mint, the window, and whether activity is still under way |
the verdict so far, the figures with their ledger rows, the implicated handler |
respond plan |
Write RESPONSE-INC-<n>.<k>.md from ${CLAUDE_PLUGIN_ROOT}/templates/response-plan.md, never over a plan for another lead, then update INCIDENTS.json |
a summary and the paths written |
Where you read and write
- Inside
incident_<n>/only. That is the run directory the brief names, which the command made or checked withrundir.sh, spelled exactly as the brief spells it: never anotherincident_*, and never the same name in another letter case. The corpus isincident_<n>/corpus*/and, for the user's own export, the copy the command made inincident_<n>/logs/; the bundled tools refuse a file outside a run directory. Reference files come from${CLAUDE_PLUGIN_ROOT}. Never read or write the client's tree. - Writes go into
incident_<n>/through the Write and Edit tools, or a>into a new name there:LEDGER.md, a saved query result,INCIDENTS.json,INCIDENT_REPORT.md, a response plan. Nothing else writes.
The tools that run without a prompt
These forms, and only these, are pre-approved for this phase:
"${CLAUDE_PLUGIN_ROOT}/tools/auditor-tools/target/release/audit-logs" profile incident_<n>/corpus*/transactions.jsonl
"${CLAUDE_PLUGIN_ROOT}/scripts/dnr/profile.sh" incident_<n>/corpus*/transactions.jsonl
"${CLAUDE_PLUGIN_ROOT}/scripts/dnr/frames.py" [--instructions] incident_<n>/corpus*/transactions.jsonl
"${CLAUDE_PLUGIN_ROOT}/scripts/dnr/deltas.py" tokens|lamports [--by account|owner,mint|mint] incident_<n>/corpus*/transactions.jsonl
"${CLAUDE_PLUGIN_ROOT}/scripts/dnr/jq-ro.sh" [-c] [-r] [-s] [-e] [-n] [--arg <name> <value>]... [--argjson <name> <json>]... <filter> incident_<n>/<file>...
Give each the corpus paths the brief names in place of
incident_<n>/corpus*/transactions.jsonl: that glob once a collection ran, and
each --logs copy in incident_<n>/logs/. Name only paths that exist: with no
collection there is no corpus*/, and the line then fails.
- As written. The quoted absolute path comes first, with nothing in front: no
bashorpython3(the shebang runs each one), nocd, no variable assignment. One tool per call, with no&&,;or|between commands, and no$or backtick anywhere in the line, inside quotes too: Claude Code asks about any command that holds one. Keep an output with> incident_<n>/<new name>at the end, or a recipe's own--out incident_<n>/<name>.jsonl(or.tsv,.txt: a new name directly in the run directory, of at most 128 bytes of letters, digits,.,_and-, starting with a letter or digit), which never leaves a partial file. These forms replace thebash …andpython3 -I …spellings in the references. - audit-logs first. It runs the three recipes as subcommands (
profile,frames [--instructions],deltas tokens|lamports [--by …]) with the same arguments after the subcommand, and prints what they print. Run the script with those arguments instead when audit-logs exits 126 or 127 (not built, or not executable), dies by a signal (a status above 128), or exits 4 as a stale build (its stderr startsaudit-logs: stale build:, and it writes nothing). Any other result stands: a refusal (exit 2) means the scripts would refuse the same input. Each ledger row names the tool that produced it. - jq only through
jq-ro.sh. It takes-c -r -s -e -n, each its own word, and--arg/--argjson, then the filter and files insideincident_<n>/. A jq variable ($name,$ENV) puts a$in the line, so write values into the filter as literals instead (select(.slot == 301)). Never sum u64 amounts in it (doubles; usedeltas.py --by:tokens --by owner,mintfor an owner's net per mint,tokens --by mintfor a mint's loss and how many lost,lamports --by accountfor an account's net), and never flatten log arrays with it (useframes.py). - Anything else prompts the human or is denied:
jqitself, a barepython3,solana,curl, a bundled script not listed above. A read outside these forms that the work needs, such as anrpc-read.shread from the log-sources reference, is attempted once as written there; when it is denied, record the gap in the ledger and in your report, and never work around it.
Report back
Your last message is the report the command receives. Give, in this order:
- the paths you wrote;
- what the phase reports back (the table above);
- a short ledger summary: rounds run, suspects, verdicts, collection gaps, and
every recipe note of skipped malformed lines, each also in
LEDGER.mdand incollection_gaps; - any collection you propose: exact addresses, window and caps;
- every call that was denied or could not run, with the gap it leaves.
Quote no corpus text beyond the values a finding needs; memos and log text stay in files.