Your coding agent, on a leash you own.
An in-path broker for any MCP client (Claude Code, Cursor, Cline, Codex). Every tool call clears a deny-by-default allowlist; a tool you never approved is blocked in-path, and your session keeps running.
$ go install github.com/akhilesharora/herkos/cmd/herkos@latest
The CLI binary and Go module are still named herkos (pre-rebrand); that is intentional.
Scan a server, broker its calls, prove the log.
A real terminal and the real binary: scan flags an unpinned, unbrokered MCP server; a brokered call lands in a signed, hash-chained log that verifies offline; tamper with it and verify catches it.
Because the gate is deny-by-default, the agent only ever sees the tools you allowed: it never loads the schema of a tool it cannot call, so a server's full tool catalog stops costing you tokens it will never use.
Three real 2025 MCP incidents, each scored honestly.
For every one: what happened, and what Herkos actually catches versus where it honestly does not. No hand-waving.
GitHub toxic flow
Injection rides an approved tool to leak private repos. The broker can't stop it; the signed audit log is the forensic record. Invariant Labs.
postmark-mcp backdoor
A malicious server BCC'd every email, server-side. The boundary case; but scan flags the unpinned, unbrokered setup. Koi Security.
MCPoison rug-pull
Approve-once, trust-forever swap. Herkos does not catch the command swap; scan --baseline catches the description-poisoning cousin. CVE-2025-54136.
Your agent talks to servers you didn't write.
Your coding agent connects to MCP servers - GitHub, Slack, a database, the filesystem - and any of them can read your code through a tool call and ship it to a backend you do not control. You get no deny-by-default say over which tools run, and no verifiable record of what left. Herkos is one in-path broker: it gates every tool call deny-by-default and signs an offline-verifiable log of what it brokered.
You pasted in a server you didn't read.
Without Herkos
From the moment it is in your config, the server can call any tool it exposes - delete_file, send_email, tools a silent update adds tomorrow. The agent runs hundreds of calls with no human in the loop and no gate on which tools are even on the table. You find out what it called when something is already gone.
With Herkos
You name the tools the server may call. Anything else gets a hard JSON-RPC error in-path before it reaches the server, and your session keeps going. A backdoored update that adds a send_email tool you never approved cannot call it - the allowlist is deny-by-default.
What Herkos does not do: if you approved send_email and an injected instruction makes the agent BCC an attacker through it, Herkos forwards it - the abuse rides a tool you allowed. That is the postmark-mcp attack, and a name allowlist cannot catch it. We tell you that here because the other tools will not.
SpanGate: the context is the allowlist.
For each query, Herkos's local code graph emits a minimal set of (file, line-range) spans. That one set is both the model's context and the egress allowlist - the minimal context an agent needs is exactly what it should be allowed to send.
A minimal context set
The spans that bound egress are also the minimal context a query needs. herkos select emits them; feed the model that set instead of the whole tree and you ship fewer tokens, scoped to exactly what may leave.
Deny-by-default egress
Pin what your agent sees and that is all that can leave: repo lines outside the served set are blocked on the way to a tool call, after normalizing case and whitespace. A userspace tripwire, not a sandbox - see what gets past it.
Signed audit log
Run serve --receipts and every brokered tool call is written to a signed, hash-chained log. Edits, drops, and reorders break it; herkos verify flags a truncated log. Offline-verifiable with only the public key, local, fail-closed.
Every other tool tells you what it stops. Here is what gets past Herkos.
A security tool that hides its gaps is worse than none. Each of these is checkable against the source.
-
The broker gates tool names, not intent. Approve
send_emailand an injected agent can still BCC an attacker through it - the postmark-mcp shape. A name allowlist cannot catch abuse of a tool you allowed. - The content gate is a tripwire, not a wall. Pin a span, then send the same repo line base64-encoded, paraphrased, or split across two calls and it sails through. It normalizes case and whitespace first, so a reflow or recase trips it - but it matches content, not meaning.
-
The audit log can be truncated locally. Edits, reorders, and mid-drops break
herkos verify, but a local attacker with write access can chop the most recent entries. Herkos makes that detectable (the log shows no clean close), not impossible. -
The broker itself is userspace. Its tool and content gates see what flows over MCP, not what a server opens on its own socket.
serve --isolatecloses that for servers that only need stdio: it starts the server in a kernel network namespace with no route out (proven here, unprivileged). A server that needs its own egress runs unisolated, and per-destination host allowlisting still needs elevated privilege.
make verify-clean builds the committed tree from a throwaway worktree, no trust in us required. Read the full threat model.
One binding, three roles.
Here is make demo: the same span set serves the model's context, bounds what may leave, and signs the receipt - one object doing all three. Read the idea.
$ make demo [1] CONTEXT minimal spans for "Authenticate": auth.go:10-30 db.go:5-25 served 40 / 500 lines -> 92% fewer tokens [2] EGRESS the SAME set is the deny-by-default allowlist: allow auth.go:12-18 (in the served set) -> true deny util.go:100-110 (never served) -> blocked 256 bytes [3] RECEIPT offline-verifiable ed25519 Merkle: signer key -> VERIFIED wrong key -> FAILED root=02659078080f474c9fac20c7a83aa070f95cda014e0b0ea0994d7dde477d3008
$ herkos scan --config .mcp.json [unbrokered] sketchy: launched directly, not through the broker [unpinned-install] sketchy: npx installs an unpinned package; a bad update runs silently herkos scan: 0 over-scoped, 0 poisoned, 0 unrestricted-egress, 1 unbrokered, 1 unpinned-install, 0 remote - your code never left this machine
That last line is true for this run because the scan only read local config. It is a statement about this run, not a guarantee about every future one. See the security model for where the line holds and where it does not, and the case studies for what Herkos does and does not catch on real 2025 incidents.
Three commands.
Generate a local key, then put Herkos in front of any MCP server, allowing only the tools you name. Everything after -- is the upstream server command.
Generate your signing key
An ed25519 key, written 0600. It stays on your machine and signs every receipt.
$ herkos keygen
Broker an upstream MCP server
The agent's MCP client launches herkos serve. A tools/call to any tool you did not allow is blocked in-path and answered with a JSON-RPC error. The session keeps running.
$ herkos serve --allow-tool read_file --allow-tool list_dir -- npx -y @some/mcp-server
Audit any MCP config
Scan an MCP config for over-scoped tools, poisoned descriptions (drift from a baseline you set), and servers with unrestricted egress. Get a receipt you can diff against a baseline.
$ herkos scan --config mcp.json
Read the full quickstart for the register flow that wires Herkos into an existing config.
We publish our own bypass.
A security tool that hides its gaps is worse than no tool, because you trust it more than you should. Here is exactly what v1's in-path broker does not do yet.
-
By default it gates the tool name, not the arguments. The broker decides which
tools/callreach the upstream by tool name. An allowed tool can carry data in its parameters. Without a served set pinned, Herkos does not inspect those. -
It only gates
tools/call. Other methods, includingresources/read, are not gated by the in-path broker in v1. -
Encode the bytes to get them past the content gate. With a served set pinned (
--served-span), Herkos blocks tool-call arguments carrying repo lines from outside that set, after normalizing case and whitespace. Base64, paraphrase, or splitting a line across calls still defeat the match; a reflow or recase no longer does. The transformation-resistant, kernel-enforced boundary is on the roadmap. -
userspacemode is advisory, not airtight. The tool and content gates are auditing and deny-by-default control, not a kernel-enforced seal on bytes.serve --isolateadds a real kernel boundary - a server with no route out of its namespace - for servers that only need stdio; the OS-enforced per-destination egress mode (Landlock and seccomp, then eBPF) is still on the roadmap.
serve --isolate a kernel network boundary for stdio-only servers. It is not yet a full per-destination egress seal. Read the full threat model and bypass.
Local-first. Open source. Apache-2.0.
Written in Go. It runs on your machine and the key never leaves it. Read every line, file an issue, send a patch.