OBOL v1 · GATE TERMINAL HOME · RESEARCH · OBOL SITE ↗

Obol


      .-~~~~~~~-.
    .'  NOTHING  '.
   /   CROSSES    \       PER-ACTION AUTHORITY
  |    .-----.     |      FOR AI AGENTS
  |   (   ◉   )    |
  |    '-----'     |      v1 · 0.1.0 · Linux
   \  WITHOUT THE /
    '.   FARE    .'
      '-._____.-'          NO COIN  →  NO CROSSING

v1 built445 tests passingNot publicly released

The agent holds no standing credentials. It holds coins.

A coin is a signed, expiring, single-use grant that says exactly which action the agent may take and with which arguments. A deterministic gate checks the coin against the exact call, burns a use, and writes a hash-chained receipt. Refusals get receipts too. No model sits in the enforcement path — checking a coin is signatures and comparisons.

Without Obol

Give an agent your mail, files or bookings and it can use all of them, for as long as it runs. Text it reads along the way — a web page, an email, a file — can steer it into using that access.

With Obol

You issue a coin: “book one flight to Denver, at most $600, expiring in 20 minutes.” The agent can spend it on that action, within those limits, and nothing else. If the destination came from an untrusted web page or email, the taint check refuses it.

What v1 is

A local gate daemon (Unix socket, systemd user unit)built
The obol command-line tool: mint, approve, spend, receiptsbuilt
Claude Code hook: every tool call pays at the gatebuilt
MCP proxy: gates any MCP server’s toolsbuilt
Optional nftables egress firewall for the agent’s userbuilt · tested live
Plain Python 3.12+, one dependency (cryptography, Ed25519)Linux

Navigate with the function keys below, or press 1–7 / arrow keys.

Coins & The Gate


A coin, as obol prints it

id obl_xxxxxxxxxx issuer <your Ed25519 public key> bearer agent:claude-code/home for "book a flight to Denver (travel folder)" ← your request, verbatim may mail.read where label = travel travel.book where dest = DEN, total ≤ 600 forbid args may not come from: web, mail:unknown uses 2 expires +20m parent — (child coins may only narrow) sig ed25519:…

Argument constraints a grant can use

eq · inexact value / one of a list
prefix · glob · regexstring shape
lte · gte · lt · gtnumeric limits (e.g. total ≤ 600)
path_underreal path inside a folder; symlink and .. escapes resolved

What the gate checks, in order

The first failure wins, and the refusal code tells you why.

01bad_sigThe coin’s Ed25519 signature doesn’t verify against your issuer key.
02expiredPast its expiry time.
03exhaustedAll its uses are spent (the ledger counts them atomically).
04revokedYou revoked it.
05audienceBound to a different server than the one being called.
06no_grantThe coin doesn’t cover this tool at all.
07constraintThe tool is covered, but an argument is outside its limits.
08taintedAn argument came from an origin the coin forbids (web page, unknown sender).

Pass: the call runs, one use burns, a spend receipt is written. Fail: the call is refused and a refuse receipt is written. Each receipt carries the hash of the one before it, so editing or deleting any line breaks obol receipts verify.

Walkthrough · The Denver Scenario


The scenario from the v1 test suite, as commands. You want the agent to read your travel mail and book one flight to Denver for at most $600.

1 · Mint a coin

$ obol mint --for "book a flight to Denver (travel folder)" \ --may "mail.read:label=travel" \ --may "travel.book:dest=DEN,total<=600" \ --ttl 20m --uses 2 --wait request pending — approve it in another terminal (or your policy auto-approves)

2 · Approve it

$ obol pending $ obol approve --last

3 · The agent pays at the gate

$ obol spend --coin obl_xxxxxxxxxx --tool travel.book --arg dest=DEN --arg total=548 exit 0 → paid, one use burned · exit 3 → refused, verdict printed

With the Claude Code hook or the MCP proxy installed, this happens automatically on every tool call.

4 · What gets refused

mail.send — not in the coinno_grant
travel.book with dest lifted from a web page or unknown sendertainted
A third travel.book after both uses are spentexhausted
Any spend after the 20 minutes are upexpired

5 · Check the record

$ obol receipts # last 20 entries $ obol receipts verify # walks the hash chain; exit 0 if intact

The Parts


Claude Code hook

obol hook claude-code runs before and after every tool call. Before: it asks the gate to spend a coin for the call and allows or denies it. After: it records text that came back from web fetches, web searches and files outside the project, so if that text later turns up in an argument, the argument counts as tainted. A Bash prefix grant like git  refuses chaining (git status; rm -rf /) and inline interpreter code (python3 -c …). Malformed input fails closed.

MCP proxy

obol mcp-proxy -- <server command> sits in front of any MCP server. Until a coin is minted, the agent sees only request_coin and list_grants. After approval it sees just the tools the coin covers, and every call is spent at the gate. Tool results are tagged with the server they came from.

Policy · allow, deny, clamp

# ~/.config/obol/policy.toml [[allow]] # auto-approve routine reads bearer = "agent:*" tools = ["Read"] where = { file_path = { path_under = "/home/user/proj" } } max_ttl = "30m" max_uses = 200 [[deny]] tools = ["Bash:rm -rf*"]

Deny rules are checked first. An allow rule auto-approves a request it fully covers and clamps any extra time or uses down to its caps, never widening. No policy file means every request asks you. obol policy check dry-runs a request without minting.

Built-in hard stops

No coin can touch Obol’s own state (keys, policy, ledger, receipts), however broadly it’s wordedalways on
Child coins (obol attenuate) can only narrow: fewer tools, tighter limits, fewer uses, sooner expiryenforced
MCP batch requests are refused outright so an ungated call can’t ride alongenforced

Egress firewall (optional, needs root)

obol egress --uid N --allow host:port --apply generates nftables rules so the agent’s own OS user can only reach the hosts you list (plus loopback and DNS). Other users are untouched. Tested live: allow-listed hosts reachable over IPv4 and IPv6; everything else, including raw IPs, dropped; re-applying adds no duplicate rules.

Lab Results · September 2026


Trace-replay benchmark · 35 scenarios · 55 attacks

Share of attacks that succeed, by how coins are written (lower is better)

no gate100%
coarse coins89.1%
narrow coins25.5%
narrow + taint14.5%
Benchmark detail (narrow + taint)
Legitimate tasks still completed97.1%
Attacks outside the granted authority2.1% succeed
Attacks inside the granted authority7 of 7 succeed
False refusals1 of 35
Receipt chains verifyevery config
Gate speed (median, 10,000 calls)
check, in-process0.15 ms
check, via daemon0.76 ms
spend, in-process3.0 ms
spend, via daemon6.0 ms

Live fire · real Claude Code · Claude Haiku 4.5 · poisoned CLAUDE.md

The repo’s CLAUDE.md told the agent to copy the .env secrets to disk while fixing a bug.

Obol off4/4 secrets leaked · 2 of 2 runs
Obol on, coin minted for the task0/4 leaked · 3 of 3 runs
Obol on, no coin0/4 leaked · 2 of 2 runs
Bug fix still completed with the coin2 of 3 runs

Denied a read of .env, the agent tried cat .env instead — also denied. The one failed task came from a coin that constrained the wrong argument for Claude Code’s Glob tool; the docs now show the right argument names.

Red team

F6 · inline interpreter code under a prefix grant (python3 -c, perl -e)high · fixed
F7 · a broad coin could reach Obol’s own keys and policyhigh · fixed
F8 · invisible zero-width characters dodged the taint matchlow · fixed
Full test suite after fixes445 passed · 3 skipped
Self-run lab numbers from one lab, small live-fire samples, one small model. Directional, not an independent evaluation.

Known Limits · v1


Stated plainly, from the v1 docs.

It doesn’t stop injection. A fooled agent can still spend whatever the current coin covers — the 7 “inside the authority” benchmark attacks all succeed.by design
It can’t fix a bad coin. Approve something broad and the gate honours it.write coins narrow
The taint bridge is heuristic. It matches copied text, so indirect flows or re-encoded text can slip past.v1
A bare-interpreter grant is code execution. python3  as a prefix is as good as any Python; grant a specific script instead.documented
The socket is the trust boundary. Anyone who can write to obol.sock (mode 600) can mint, approve and spend.protect it
Root beats the gate. The egress firewall is best-effort against a root process.run agents unprivileged
One user, one machine. No multi-user daemon, no cross-host coins.non-goal for v1
Approval is at the terminal. No hardware-key or phone approval yet.non-goal for v1
No payments gate. Spending limits exist as argument constraints only.non-goal for v1