Local-first · open source · Apache-2.0

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.

See it run

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.

agent Claude Code, Cursor herkos serve deny-by-default tool gate + content tripwire upstream MCP server possibly untrusted signed audit log hash-chained, context-bound herkos verify offline, public key only tools/call allowed: forwarded denied: blocked in-path every call

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.

herkos scan flags an unbrokered, unpinned, remote MCP server; herkos serve brokers a real MCP server and blocks a non-allowlisted tool in-path; herkos verify reports VERIFIED for the signed context-bound receipt and FAILED when a byte is tampered
The problem

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.

The scenario

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.

The one mechanism

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.

query from the agent local code graph emits minimal (file, line-range) spans one span set model context the spans it needs egress allowlist deny-by-default

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.

The published bypass

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_email and 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 --isolate closes 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.
We ship our own bypass list - the gaps included. Clone it and try to break the gate yourself - make verify-clean builds the committed tree from a throwaway worktree, no trust in us required. Read the full threat model.
Live demo

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.

herkos - SpanGate demo
$ 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 - MCP security receipt
$ 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.

Quickstart

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.

Honest limits

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/call reach 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, including resources/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.
  • userspace mode is advisory, not airtight. The tool and content gates are auditing and deny-by-default control, not a kernel-enforced seal on bytes. serve --isolate adds 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.
State it plainly: the userspace broker gives you deny-by-default tool control plus a signed audit trail, and 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.

Deny-by-default Signed receipts Local-first Apache-2.0