Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/C2.12.mp4An agent narrates a confident architecture from status codes, and the report is fiction that reads like findings.
Every claim is labelled observed or inferred, with the evidence that supports it, and inferences never carry a severity.
Score twelve claims from an external probe run and see which survive the observed/inferred split.
From outside, an agent will tell you the target runs Django on PostgreSQL with asynchronous refunds, in fluent prose, from a cookie name and a stack trace. Some of that is entailed by the evidence and most of it is a guess, and the report does not say which until you make it.
pentest.Claim makes the distinction structural rather than editorial: an inference cannot be given a severity, and an observation with no evidence will not construct. report_claims() prints the inference ratio in the header, because a black-box report that is 80% inference is a hypothesis document and saying so is what makes the other 20% worth reading.Keeping observation and inference apart structurally rather than editorially — an inference cannot carry a severity, an observation cannot exist without evidence — and publishing the ratio, which is what makes the observed part worth acting on.
An agent narrates a confident architecture from status codes, and the report is fiction that reads like findings.
Label every claim observed or inferred, keep the evidence beside it, and attach no severity to an inference.
Findings versus open questions — five and seven on the sample run — and what each open question would need to resolve it.
Measure this
| what to measure | denominator or target |
|---|---|
| findings against open questions | two counts — five and seven on the sample run |
| what each open question would need to resolve it | named, per question |
evidence claim verdict
handshake completed serves TLS 1.3 OBSERVED -> finding
200 known / 404 random /bookings/{id} exists OBSERVED -> finding
404 on a random id returns ANOTHER tenant's INFERRED -> question
csrftoken cookie runs Django INFERRED -> question
1.9s vs 120ms refunds are synchronous INFERRED -> question
a finding carries a severity. a question carries what would resolve it.
an inference with a CVSS score is a finding to everyone downstream.Black box is the mode with the least information and the most room to narrate. An agent handed status codes, headers, error strings and timings will produce a fluent architecture, and every sentence of it will be plausible. Some of it is entailed by the evidence. Most of it is not, and the report does not separate them unless you make it.
The single discipline is provenance per claim. For each thing the agent wants to assert, keep the actual observation beside it and ask one question: does this evidence entail the claim, or is it merely consistent with it?
csrftoken cookie is consistent with Django and with anything that copied its conventions.Observed claims are findings. Inferred ones are open questions, and they carry no severity — because an inference with a CVSS number beside it reads as a finding to everyone downstream, and the person who has to retract it is never the person who wrote it.
The open questions are not filler. Each one says what mode would resolve it, and the ones that need a second credential are telling you the engagement should become grey box (C2.13) rather than a more elaborate black-box guess.
The skill takes twelve claims from an outside probe of CyberTravels and sorts them into two piles:
It reports the two piles separately — findings in one, open questions in the other. Neither pile gets a severity attached to a guess.
The skill — skills/redteam/blackbox-claim-provenance/SKILL.md
name: blackbox-claim-provenance
description: >-
Split an external probe's claims into what the evidence entails and what it
merely suggests, and refuse to attach a severity to the second kind. Use for a
black-box engagement, or when a report reads more confidently than its
evidence supports.
allowed-tools: Read, Grep, GlobBlack box is the mode with the least information and the most room to narrate. A model handed status codes, headers and timings will produce a fluent architecture, and every sentence of it will be plausible. Some of it is entailed by the evidence. Most of it is not, and the report does not distinguish them unless you make it.
An external engagement with no credential, a bug-bounty triage queue, and any report where the reader cannot tell which claims were tested.
1 — List what the mode may look at. DNS and certificate transparency, the TLS handshake, HTTP status/headers/body, error strings, response timing, public artefacts, the scope document. The list is short, and that is the constraint.
2 — Write each claim with the evidence beside it. Not a summary of the evidence — the actual observation that produced the claim.
3 — Ask one question per claim: does this evidence entail the claim, or is it merely consistent with it? A 404 on a random id is consistent with correct authorisation and with a missing object. A csrftoken cookie is consistent with Django and with anything copying its conventions.
4 — Report observed claims as findings and inferred ones as open questions, with no severity on the second list. Then say what each open question would need. The ones needing a second credential are grey-box tests, not better black-box ones.
report: 5 findings, 7 open questions
no severity is attached to anything in the second list.
The run continues past this. test_skills.py executes the script on every build, with no model configured — so what CI proves is that it runs and refuses legibly, not that it prints these lines. The output above is a recorded run against a real model, and nothing re-diffs it: a model's answer is not reproducible, and this repository does not claim otherwise anywhere it can be checked.
{
"data_sources": [{"source": "str", "establishes": "str"}],
"claims": [{"claim": "str", "evidence": "str", "entailed": true}],
"findings": ["str"],
"open_questions": [{"claim": "str", "would_need": "str"}]
}
Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.
The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.
Install it, once. From the folder you cloned. --all does every agent it finds on your machine:
python3 scripts/install_skills.py --all --lessons C2.12Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick c2-12-black-box-agentic-pentest:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons C2.12 | type /c2-12-black-box-agentic-pentest |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons C2.12 | type /c2-12-black-box-agentic-pentest in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons C2.12 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons C2.12 | type $c2-12-black-box-agentic-pentest, or run /skills |
No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.
python3 scripts/lesson.py C2.12Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.
python3 scripts/checkpoint.py --at C2.12 --out work/cybertravels
python3 scripts/checkpoint.py --at C2.12 --diffOn Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.
Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.
Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.
If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.
Five claims are entailed by their evidence and reported as findings; seven are only consistent with it and become open questions with no severity attached. The one that matters — whether the refund endpoint accepts a booking it does not own — is untestable with one account, so it is escalated to a grey-box test rather than guessed at.
Take your last external report and mark every claim observed or inferred. The inferred ones that carry a severity are the ones a client can disprove, and disproving one is how they learn to discount the rest.
Tools used
Skill source: skills/redteam/blackbox-claim-provenance