Concepts
Three ideas carry the whole tool: SpanGate, signed receipts, and the two enforcement modes. Read these and the rest of the docs follow.
SpanGate
SpanGate is the one mechanism, and Herkos is infrastructure that runs it, not an AI agent of its own. For each query, Herkos's local code graph emits a minimal set of (file, line-range) spans. That same set does two jobs at once:
- It is the egress allowlist the shipping broker enforces. Anything outside the served span set is not allowed out, deny-by-default.
- It is the minimal context the select pipeline would hand a model: the spans needed to answer, not the whole tree. That is a design property of how the set is chosen, not a token cut the broker performs at runtime.
The reason one set can do both is the claim at the center of Herkos: the minimal context an agent needs is exactly the only thing it should be allowed to send. Derive the context and the egress allowlist from one set and they cannot drift apart; split them across two tools and the gap between them is where leaks live. One set, no gap, with a signed receipt over what was served.
Receipts
Herkos produces two kinds of signed proof, for two different questions. Both are signed with your local ed25519 key and verifiable on your machine, so no server has to vouch for either.
The span receipt: which spans were served
A SpanGate run ships a signed Merkle receipt of the served span set. The receipt commits exactly which (file, line-range) spans the pipeline selected for a query, so you can answer after the fact, with proof, which context was in scope. It is a record you hold, not a log you have to trust a vendor to keep.
SpanGate demo (files [auth.go db.go]) served 40 / 500 lines (the minimal span set for the query) bytes blocked from egress: 256 (enforcement=userspace) receipt: VERIFIED root=02659078080f474c9fac20c7a83aa070f95cda014e0b0ea0994d7dde477d3008
The Merkle root is deterministic over the canonicalized span set, so two runs that served the same spans produce the same root. herkos receipt --file <r.json> summarizes a saved receipt; to cryptographically re-check its signature and root, use herkos verify --file <r.json> --pubkey <key.pub>.
The serve audit log: what the broker did
The span receipt proves what went in. The audit log proves what the broker let out. Run herkos serve --receipts <dir> and Herkos writes a per-session log with one record for every brokered tools/call: the tool name, a hash of the request, and whether it was allowed or denied. Each record is ed25519-signed and sha256 hash-chained to the one before it, so any edit, reorder, or dropped record breaks the chain. A reader verifies the whole log offline with only the public key.
When you serve with a pinned span set, the log's opening record also commits a fingerprint of that served context. The signed chain then proves which context-egress binding was in force for the calls it records, not just that some calls happened. Verify a log the same way as a receipt: herkos verify --file <log> --pubkey <hex>.
Enforcement modes
At runtime the broker sits between the agent and the upstream MCP server: it gates each tools/call deny-by-default, forwards allowed calls untouched, and records every brokered call to the signed log. It also trims the tools/list the agent loads to the allowlist, so the agent never loads the schema of a tool it cannot call - fewer tool-catalog tokens at session start, and a smaller surface. How strictly egress is enforced beyond that comes in layers. Be clear about which you are running.
| Mode | Status | What it enforces |
|---|---|---|
userspace |
Shipped | Advisory and auditing. Deny-by-default tool-name control on outbound tools/call, plus a signed audit trail. Not a kernel-enforced seal. |
serve --isolate |
Shipped (Linux) | Kernel network namespace with no route out: the brokered server gets only loopback and cannot open its own socket to any host. Unprivileged. For servers that only need stdio to Herkos; not a per-destination allowlist. |
hardened |
Roadmap | OS-enforced, per-destination byte-level egress boundary using Linux Landlock and seccomp, then eBPF host allowlisting. The airtight mode for servers that do need their own network; not shipped yet. |
userspace mode is not airtight. It gives you deny-by-default tool control and a signed audit trail, not a kernel-enforced seal on outbound bytes. serve --isolate adds a kernel boundary for a server that needs no network of its own. The per-destination seal for servers that do (and the byte-level provenance boundary that ties egress to exactly the served span set) is hardened-mode work, on the roadmap.
The local code graph
The span set comes from a code graph that Herkos builds locally with a tree-sitter parser (Go, TypeScript, and Python today). The graph never leaves your machine, and neither does the signing key. Local-first is not a slogan here; it is what makes the receipt trustworthy, because the thing producing it is under your control.
Next
The security model spells out the threat model and publishes the bypass. The CLI reference documents every command.