The Problem With Toolchains
Most bug bounty recon looks like a pile of terminal tabs.
One window running subfinder, another with httpx, a third with nuclei,
outputs scattered across your home directory, nothing talking to anything else.
When you come back to a target a week later, you’re piecing together what you ran
from bash history.
I wanted something different: a single workspace layout that every tool writes to, a shared contract for what data lives where, and an audit trail that shows exactly what ran and when. And I wanted it to be readable by an AI operator without custom integration code — just files.
That’s the core idea behind GeistScope.
The Engagement Directory
Every target gets an engagement directory. When you initialize one:
mg-engagement init target-bounty \
--target target.example.com \
--platform hackerone
You get a directory like this:
engagements/target-bounty/
├── engagement.json ← metadata: name, target, platform, created_at
├── scope.json ← in_scope and out_of_scope patterns
├── notes.md ← human-written notes, appended with mg-engagement note
├── audit.log ← append-only record of every tool invocation
└── recon/ ← populated by mg-recon, mg-probe, ai-prioritize
└── crawl/ ← populated by mg-crawl, one subdirectory per host
└── findings/ ← populated by mg-probe, mg-fuzz, manually
The shape is the API. Every tool in the pipeline reads and writes to this
same layout using the paths defined by the engagement library.
When you run mg-probe, it looks for recon/summary.json — which mg-recon wrote.
When ai-prioritize runs, it reads summary.json and writes priorities.md.
No configuration files that say “here’s where tool A puts its output for tool B.”
The layout is the configuration.
Scope Enforcement
One of the early design decisions was making scope enforcement a first-class feature rather than something you remember to check manually.
scope.json stores two lists: in_scope and out_of_scope. Patterns are wildcard-
matched against hostnames — *.target.example.com covers all subdomains,
and explicit denies in out_of_scope let you carve out exclusions.
Every active tool — subdomain-enum, mg-scan, mg-fingerprint, mg-crawl — calls
scope.is_in_scope(hostname) before doing anything to a target. If a discovered
subdomain falls outside the pattern, it’s dropped before any network probe goes out.
This matters in bug bounty: probing out-of-scope assets is a program violation.
You can manage scope from the CLI:
mg-engagement scope-add target-bounty "*.target.example.com"
mg-engagement scope-deny target-bounty "legacy.target.example.com"
mg-engagement check target-bounty "api.target.example.com"
# → IN SCOPE api.target.example.com
The check subcommand returns exit code 2 for out-of-scope targets, which makes it scriptable. If you’re writing a wrapper script and need to gate on scope, just check the exit code.
The Audit Log
Every tool call appends a line to audit.log:
2026-05-08T14:23:01Z subdomain-enum target.example.com count=47 mode=All
2026-05-08T14:24:18Z mg-scan api.target.example.com open=3
2026-05-08T14:25:07Z mg-fingerprint api.target.example.com
2026-05-08T14:31:44Z mg-recon target-bounty
This is an append-only file. The mg-engagement library opens it with OpenOptions::append(true)
every time, so there’s no way to overwrite a prior entry even by mistake.
The audit log serves two purposes. The practical one: when you come back to an engagement days later, you can see exactly what ran, in what order, and when. The security one: if you’re running a toolchain against a real target, the audit log is your record of what touched what. Bug bounty programs occasionally ask for evidence of what you ran.
Findings
A finding is a markdown file in findings/ with structured frontmatter:
---
title: "Missing HSTS on api.target.example.com"
severity: medium
status: draft
target: api.target.example.com
created: 2026-05-08T14:45:00Z
---
## Description
...
## Evidence
```bash
curl -I https://api.target.example.com
The ID is date-based and sequential: `2026-05-08-001`, `2026-05-08-002`.
The Evidence section uses literal `curl` commands — which `mg-replay` later
extracts and re-executes to verify the finding is still valid before submission.
`mg-probe` and `mg-fuzz` create findings automatically when they detect issues.
You can also create them manually:
```bash
mg-engagement finding target-bounty \
"Missing HSTS on api subdomain" \
api.target.example.com \
--severity medium
The CLI enforces scope here too: if the target isn’t in scope, the finding is refused. That check is worth having — it’s easy to accidentally attribute a finding to a host you weren’t authorized to test.
Why File-Native AI Collaboration
The “no custom IPC” decision was deliberate. When I run a recon pipeline and want an AI operator to help analyze results, I don’t want to build a protocol for that. The AI just reads the same files the tools wrote.
summary.json is a structured JSON document that describes every discovered host:
subdomains found, IPs resolved, open ports, fingerprinted tech stack, HTTP
accessibility status. An AI reading that file has everything it needs to suggest
what to look at first.
priorities.md is what ai-prioritize produces after sending that summary to
an AI model or a local Ollama model alongside 18 bug-hunting skill files. It comes
back with a ranked table of attack surface, highest payout × exploitability first.
No API call from the AI operator to the toolchain — it reads a file.
That design keeps the AI collaboration stateless and auditable. You can read
priorities.md yourself. You can disagree with the ranking. You can see
exactly what the model was given, because the input is summary.json sitting
right there.
What Comes Next
This post covers the workspace layer — the foundation everything else builds on. The next posts in this series cover the actual pipeline: subdomain enumeration, port scanning, fingerprinting, crawling, security posture checking, fuzzing, and the terminal dashboard that ties it together.
Each one of those tools is its own Rust binary in the engine-rust workspace.
Each one reads the engagement directory, does its job, writes structured output,
and logs to the audit trail. The workspace contract is what makes them composable
without glue code.
GeistScope is available on GitHub. All binaries install via cargo install.