November 3, 2026
How I Built an AI-Powered Bug Bounty Agent That Actually Respects Scope
If you’ve been following the AI + hacking space, you’ve probably seen a dozen demos of “ChatGPT running sqlmap” or “Claude reading Burp…

By henriqqw
3 min read
If you've been following the AI + hacking space, you've probably seen a dozen demos of "ChatGPT running sqlmap" or "Claude reading Burp output." They look impressive for five minutes. Then you realize: the agent has no concept of scope, no way to validate evidence, and no audit trail. It's a prompt wrapper around a tool, not an agent.
I spent the last several months building something different. This is the writeup.
The Problem With Naive AI Agents in Security
When you give an LLM access to offensive tools without constraints, a few things happen:
- It hallucinates findings. A 403 becomes "potential authorization bypass." A timing difference becomes "SQL injection." It wants to complete the task, so it manufactures conclusions.
- It ignores scope. If the brief says "test
/api/v1", nothing stops it from drifting into/adminwhen it gets curious. - It can't reproduce. It found something interesting, ran the request once, and reported it. That's not a finding — that's a hunch.
These aren't model problems. They're architecture problems. The model will do what you let it do.
The Architecture: External Operator Model
The core design decision was treating the AI agent as an external operator — not as the execution layer.
Here's what that means in practice:
Claude Code / Codex
│
│ MCP (stdio)
▼
TypeScript MCP Server ←── ~140 khaos_* tools
│
│ REST
▼
Go API Backend
│
┌────┴────┐
│ │
PostgreSQL RedisClaude Code / Codex
│
│ MCP (stdio)
▼
TypeScript MCP Server ←── ~140 khaos_* tools
│
│ REST
▼
Go API Backend
│
┌────┴────┐
│ │
PostgreSQL RedisThe agent reasons about what to do. It calls tools like khaos_run_catalog_action or khaos_http_request. But the Go backend is the only authority on:
- Whether the target is in scope
- Whether the tool is installed and authorized for this assessment
- Whether execution windows allow running active tools
- Whether evidence meets the bar to advance a finding
The agent can propose any action it wants. The backend accepts or rejects it. No amount of creative prompting bypasses the execution contract.
The Evidence Gate
This was the hardest part to get right.
My first version let the agent report a finding after a single successful request. That's useless in real bug bounty — no program accepts a single unverified finding without reproduction steps.
The current gate requires:
- Three independent reproductions — same result, same parameters, different request IDs
- Captured artifacts — raw request/response, hashed for integrity
- For BOLA/IDOR specifically: two distinct auth profiles (owner ≠ non-owner) with the object ID known in advance
The finding state machine looks like this:
candidate → evidence_review → validated (3 repros + hash)
→ rejected (gate failed)candidate → evidence_review → validated (3 repros + hash)
→ rejected (gate failed)If the agent tries to advance a finding without hitting the gate, it gets a 422 with the specific reason: "artifact count insufficient" or "owner profile missing." It has to go collect the missing evidence.
Does the agent hate this? Yes. It wants to close tasks. But forcing it to gather real evidence eliminates the hallucinated finding problem entirely.
Skills as Guidance, Not Authority
One thing I learned early: don't put methodology in the system prompt. It becomes stale, it's the same for every context, and it bloats every request.
Instead, the backend serves a skill catalog — 100+ versioned methodology entries, each tagged by stage and tool. Before each task, the agent calls:
khaos_list_skills(stage="active_testing")
→ [recon-passive, sqli-detection, idor-hunt, ...]
khaos_get_skill("idor-hunt")
→ { instructions: "...", suggested_tools: ["nuclei", "ffuf"] }khaos_list_skills(stage="active_testing")
→ [recon-passive, sqli-detection, idor-hunt, ...]
khaos_get_skill("idor-hunt")
→ { instructions: "...", suggested_tools: ["nuclei", "ffuf"] }The agent reads the skill and uses it as a reference. But the skill is not an execution order. If nuclei isn't installed, if the endpoint isn't in scope, or if the stage doesn't allow active testing — the backend refuses regardless of what the skill says.
This separates "what good methodology looks like" from "what you're allowed to do right now."
Running It
The setup is a one-command install:
# Linux / macOS
curl -fsSL https://raw.githubusercontent.com/henriqqw/KhaosMCP-Pentester-Agent/main/install.sh | bash
# Windows
irm https://raw.githubusercontent.com/henriqqw/KhaosMCP-Pentester-Agent/main/install.ps1 | iex# Linux / macOS
curl -fsSL https://raw.githubusercontent.com/henriqqw/KhaosMCP-Pentester-Agent/main/install.sh | bash
# Windows
irm https://raw.githubusercontent.com/henriqqw/KhaosMCP-Pentester-Agent/main/install.ps1 | iexThis clones the repo, generates secrets, builds the MCP server, writes .mcp.json for Claude Code (or .codex/config.toml for Codex), and starts the backend via Docker Compose.
After that, restart Claude Code and the khaos_* tools appear automatically.
A Real Session Flow
Here's what an actual recon task looks like from the agent's perspective:
1. khaos_claim_codex_task(stage="recon")
→ task_id, suggested_skills: ["recon-passive", "js-analysis"]
2. khaos_pin_skill("js-analysis")
→ instructions loaded
3. khaos_run_catalog_action(
tool_id: "nuclei",
args: { templates: ["exposures/tokens"], target: "https://target.com" }
)
→ action proposed, requires decision
4. khaos_run_catalog_action(action_id: "...", decision: "approved")
→ executing, poll_interval_seconds: 15
5. khaos_assessment_events(since: ...)
→ nuclei results streaming in
6. khaos_note_set("Found AWS key in /static/app.js line 847")
7. khaos_complete_codex_task(status: "completed")1. khaos_claim_codex_task(stage="recon")
→ task_id, suggested_skills: ["recon-passive", "js-analysis"]
2. khaos_pin_skill("js-analysis")
→ instructions loaded
3. khaos_run_catalog_action(
tool_id: "nuclei",
args: { templates: ["exposures/tokens"], target: "https://target.com" }
)
→ action proposed, requires decision
4. khaos_run_catalog_action(action_id: "...", decision: "approved")
→ executing, poll_interval_seconds: 15
5. khaos_assessment_events(since: ...)
→ nuclei results streaming in
6. khaos_note_set("Found AWS key in /static/app.js line 847")
7. khaos_complete_codex_task(status: "completed")Every step is audited. Every action has a decision point. The assessment history is fully reproducible.
What I Didn't Build (and Why)
No multi-user auth. This is local-first, single-user, runs on loopback. Exposing it to a network without proper auth would be reckless — the agent can execute sqlmap. I didn't build multi-tenancy and I don't recommend running this on a public server without designing proper authentication first.
No autonomous scheduler by default. The scheduler exists but requires explicit operator authorization to enable. An unsupervised agent running active scans autonomously is not something I'm comfortable shipping as a default.
Key Takeaways
If you're building AI agents for any domain where wrong actions have real consequences (security, infra, finance), the pattern that worked for me:
- Backend as authority, agent as reasoner. The agent proposes, the backend enforces. Never let the agent be both.
- Evidence gates before state advancement. The agent can't mark a task done without meeting the evidence contract. Forces quality over speed.
- Skills as versioned guidance. Keep methodology out of the system prompt. Serve it on demand, per context, per stage.
- Audit everything. Every action, every decision, every piece of evidence. The reproduction requirement is easier to enforce when you have the full trail.
The repo is at github.com/henriqqw/KhaosMCP-Pentester-Agent if you want to dig into the implementation.