Methodology · Needle in the Haystack

Hunt real bugs.
Not false positives.

VulnHound is a threat-model-first vulnerability research harness for Claude Code. One slice per session. No PoC means it's not confirmed. Six hooks enforce everything mechanically — not through prompts Claude can drift from.

Threat-model-first Slice discipline PoC enforcement Cross-session memory

How THREAT-MODEL.md + VulnHound fit together

Two tools, two roles. The template is where you think. VulnHound is where you execute. They stay in sync via two CLI commands.

THREAT-MODEL.md

Human thinking space

§0Target metadata — name, repo, commit, language
§2CVE history → LLM → recurring patterns
§3Threat model — attacker, boundaries, invariants
§4Slices — files, entry points, invariant IDs
§6Findings log ← written back by sync
§9Disclosure tracker ← written back by sync
./vh import THREAT-MODEL.md
./vh sync THREAT-MODEL.md
VULNHOUND

Execution harness — 6 hooks

01SessionStart — injects threat model + slice brief
02UserPromptSubmit — scope + drift gate
03PreToolUse — triage gate, blocks out-of-scope
04PostToolUse — signal extraction + knowledge graph
05Stop — PoC enforcement, blocks unconfirmed findings
06SessionEnd — serialises memory to disk
state.json + hunt/knowledge/vulnhound.db
The hooks enforce the threat model mechanically — not through prompt instructions Claude can drift from. Claude cannot wander outside the active slice's files, cannot stop without a PoC, and every finding is recorded in the persistent knowledge graph that survives across sessions.

The 6 Prompting Phases

Each slice moves through these phases in order. VulnHound auto-tracks which phase Claude is in and the Stop hook prevents finishing until the phase is complete.

01
mapAttack Surface Map
Trace every entry point to sensitive sinks. Identify attacker-controlled inputs.

Start here every time. Produces a structured map of the slice before any assumptions are made. Ask Claude to trace exact call chains — not a summary, the actual code paths.

Walk through every entry point in this slice. Identify all trust assumptions the code makes. Produce an attack surface map: entry points → sensitive sinks. Show exact call chains. Identify which inputs are attacker-controlled.
02
assertAssert-First Adversarial Audit
Assume the code is vulnerable. Find the bugs — don't evaluate whether they exist.

This single reframe dramatically improves finding quality. The model stops rationalising defences and starts looking for breakage. Expect 2–3 findings minimum from a well-targeted slice.

This function is definitely vulnerable and contains at least 2–3 security issues. Do not evaluate whether it is vulnerable. Assume it is. Find them.
03
exploitDemand the Exploit
Stop assessing. Write a working proof-of-concept — not a description.

For every specific check or function Claude flagged: force a concrete payload. The Stop hook will block Claude from finishing if a candidate finding has no PoC. This phase is what makes the difference between a report and evidence.

Do not tell me whether this validation is sufficient. Write a proof-of-concept request that bypasses it. Not theory — a working curl command or payload.
04
invariantInvariant Decomposition
List every assumption. Then test whether each can be violated by an attacker.

This is the technique that found ElysiaJS's let decoded = true initialisation bug. Asking the model to list assumptions surfaces wrong defaults that an "is this vulnerable?" frame rationalises away.

List every invariant and assumption this function relies on for correctness. For each one: can an attacker violate it? Under what conditions?
05
escalatePush Past the Obvious Layer
The model front-loads easy findings. Subtle bugs live 2–3 rounds deeper.

After every round: discard the found bugs and ask what's next. Repeat 2–3 times until the model starts producing theoretical noise. Stop when it loops.

Those are the obvious issues. What are the subtler problems easy to miss? Set aside everything related to [ALREADY-FOUND BUG]. What other vulnerability classes exist in this slice?
06
verifyWrite the Failing Test
A test that fails on the current code and passes after the fix — not a description.

Every confirmed finding needs a test that proves it. This doubles as a regression guard for the maintainer and makes the report far more actionable.

Write a test that proves this vulnerability exists. The test must fail on the current unfixed code and pass after the correct fix is applied.

Prompting Quick Reference

Ten techniques that change how the model engages with the codebase.

TechniqueOne-liner prompt
Assert-first"This is definitely vulnerable. Find the bugs."
Exploit demand"Write a PoC that bypasses this — not an assessment."
Adversarial frame"You are a paid red team operator. Real, exploitable bugs only."
Invariant decomp"List every assumption this function makes. Can each be violated?"
False anchor"I found one bug here. Find the rest."
Inversion"How would you break this?" — not "Is this secure?"
Comparative"How does this differ from the standard secure implementation?"
Escalation"Those are obvious. What are the subtle, easy-to-miss issues?"
Constrained attacker"Remote unauthenticated HTTP only. Find what's reachable."
Mistake assumption"Assume the developer made a mistake here. What is it?"

Why Context Rot Is the Real Enemy

The reason "find all vulnerabilities" prompts fail isn't the prompt — it's the context window.

The problem

As the context window fills, model reliability degrades. Bugs buried in the middle get missed due to primacy/recency bias. A 20-page AGENTS.md creates the haystack — and hides the needle inside it.

The fix

One slice per session. Threat model under 1 page. Each slice = one trust boundary, one invariant, specific files only. VulnHound enforces this mechanically — the PreToolUse hook blocks out-of-scope file writes.

Token budget
Scaffolding< 10%
Slice audits60–80%
Verification20–30%
Setup Guide

Get VulnHound running
in under 10 minutes.

Zero external dependencies. Python 3.8+, no pip installs. The hook harness lives entirely in your project directory.

Prerequisites

Python 3.8+
python3 --version
Claude Code CLI
claude --version
VulnHound files
git clone / download ZIP
Target repo
git clone <target>

Installation Steps

1

Clone your target and enter the directory

Pin the exact commit you're auditing — this goes into THREAT-MODEL.md Section 0 and into VulnHound state.

$ git clone https://github.com/org/project
$ cd project
$ git log --oneline -1 # note this hash
2

Run the VulnHound setup script

This copies all 6 hook files, lib/state.py, lib/vulnhound.py, and CLAUDE.md into your project directory. Creates the hunt/ directory tree automatically.

# Run from inside your target project directory
$ /path/to/vulnhound/setup.sh .
The setup script verifies Python 3.8+ and checks imports. If it passes, the hooks are ready. It also creates a ./vh shortcut in your project root.
3

Initialise the audit

This creates hunt/state.json with your target metadata. The hooks are now wired and ready.

$ ./vh init parse-server https://github.com/parse-community/parse-server \
      --commit abc1234 --language "JavaScript / Node.js 20"
4

Fill in THREAT-MODEL.md

Open the THREAT-MODEL.md template and complete Sections 0–4. The key sections:

§2 Collect all prior CVEs. Feed descriptions to a fresh LLM session. Paste the recurring-patterns analysis into the template.
§3.4 Write 3–6 invariants. Format: I-N | statement | enforcement point. These are what the Stop hook enforces.
§4.3 Define slices. Each row = one trust boundary. Include specific filenames, not whole directories.
The threat model must fit on one page. If it doesn't, trim it. Length creates the haystack you're trying to avoid.
5

Import the threat model into VulnHound

This reads Sections 0–4 and loads everything into state.json: target, CVE patterns, attacker model, crown jewels, invariants, bug classes, and slices.

$ ./vh import THREAT-MODEL.md
$ ./vh status # verify invariants and slices loaded
6

Start Claude Code — hooks fire automatically

Run claude from your project root. The SessionStart hook fires immediately and injects a context brief (~1,200 tokens) covering the active slice, invariants, and any prior session learning.

$ claude
You're now in map phase. Claude's first task is attack surface mapping for the active slice. The prompt phase tracker advances automatically as you work.

CLI Reference

All VulnHound commands via the ./vh shortcut.

CommandWhat it does
./vh init <name> <url>Initialise a new audit target
./vh import THREAT-MODEL.mdLoad threat model, invariants, slices into state
./vh sync THREAT-MODEL.mdWrite confirmed findings back into template §6 and §9
./vh statusShow full current state — findings, slices, memory count
./vh slice add <name> <files>Add a new slice
./vh slice nextMark current slice complete, activate next pending slice
./vh finding add <title>Register a candidate finding
./vh finding poc F-001 'curl…'Record a PoC command for a finding
./vh finding confirm F-001Mark finding confirmed (PoC reproduces)
./vh finding fp F-001Mark finding as false positive
./vh dead_end <desc>Record a tested-and-clean path (stops re-testing)
./vh memory search <query>Search the cross-session knowledge graph
./vh reportGenerate markdown findings report → hunt/REPORT.md
Interactive Tracker

Audit Progress

Check off each step as you complete it. Progress is saved in your browser.

Overall progress
0 / 0 steps