The model that finds the bug will defend the bug
Why I separated discovery from validation in BBHUNTER, and why I require a reproducible request before accepting an AI verdict.

While building BBHUNTER, I noticed a failure in how I was using models. A model would suggest a plausible bug. When the lead was missing a clean request, a second account, or proof that the data was private, asking the same chat to review itself rarely exposed that gap. It mostly produced a better explanation of the original guess.
That is a bad property for a security workflow. A finding is not real because its explanation sounds coherent. It is real when another person can reproduce the behavior, inside scope, from the requests and conditions recorded in the report.
So I stopped treating “find” and “judge” as one job.
A model is useful before it is authoritative
LLMs are good at the loose work near the beginning of an investigation. They can group similar endpoints, summarize an unfamiliar bundle, point out parameters I missed, and turn a directory of recon output into questions worth checking. None of those jobs require the model to declare that a vulnerability exists.
The trouble starts when the same context is asked to validate its own idea. The model has already spent tokens constructing a story. The next response is generated from that story as well as the evidence. “Review your answer” sounds independent, but it is still the same conversation defending the same frame.
The state model I use
BBHUNTER keeps observations, leads, findings, and reports separate. That distinction looks fussy until a scanner result starts getting repeated as fact.
observation something responded
lead the behavior may cross a security boundary
finding the behavior was reproduced and impact was demonstrated
report another person can follow the evidence without my private notes
A JavaScript route is an observation. An exposed-looking identifier is an
observation. A Nuclei alert is a lead at best. Moving one of them into findings/
requires a request, response, affected principal, expected boundary, and a result
that survives a fresh session.
The repository makes that progression visible:
targets/<program>/
├── recon/
├── notes/
├── leads/
├── findings/
└── reports/
This is more important than prompt cleverness. A file cannot quietly become a confirmed issue because the language around it grew more confident.
Two papers helped me name the problem. One found that self-critique could add false positives when the critic had no sound verifier. In experiments on small models and reasoning tasks, another found better self-correction when the verifier was stronger than the model producing the answer. A fresh model is not enough; it needs better evidence. See Stechly et al. and Wang et al..
P8: try to make the lead real
The first gate asks two questions: can I confirm the behavior, and what is the real impact ceiling? I check parameter variants and sibling endpoints only within the same authorized test boundary. Reproduction from scratch belongs to P9.
I record the smallest request that triggers it, the identity used, the identity that should own the resource, and the response that proves the boundary was crossed. I also check the boring explanations first: public data, a sandbox tenant, a cache, an intended anonymous route, or a scanner matching text without understanding it.
My worksheet is deliberately plain:
Claim:
In-scope host:
Actor and permissions:
Resource owner:
Exact request:
Expected result:
Observed result:
Security boundary crossed:
Evidence still missing:
An LLM may help list alternative explanations, but it does not get to fill the evidence fields from memory. Unknown stays unknown.
P9: reproduce it without the discovery trail
At P9 I open a clean session and use only what would appear in the report. No proxy history, no half-remembered setup step, no cookie that I cannot explain.
The test is simple:
- Create or identify the minimum actors required.
- Establish the expected authorization boundary.
- Send the recorded request.
- Capture the response and resulting state.
- Repeat from a new session.
If the reproduction needs a detail that exists only in my head, the report is not ready. If it works only once, I do not yet understand the condition. If I cannot separate my own accounts or test data from another user’s data, I have not proved impact.
This gate also catches accidental dependencies: service-worker responses, old cookies, and roles granted earlier in the session.
P10: read it like the person rejecting it
The last gate is hostile triage. I try to kill the report before a program does.
- Is the asset and behavior in scope?
- Is the affected data actually private?
- Is the result different from the documented product behavior?
- Does the proof rely on a privileged or self-created condition?
- Can the impact be stated without “could,” “might,” or “potentially”?
- Are the steps complete enough for a stranger to reproduce?
This is where a second model or session can help. It gets the proposed report and program rules. It gets none of the discovery chat. I ask it for rejection reasons and missing evidence, not a severity or a verdict.
Act as a skeptical vulnerability triager.
Use only the supplied report and policy.
Return:
1. unsupported claims
2. plausible benign explanations
3. missing reproduction steps
4. scope or eligibility conflicts
5. the exact evidence needed to resolve each item
Do not repair the report or infer facts that are not present.
The output is a checklist, not a verdict. I resolve each item against the target or remove the claim.
Rules I still enforce by hand
I do not ask a model to remember scope, compare raw responses, redact secrets, or decide whether a request changed server state. These are operating rules, not guarantees the current BBHUNTER code enforces automatically.
- Scope is stored as explicit hosts and exclusions.
- Requests and responses are saved verbatim, with secrets redacted before sharing.
- Diffs compare status, headers, body, and relevant state.
- A finding keeps links back to the lead and its evidence.
- A report is generated only from the finding record.
The model can explain a diff. The diff itself should come from bytes, not prose.
Authorization and harm require judgment. A system cannot infer permission from a hostname, and a clever hypothesis does not widen a program’s scope. I stop when a test would touch another person’s account, create lasting state, or exceed the published rules.
I also do not use model agreement as confidence. Three agents repeating the same claim may share the same training patterns and the same missing context. Independence comes from evidence collection: a fresh session, a smaller request, a second actor, and an observable boundary.
The model that found the lead can keep its enthusiasm. The report has to survive without it.
This note also appears on Shellcat. Read that edition.