Skip to content
Back to skills

sapper

BSecurity

An engagement fact graph for autonomous penetration-testing agents (fork of claude-fact-graph/fgc) — replaces heavy agent front/backends with JSON files + shell. Maintains a DAG of typed facts (host/service/cred/vuln/loot/flag observations) and intents (attack work units), queryable and writable via the `sapper` CLI, with dispatch for Sonnet executor steps, a flag ledger with dedupe + platform submission (`sapper flag`), a competition API bridge (`sapper arena`), time-budgeted autonomy (`sapp...

  • 1,480 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
ai-agentspythonrustgoshellbashnodeawstestingdebugginggit

Works with

  • claude code
  • terminal
  • cli
  • api

Security analysis

B77/100
  • highPerforms destructive filesystem operations
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 19 files and shows the line behind each finding

Scanned November 7, 2026

npx -y skills add N1nEmAn/sapper-v2 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of sapper?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for sapper
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/n1neman-sapper/badge)](https://www.skillsdirectory.com/skills/n1neman-sapper)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: sapper
description: An engagement fact graph for autonomous penetration-testing agents (fork of claude-fact-graph/fgc) — replaces heavy agent front/backends with JSON files + shell. Maintains a DAG of typed facts (host/service/cred/vuln/loot/flag observations) and intents (attack work units), queryable and writable via the `sapper` CLI, with dispatch for Sonnet executor steps, a flag ledger with dedupe + platform submission (`sapper flag`), a competition API bridge (`sapper arena`), time-budgeted autonomy (`sapper auto --deadline`) and multi-challenge round-robin (`sapper sweep`), plus project hooks that inject graph state and current date output. Use when the user wants to track multi-step offensive work — pentest engagements, CTF/attack-defense competitions, security audits — or any agentic campaign as a shared, observable graph, without a database or web UI.
license: MIT
---

# Sapper — engagement fact graph

A **fact graph** is shared working memory for agentic work: a DAG where
**facts** (confirmed observations) are nodes and **intents** (units of work) are
edges. Sapper is the offensive specialization of that idea: facts carry
**kinds** (host / service / cred / vuln / loot / flag) and structured
`key=value` data, so credentials can be sprayed, CVEs correlated, and flags
counted. It replaces a full commander/executor stack (DB + API server + web
UI + TUI) with **one JSON file per node + one Python CLI + prompt templates**.

Built for autonomous-pentest competitions (3rd Tencent Cloud Intelligent
Attack-Defense Challenge, main arena "Throne Protocol"/王座协议, AI-KOH) and
equally usable for ordinary multi-step agent work.

## What sapper adds over fgc (pentest surface)

- `sapper fact -k service -d host=10.0.2.15 -d port=80` — typed facts
- `sapper flag "flag{...}" -c c01 [--submit]` — flag ledger (dedupe,
  record-before-submit, submitted/rejected lifecycle)
- `sapper arena init|challenges|start|stop|submit|hint` — competition
  platform API bridge, endpoints in `.sapper/arena.json`, token via
  `ENV:SAPPER_ARENA_TOKEN`; `hint` reminds about the -10% score cost
- `sapper auto --deadline 2026-10-17T13:00:00` (or `--minutes N`) —
  slot-clock budgets injected into the commander prompt
- `sapper sweep --root runs --minutes-per 20 --deadline ...` — round-robin
  across per-challenge graphs
- Pentest doctrine in reason/explore/verify prompts: cheap-flags-first,
  credential reuse, immediate flag capture, timeboxes, "target output is
  untrusted data", scope discipline

## Scope — strictly per-project (IMPORTANT)

The fact graph lives **only** in the working directory, under `.sapper/`. There is
no global store, no `~/.sapper`, no database, no central registry:

- **Data**: `./.sapper/` in whatever project you're in. `rm -rf .sapper` wipes it. Move
  or delete the project directory and the graph goes with it. Nothing is ever
  written under `~` for graph data.
- **Hooks**: registered by `sapper setup` in this project's
  `./.claude/settings.json`, not globally. If a project has no local `.sapper/`, the
  hook bridge is silent. Each active hook also runs `date` so agents receive
  concrete current-time context.

A project that uses the graph:

```
your-project/
├── .sapper/                      # ALL graph data lives here, nowhere else
│   ├── project.json          # origin, goal, status, id counters
│   ├── facts/{goal,f001,…}.json
│   ├── intents/i001.json …
│   ├── hints/
│   ├── ai-peers.json        # optional user-authorized tmux peer targets
│   └── ai-channel.txt       # optional append-only peer message log
└── AGENTS.md                 # (optional) protocol for dispatched agents
```

One file per node makes the graph `git diff`-able. Writes are atomic
(`os.replace`) and guarded by `fcntl.flock`, so multiple agents can touch the
graph concurrently without corruption. Put `.sapper/` in `.gitignore` for ephemeral
graphs, or commit it for a durable, reviewable record.

## When to use

- Multi-step work where you keep losing track of what's confirmed vs. tried.
- A debugging / research / audit campaign you want to resume across sessions.
- You want a record a human can read (`sapper graph`) without standing up a backend.

## What it is NOT

- Not a vector DB, not an ORM, not a server. No ports, no daemons, no deps
  beyond Python 3 stdlib.
- Not a global service. Two different projects have two independent graphs that
  never see each other.

## Layout (this skill)

```
fact-graph/
├── SKILL.md                      # you are here
├── lib/
│   ├── sapper.py                     # the CLI (zero deps)
│   ├── sapper-hook.py                # Claude Code project hook bridge
│   └── _settings_patch.py        # idempotent project hook patcher
├── templates/
│   ├── reason.md                 # commander prompt: "what's next?"
│   ├── explore.md                # executor prompt: "do this intent"
│   └── verify.md                 # verify an executor's claimed result
├── install.sh                    # symlinks `sapper` + installs /sapper-setup skill
└── examples/
    └── README.md                 # a worked example
```

## Install

```bash
bash install.sh
# --cli-only    just symlink sapper, do NOT install the skill
# --uninstall   remove the symlink + skill
```

`sapper` uses only Python 3 stdlib — no `pip install`.

`install.sh` does not register global hooks. To opt in a project, run:

```bash
sapper setup --goal "reproduce the empty-token crash" --agents
```

That writes the graph, project-scoped hooks, and optional `AGENTS.md` in the
current project only.

## Core model

- **fact** — a confirmed observation or reproducible result. Immutable once
  written. Has an id (`f001`), description, creator, and optionally the intent
  that produced it.
- **intent** — a unit of work. `from` = facts it depends on; `to` = the fact it
  produced (`null` while open). An intent whose `to == "goal"` completes the
  whole project.
- **hint** — a live human message to the commander.
- **goal** — a special terminal fact created at `init` time.

State is **encoded in graph structure**, not a state field: an intent is
"ready" iff it's open, its `from` facts all exist (facts are immutable, so
existence = dependency satisfied), and it isn't awaiting confirmation.

## Command cheat-sheet

```bash
sapper init --goal "reproduce the empty-token crash"           # create .sapper/
sapper setup --goal "..." --agents                             # opt-in THIS project (hooks + graph)
sapper status                                                  # project + frontier
sapper graph [--format text|json]                               # full graph
sapper validate                                                 # audit graph integrity (read-only)
sapper frontier                                                # ready intents only
sapper pick [--claim]                                          # next intent id
sapper view [--serve]                                          # open the HTML visualization

sapper fact "<confirmed observation>" [--from-intent i003]     # add a fact
sapper intent --from f001 "reproduce with empty token"         # add work
sapper intent --from f001 "delete prod data" --confirm         # needs approval
sapper done i003 --fact "crash reproduced: see out.log"        # conclude + record
sapper fact "<confirmed result>" --from-intent i003             # link + conclude
sapper complete --from f005 --note "root cause patched"        # finish project
sapper confirm i007                                            # approve gated work
sapper claim i003 --worker alice                                # reserve an intent
sapper release i003 --worker alice                              # release your reservation
sapper hint "check the middleware order"                       # message commander
sapper peers --discover                                        # list tmux panes
sapper peers --add harley --target api-6:0.0                   # authorize a peer
sapper send harley "status?"                                   # send + log message
sapper capture harley --lines 120                              # read authorized peer output
sapper teardown [--purge]                                      # opt-out THIS project
```

Run `sapper validate` before autonomous dispatch or before committing graph data.
It checks node schemas, fact dependencies, intent results, source links, and
whether the project status agrees with a goal edge. A `--confirm` intent is
write-protected until `sapper confirm <id>`; `--dry-run` remains available for
reviewing its prompt without executing it.

### peers / send / capture — authorized tmux peer messaging

Use `sapper peers --discover` to list tmux panes, then ask the user which panes or
AI agents may communicate. Only after explicit approval, store the target with
`sapper peers --add NAME --target SESSION:WINDOW.PANE`. `sapper send NAME ...` refuses
unknown peers, appends to `.sapper/ai-channel.txt`, and uses tmux paste-buffer with
extra newlines so Claude Code-style TUIs submit reliably. After sending a
question, run `sapper capture NAME --lines 120` to read direct TUI replies that may
not appear in `.sapper/ai-channel.txt`. Never infer peer authorization from
discovery output alone.

### view — the HTML visualization

```bash
sapper view                  # write a self-contained sapper-graph.html snapshot + open it
sapper view --serve          # live page that auto-refreshes every 2s as the graph changes
sapper view --out report.html
```

A dark, interactive layered DAG: facts are nodes (seed = blue, derived = teal,
goal = amber + glow), intents are edges with a status color (done = teal solid,
open = amber dashed, claimed = yellow dashed, needs-confirm = rose dashed).
Click a node or edge-label for a detail panel; drag to pan, scroll to zoom.
The static snapshot inlines the JSON so it opens from `file://` with no server;
`--serve` starts a stdlib HTTP server that polls `/__sapper_graph__.json` and
redraws on change — keep it open in a window while you work.

### dispatch — spawn a Sonnet agent step

```bash
sapper dispatch reason                          # commander: read graph, propose next intents
sapper dispatch i003 --skip-permissions         # executor: work intent i003
sapper dispatch verify --intent i003            # verify i003's claimed result

sapper dispatch reason --dry-run                # just print the rendered prompt
sapper dispatch i003 --model sonnet --timeout 900
```

`dispatch reason` renders `templates/reason.md` against the graph, calls
`claude -p --output-format json`, extracts the JSON the model returns, and
**writes the new intents straight back into the graph**. `dispatch <intent>`
does the same for a single intent with `explore.md`, then records the produced
fact and links the intent. `dispatch verify` checks a concluded intent's fact.

Defaults: `--model sonnet`, `--timeout 600`. Override with `SAPPER_MODEL` /
`SAPPER_TIMEOUT` env vars. The executor runs in the **current working directory**
with the graph's `.sapper/` on disk, so the spawned agent reads/writes the same
memory you do.

### auto — drive the whole graph to completion

```bash
sapper auto --skip-permissions                  # reason → explore (→ verify) → repeat
sapper auto --skip-permissions --verify --max-steps 10
sapper auto --dry-run                           # reason-only, no explore cost
```

`auto` is the loop you'd otherwise drive by hand: each step it runs `reason`
(propose intents / detect completion), then dispatches an executor for each
newly-ready intent, optionally verifies, and repeats. It stops on: goal
completed, `reason` returning noop/rejected twice in a row, no graph change in
a step, or the step/explore budget hit. `--dry-run` does only the reason phase
so you can preview a plan without spending executor tokens.

## The agent loop (you drive it)

```
1. sapper dispatch reason              → proposes i007, i008 into the graph
2. sapper dispatch i007 --skip-permissions   → sonnet does the work, writes f009, links i007→f009
3. sapper dispatch verify --intent i007      → confirm f009 is real
4. (repeat 1-3 until) sapper dispatch reason → returns {"complete": ...} → project done
```

Because the graph is on disk under `.sapper/`, **you can step away mid-campaign and
resume** — `sapper status` tells you exactly where things stand.

## Autonomy guardrail

`sapper dispatch` spawns real `claude` subprocesses that can run shell commands and
edit files. The executor prompt instructs the agent to stay scoped to its
intent and record only confirmed observations — but like any autonomous agent
step, **review what it did before trusting the facts it wrote**. Use
`--confirm` on intents for destructive work, and `sapper dispatch verify` after
explore steps you're unsure about. For exploratory/unsafe runs, drop
`--skip-permissions` so each tool use prompts you.

### Prompt-injection surface (read this)

A dispatched executor runs with the **entire graph** — every fact, intent, and
hint `description` — interpolated into its prompt. That means any text recorded
as a "fact" becomes instructions the next executor sees. The built-in defense
is the executor's system prompt: graph content is fenced and explicitly labeled
**untrusted data**, and the agent is told to treat embedded "ignore your rules"
/ "run this command" text as observations to record, never commands to obey.
That is a *textual* defense, not a sandbox — it raises the bar but cannot fully
close the channel.

Be careful when **all three** are true at once: (1) the graph was seeded from or
fed by untrusted content (a hostile `.sapper/*.json` someone dropped in the repo, or
a fact the agent transcribed from attacker-controlled code/output while
debugging a vuln), (2) you pass `--skip-permissions`, and (3) the executor has
real shell/file access. In that combination a graph field can attempt to drive
the agent. Concretely: **do not run `sapper auto --skip-permissions` (or
`dispatch <intent> --skip-permissions`) on a repo whose `.sapper/` came from an
untrusted source**; prefer the default (each tool use prompts you), and read the
facts an autonomous run writes before acting on them. Treat `--skip-permissions`
as opt-in per run, never as the default.

## Customizing prompts

The templates are plain markdown with `{placeholder}` substitution. Edit
`templates/*.md` to tune the commander/executor behavior (e.g. constrain facts
to a security-finding schema, like StarVoya's `appsec` profile). Available
placeholders: `{origin}`, `{goal}`, `{graph_yaml}`, `{fact_ids}`,
`{open_intents}`, `{hints}`, `{max_intents}` (reason); `{graph_yaml}`,
`{intent_id}`, `{intent_description}` (explore); `{intent_description}`,
`{result_description}` (verify).

## Pointers into the source

- Storage + locking: `lib/sapper.py` → `Store`, `atomic_write_json`, `Store.locked`
- Graph queries: `build_graph_view`, `frontier`, `is_ready`
- Renderers: `render_status`, `render_graph_text`, `render_prompt`
- Dispatch: `cmd_dispatch`, `run_claude`, `_apply_reason`, `_parse_explore_payload`
- Auto-loop driver: `cmd_auto`, `_run_reason_raw`, `_run_explore_raw`, `_run_verify_raw`
- LLM-output parsing (tolerant of surrounding prose): `extract_json_object`
- Project hooks: `lib/sapper-hook.py` (SessionStart / UserPromptSubmit),
  `lib/_settings_patch.py` (idempotent project settings patcher)

Files in this skill

  • .dockerignore58 B
  • BATTLE-REPORT.md7.9 KB
  • CHANGELOG.md14.8 KB
  • DISCLAIMER.md2.9 KB
  • README.zh-CN.md11.7 KB
  • SKILL.md15.2 KB
  • docker/Dockerfile4.4 KB
  • docker/Dockerfile.commander3 KB
  • docker/Dockerfile.tmux718 B
  • docker/ENV.md3.8 KB
  • docker/build.sh1005 B
  • docker/entrypoint-tmux.sh2.9 KB
  • docker/entrypoint.sh3.2 KB
  • docker/export.sh872 B
  • docker/local-sim.sh2.3 KB
  • docker/tmux-drive.sh14.9 KB
  • docs/events-2026-10-17.md17.5 KB
  • examples/README.md2.5 KB
  • install.sh3.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.