Autonomous CTF batch solver. JSON in, flags out.
hydra challenges.json
# ✓ baby-rsa → flag{w1ener_w1ns} (47s)
# ✓ login → flag{sqli_r0ck5} (2m13s)
# ✗ pwn2 → (timeout after 60m)
#
# solved 42/50 in 48m21s → ./flags.json
(flags above are illustrative)
Built for CTF players who want to clear a category overnight, researchers benchmarking agent capability across pwn/crypto/web/rev/forensics/misc, and tool builders studying how to keep long-running LLM agents on-track.
Each challenge runs in its own Docker container with Claude Code inside.
A triage agent (CLAUDE.md) classifies the category and dispatches
to one of 7 specialist subagents (.claude/agents/),
which pull from ~30 attack-pattern playbooks
(.claude/skills/ — RSA / ECC / padding-oracle / LFI-to-RCE /
prototype-pollution / volatility / anti-debug / …). Hydra harvests and
aggregates across the whole batch — with live log streaming, automatic resume,
and pass@k parallel attempts.
Three supervision layers keep the agent honest:
- Operator babysit — a separate Claude session running the
prompts/hydra-babysit.mdplaybook monitors the batch every 270s and acts on a decision matrix (CONTINUE / KILL / UPGRADE / PAUSE). - Deterministic watchdog — a sidecar per worker that catches loop / OOM / cost-cap / idle failures without an LLM call.
- Pre-commit flag gate —
hydra/flag_gate.pyvetoes malformed or provenance-light candidates before they reachflags.json.
┌── L1: operator babysit (separate Claude session)
│ ScheduleWakeup 270s → jq logs → decision matrix
│ codified in prompts/hydra-babysit.md
│
challenges.json │
│ ▼
▼ [hydra batch — running, monitored]
┌─────────────┐
│ orchestrator│ ── spawns N workers (--parallel)
└──────┬──────┘
│
▼
┌──────────────────────────┐
│ Docker container │ ◀── L2: watchdog sidecar
│ │ (loop / OOM / cost / idle — 0 token)
│ Claude Code (triage) │
│ └─ specialist agent │
│ └─ flag.txt │
└──────────┬───────────────┘
│
▼
flag_extractor ──▶ L3: flag_gate ──▶ flags.json + results.json
(deterministic, 0 token)
See Supervision for the matrix + signals.
- Python 3.12+
- Docker CE (or Podman — set
HYDRA_CONTAINER_ENGINE=podman) - Claude Code auth: a logged-in
~/.claude/(preferred) orANTHROPIC_API_KEY
git clone https://github.com/iamkorun/hydra.git
cd hydra
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
docker build -t hydra-worker . # 10–20 min on first buildTry the toy challenges first — they exercise the full pipeline (orchestrator
- watchdog + flag gate) in ~45 seconds and don't need a real CTF target:
hydra examples/challenges-toy.jsonSee examples/quickstart.md for a 5-minute walkthrough.
For real CTF batches:
# 1. Create challenges.json (see Input format below).
# 2. Solve:
hydra challenges.json
# Output lands in:
# ./flags.json — flags only, ready for CTF-platform upload
# ./results.json — full results with summary stats
# ./runs/<name>/ — per-challenge artifacts (input, scratch, logs)
# ./failures/<name>.md — postmortem for each unsolved challengeHydra auto-detects auth. Subscription is preferred.
| Method | Setup |
|---|---|
| Claude Max / Pro subscription | Run claude once on the host to log in. Hydra bind-mounts ~/.claude into each container as read-only. |
| Anthropic API key | export ANTHROPIC_API_KEY=sk-ant-.... Used as fallback, or forced via --use-api-key. |
Override the auto-detected credential dir with --credentials-dir /path/to/.claude.
Top-level is a JSON array. Hydra sniffs field names — each entry needs:
- A name field (
name,title, orid) — auto-generated from a content hash if absent - At least one of description / task / prompt or files / attachments / paths
Optional: category, points, hints, remote.
[
{"name": "baby-rsa", "description": "Decrypt this.", "files": ["/path/chal.py"]},
{"name": "login", "description": "http://ctf.example.com:8080"},
{"name": "pwn1", "category": "pwn", "points": 200, "description": "ret2libc",
"files": ["/tmp/pwn1"], "remote": "nc chal.example.com 1337"}
]| Flag | Default | Description |
|---|---|---|
--parallel N |
challenge count | Concurrent workers — defaults to the number of challenges in the input JSON |
--timeout S |
3600 |
Per-challenge wall-clock (seconds) |
--model NAME |
claude-opus-4-7 |
Claude model |
--attempts K |
1 |
pass@k — K parallel attempts per challenge, first flag wins |
--retry-failed |
off | Re-run entries marked failed / timeout / error |
--only A,B,C |
— | Comma-separated names to run (skip others) |
--runs-dir PATH |
./<json-stem>/runs |
Per-challenge artifact dir |
--results PATH |
./<json-stem>/results.json |
Aggregate output |
--jsonl PATH |
./<json-stem>/results.jsonl |
Streamed one-line-per-challenge |
--flags-out PATH |
./<json-stem>/flags.json |
Flags-only output |
--credentials-dir PATH |
~/.claude |
Host dir mounted at /root/.claude:ro |
--use-api-key |
off | Force API-key mode even if subscription is found |
--dry-run |
off | Normalize + prepare workdirs, don't run |
--rebuild-image |
off | docker build before running |
Run hydra --help for the canonical list.
All outputs default to ./<json-stem>/ (e.g. hydra phase-1.json writes
into ./phase-1/). Stdin (-) falls back to cwd. Any explicit
--runs-dir / --results / --jsonl / --flags-out overrides win.
| File | Contents |
|---|---|
<json-stem>/flags.json |
{"name": "flag", ..., "__failed__": [names]} — ready for platform upload |
<json-stem>/results.json |
Final aggregate with per-challenge status and summary stats |
<json-stem>/results.jsonl |
One line per finished challenge, appended live |
<json-stem>/runs/<name>/ |
Input files, agent scratch, full transcript logs |
<json-stem>/runs/<name>/logs/claude.stdout.jsonl |
Full agent transcript, streamed live (tail -f works) |
<json-stem>/failures/<name>.md |
Postmortem + last 50 log lines per unsolved challenge |
<json-stem>/failures/SUMMARY.md |
Index of all failures with a reason column |
Re-running with the same output files is idempotent:
- Entries with
status == "solved"are skipped automatically. - Failures (
failed/timeout/error) are also skipped by default so a long batch doesn't re-burn budget on hopeless challenges.
Retry failures explicitly:
hydra challenges.json --retry-failedDelete results.jsonl to start fresh.
Run K attempts per challenge — first flag wins, siblings are cancelled:
hydra challenges.json --attempts 3 --parallel 8Each attempt consumes a --parallel slot, so K=3 reduces effective
cross-challenge concurrency by 3×. In exchange, solve rate climbs on
flaky challenges — Palisade (arxiv 2412.02776) reports 83% → 95% on
InterCode-CTF moving from k=1 to k=10.
cat failures/SUMMARY.md # index of all failures
cat failures/<name>.md # postmortem + log tail
less runs/<name>/logs/claude.stdout.jsonl # full agent transcript
ls runs/<name>/work/ # solver scratch filesLogs stream to disk as the agent runs, so
tail -f runs/<name>/logs/claude.stdout.jsonl works live during a batch.
Hydra enforces "derive, don't recall" at the prompt layer — a CTF solve must be produced by running code against the target, not imported from training memory:
.claude/skills/meta/exploit-debug.md— when a payload doesn't fire, the specialist runs a 6-step diagnostic ladder (reachable → payload arrives → endpoint live → response diff → oracle sanity → public-PoC diff) before iterating. Cuts the "write v1 → no response → write v2 that's basically v1" context burn we saw on time-based SQLi challenges..claude/skills/meta/no-prior-knowledge.md— if a specialist falls back to training memory ("I know this room's creds aremitch:secret") it must audit-log to./work/prior-knowledge.logwith derivation-attempt + risk. Theverifier-specialistauto-SUSPECTs any candidate whose run emitted that log, so the triage agent re-dispatches with "derive the skipped step". Skipping the log = fabrication; verifier catches it on provenance.
Why it matters: without this, a specialist can score on a canonical challenge by recalling the answer instead of exploiting it, and collapse on any variant. The log turns silent shortcuts into an auditable signal.
Hydra's failure modes split cleanly in two: mechanical (looping, OOM, runaway cost, malformed output) and semantic (wrong direction, hallucinated facts, scope drift, partial flags). The supervision stack is designed around that split.
- Mechanical failures are caught by deterministic code — the watchdog sidecar and the flag gate, both 0-token, both shipped in this repo as Python.
- Semantic failures are caught by a separate Claude session
running a codified operator playbook. The playbook lives in
prompts/hydra-babysit.mdas a versioned prompt — not as Python code — so it stays forkable and domain-portable. A second playbook,prompts/bb-babysit.md, applies the same pattern to bug-bounty workflows.
A separate Claude Code session, primed with prompts/hydra-babysit.md,
runs alongside the batch. The playbook gives it:
- Pre-flight gates — verify the target's protocol handshake (not just port-open) before launching a worker. Defends against IP recycling on platforms that reuse boxes.
- Cheap-check loop —
ScheduleWakeupevery 180–270s (prompt-cache warm), thenjq/tail/staton the worker's jsonl to grade progress without reading the full transcript. - Decision matrix — first-matching-row dispatch over CONTINUE / KILL / UPGRADE (sonnet → opus) / PAUSE on signals like partial flag, target down, training-memory shortcut, cost over cap, solver spam.
- Scrub-before-commit — if a partial / wrong flag was already
recorded, the supervisor scrubs
flags.json+ downgradesresults.jsonlso--retry-failedcan re-pick the challenge.
To use it: paste the playbook into a Claude Code session, tell it the
challenges JSON + target IPs, and let it run. The worker is hydra
itself; the supervisor never invokes the model directly.
Runs alongside each worker container and tails
runs/<name>/logs/claude.stdout.jsonl for bad-behavior signals. Kills
the container before it blows its budget. Signals:
| Code | Trigger | CLI flag |
|---|---|---|
bash_repeat |
Same Bash command prefix fires N+ times | --watchdog-max-bash-repeats (default 3) |
solver_spam |
>N files written matching work/{solve,probe,exploit}NNN.py |
--watchdog-max-solver-variants (default 5) |
cost_cap |
Estimated token cost exceeds cap | --watchdog-cost-cap (default $10) |
oom_preempt |
Container RSS ≥ X% of memory limit | --watchdog-mem-kill-pct (default 90%) |
idle_work |
work/ unchanged N sec while agent still tool-using |
--watchdog-idle-work-timeout (default 180s) |
Disable with --no-watchdog (for debugging the agent itself).
Killed runs land in results.jsonl as status: failed with
reason: watchdog: <code> (<detail>) — grep-friendly, and
--retry-failed re-picks them.
Every flag candidate runs through hydra/flag_gate.py before being
written to flags.json:
- REJECT: unclosed brace, wrong prefix, format mismatch, length bounds,
control chars, whitespace.
flags.jsonstays clean; status =failed. - WARN: missing scratch artifacts, or
prior-knowledge.logpresent. Status =solved_uncertain; flag is recorded but flagged for human verification. - ACCEPT: normal.
Tighten the gate per-challenge in your JSON:
{"name": "splash", "flag_prefix": "EXAMPLE",
"expected_format": "EXAMPLE\\{[0-9a-f]{32}\\}"}
To be straight about scope:
- Not a CTF curriculum or learning tool. It solves; it doesn't
teach. If you want to learn pwn, read the writeups in
notes/lessons-learned.mdor the skill playbooks under.claude/skills/— but Hydra itself optimizes for flag-out, not human-understands. - Not SOTA on the hardest challenges. pass@k helps on flaky ones, but the worker LLM is still the ceiling. Hard pwn / custom crypto / multi-stage chains still benefit from a skilled human.
- Not cheap. Per-challenge cost on
claude-opus-4-7typically lands in the $0.50 – $4 range depending on category and timeout. The watchdogcost_capdefaults to $10/challenge — tune it. - Not zero-config. You assemble
challenges.jsonyourself. Hydra sniffs field names liberally, but it doesn't scrape CTF platforms for you. - Not affiliated with Anthropic. Hydra uses Claude Code as the worker runtime, but is a third-party project.
- Not a fleet hunter for bug bounty. The
prompts/bb-babysit.mdplaybook is deliberately single-worker, supervised, scope-gated — because unsupervised fleets get programs banned.
.venv/bin/pytest
.venv/bin/python -m ruff check # lint (E + F + B + UP rulesets)See ROADMAP.md. Headline items for v0.2: benchmark
on InterCode-CTF, hydra supervise subcommand, per-category cost
telemetry, and a "Hydra on Haiku" cheaper-supervisor variant.
MIT — see LICENSE. Read CONTRIBUTING.md
before opening a PR; it lists what merges easily, what requires
discussion, and what's out of scope. Issues and discussions live at:
- 🐛 Issues — bugs, features
- 💬 Discussions — design questions, war stories, playbook forks
If you fork the supervision playbook for a different domain
(red-team engagement, kaggle-style ML competition, scraping pipeline,
etc.), please open a playbook-fork issue
— the prompts/ directory is meant to collect these.