skill_map.py already verified that every mapped skill is installed and every installed hunt-* skill is reachable, but it only printed the result: it never exited non-zero, and nothing in CI ran it. The guarantee it describes was therefore unenforced — a skill added without a mapping would go unreachable again exactly as it did before #81. Three changes: 1. main(argv) returns 1 on error, with ::error:: / ::warning:: annotations when $GITHUB_ACTIONS is set, matching scripts/lint_skills.py. Errors are the unreadable-skills-dir, mapped-but-absent, and unreachable-hunt-* conditions. Classes with a mapping but no tailored starter probe are reported as warnings, not errors, since they still route correctly. 2. _present() returns None when the skills directory cannot be read, instead of an empty set. _filter() previously read an empty set as "assume everything is installed" and returned every name unfiltered, so a missing or unreadable skills directory made the module name skills that are not there — the opposite of its documented contract. 3. skill-lint.yml runs the check, and triggers on engine/** so a change to the maps is validated too. Verified: healthy checkout exits 0; an unreadable CBH_SKILLS_DIR, a missing mapped skill, and an unmapped installed skill each exit 1. The existing lint_skills.py gate and the public API (skills_for/tech_skills/probe_for) are unchanged.
Engagement engine
A decision-support engine, not an autonomous robot. It maps a target's attack surface
deterministically, categorizes it against the skill arsenal (which hunt-* skill applies
where), and shows you each stage live — so you spend your expert 20% where the 80%
automation points. It does not try to test everything itself; active hunting is opt-in.
Design principles
- Deterministic-first. Breadth (recon, parameter discovery, secret scanning, service
fingerprint, categorization) is plain Python + passive tooling —
$0, no agents, no rate-limit burn. The LLM is reserved for the few judgment calls in the opt-in hunt. - Map-first, hunting opt-in. A default run stops at the arsenal map and hands off to
you. Spend agents only when you choose (
--hunt). - Show every stage. Each phase logs what it found, live, so you always know where to focus.
- Curl-first. Active testing uses
curl; Burp MCP is optional (OOB/blind/fuzzing only). - Safe by construction. Scope is a deterministic allowlist; agents run read-only by default with hard rules-of-engagement; a deterministic scope-audit flags any out-of-scope PoC.
DEFAULT (deterministic, $0, no agents) │ OPT-IN (--hunt)
recon ───▶ rank ───▶ map ──────────────────────────────▶│ hunt ─▶ validate ─▶ report
│ │ └ surface → skill arsenal + │ │ └ adversarial verifier (read-only)
│ │ curl-first probes (arsenal.md) │ └ curl-first agent per ranked (url,param,class), skills-off
│ └ class-weight priority (secrets boosted) │
└ service/tech + JS endpoints + JS secrets + ALL params (gau + katana), categorized
OSINT (subdomain/asset enum) is a separate concern — handled by Claude-OSINT, not the engine. The engine takes the in-scope seed target(s) and goes straight to deterministic recon.
| Piece | What it is |
|---|---|
scope.py |
deterministic allowlist (apex/wildcard/CIDR/regex; deny-wins; default-deny). Enforced at recon and hunt. |
recon.py |
deterministic recon — per target: service/tech (JS markers), JS-bundle endpoint mining, JS secret scanning (AWS/GCP/Anthropic/OpenAI/Slack/GitHub/Stripe/EmailJS… keys, redacted), and all input parameters from two independent sources — gau (passive historical) + katana (active live crawl). Noise-filtered, scope-filtered, multi-class categorized. No LLM in the find-step. |
skill_map.py |
arsenal categorization — maps each (endpoint|param, class) → the specific hunt-* skill(s) actually installed in ~/.claude/skills, plus a curl-first starter probe. Tech-stack skills (hunt-nextjs, hunt-nodejs…) mapped from the fingerprint. |
osint.py |
separate/optional — per-target service/tech probe (PD httpx-toolkit) reused by recon. Its subdomain-enum path is not in the engine flow (that's Claude-OSINT). |
state.py |
persistent, resumable engagement store (state.json + evidence/ + engine.log + arsenal.md + report.md). |
agent.py |
headless claude -p dispatch + JSON extraction. Skills OFF by default (eval: ~0 capability gain, saves ~12–15k tokens/agent). Used only for the opt-in hunt/validate. |
engine.py |
the orchestrator: phases, scope enforcement, ranking, the map, parallel hunt/validate, candidate→confirm, report. |
The map (the default deliverable)
map writes arsenal.md and logs every target live — for each endpoint/parameter: the attack
classes, the exact hunt-* skill(s) to open, and a ready-to-run curl. Example line:
map: redirect_to https://t/wp-login.php?redirect_to=FUZZ ssrf→hunt-ssrf lfi→hunt-lfi open-redirect→hunt-open-redirect
Run
# DEFAULT — deterministic map only ($0, no agents). Stops for you with arsenal.md:
python3 engine/engine.py --scope my-engagement.json
# scope file = {name, in_scope, out_of_scope, seeds}; output in ~/.bughunter-engagements/<name>/
# OPT-IN — auto-test the mapped surface with agents (read-only, curl-first, parallel):
cp engine/burp-mcp.json.example engine/burp-mcp.json # only if you want Burp for OOB/blind
python3 engine/engine.py --scope my-engagement.json --hunt --parallel 3 --max-hunts 12
python3 engine/engine.py --scope my-engagement.json --hunt --allow-intrusive # permit state-changing PoCs (off by default)
# dry-run the whole wiring with canned output (no agents, no budget):
python3 engine/engine.py --scope engine/engagement.example.json --base /tmp/eng --mock --hunt
# standalone recon (deterministic, no engine state):
python3 engine/recon.py https://target/ target.com
Key flags: --hunt (opt into agents) · --allow-intrusive (default OFF = read-only) ·
--parallel N (concurrent agents, default 3) · --phases a,b,c (explicit override).
Safety
- Read-only by default — hunt/validate agents are told: in-scope hosts only (never
third-party, even to prove a finding), and no state-changing actions (no emails/writes/
deletes/cache-purge, don't exercise exposed creds).
--allow-intrusivelifts this. - Deterministic scope-audit — every confirmed finding is checked for out-of-scope hosts in its PoC; any hit is flagged (⚠) in the report for review.
- Calibrated severity — the adversarial validator's severity (not the hunt's raw guess) is what's stored.
Why this and not more skills
The measured result (eval/): skills add ~0 capability on benchmarkable tasks — the base model
already exploits standard classes, and skills cost ~12–15k tokens/agent to load. The leverage is
here: turning that raw capability into a safe, deterministic, skill-routing, operator-in-the-
loop engine. Breadth is free and visible; you bring the judgment.