Your agent ran twice. Which step caused it?

Two 47-step runs, side by side, is a spot-the-difference puzzle. behalf records every action an agent takes and shows you the first divergence — not the noise.

the diff
$ npx onbehalf diff run_9f2a run_c71e

  ── first divergence ────────────────── hop 3, t+60s
  step 12   billing-agent → orders.search
     9f2a   [0] ord_5512  $12.00     → ok
     c71e   [0] ord_5518  $1200.00   → ok   ← different first result

  ── consequence ───────────────────────────────────
  step 31   billing-agent → refund.issue(...)
     9f2a   amount=12.00    target=ord_5512   → ok
     c71e   amount=1200.00  target=ord_5518   → ok

  ── 20 downstream differences suppressed (--all to show)

Forty-seven steps each, twenty-two of them different, both runs ok from end to end. An error tracker has nothing to show you. diff names the one step that caused the other twenty-one.

Try it

Two recorded runs of one agent, unpacked onto your machine. No network, no key, no account; nothing is written until you type demo.

four commands
$ npx onbehalf demo
$ npx onbehalf runs                       two runs of one agent. both ok.
$ npx onbehalf diff run_9f2a run_c71e     which step made them differ
$ npx onbehalf why run_c71e:31            and on whose behalf it was done

What those commands render, if you would rather look first. The verifier on its own: cargo install behalf-verify. The Go module:go install github.com/behalf-sh/behalf/cmd/behalf@latest. macOS and Linux, x64 and arm64; not Windows — the log's storage driver is POSIX-only, WSL works. Apache-2.0, except the log service and witness (FSL-1.1-ALv2). Get started has the rest.

Every action is a receipt. Try covering one up.

Each action an agent takes across a trust boundary — a tool call, a delegation, an approval — becomes a signed, hash-chained action receipt in an append-only log. Edit one and everything after it stops verifying:

the cover-up
$ sed 's/1200.00/12.00/' run_c71e.jsonl > covered.jsonl
$ behalf-verify covered.jsonl
✖ TAMPERED
receipt 31: content hash mismatch (expected 4f9d… computed 9690…)
chain breaks at 31; receipts 32-46 unverifiable.
$ echo $?
1
stderr: class=content index=31

Exit codes are stable — 0 verified, 1 tampering detected, 2 unverifiable — so CI can gate on them. Verification is offline: no network, no call to us. Hand the export file and the verifier to anyone and they can check it themselves, or drop it on the browser verifier— the file never leaves the tab.

And on whose behalf?

Every receipt carries the delegation chain that authorised it — human → agent → sub-agent → tool. why walks it, and says which hops are signed and which are only claimed:

the chain
$ npx onbehalf why run_c71e:31
refund.issue(amount=1200.00)                    run_c71e  step 31

  ✔ alice@acme.com                    verified   OIDC/google  02:16:58Z
       │ delegated: "resolve ticket 4417"
       │ scope: tickets.*, orders.read, refund.issue<=100.00
       ▼
  ✔ support-orchestrator @1.4.2       verified   ed25519 ..whCQN8
       ▼
  ✖ billing-agent                     UNVERIFIED
       │ actor "alice@acme.com" is caller-asserted. no signature.
       ▼
    refund.issue  amount=1200.00

  ⚠ scope: refund.issue<=100.00 delegated; 1200.00 issued. (recorded, not enforced)

  chain intact for 2 of 3 hops.

The refund was issued under a name the caller typed in. Nothing signed it. The human at the root delegated a hundred dollars, and the receipt says so — recorded, not enforced.

How it works

Capture

A proxy sits on the trust boundary and records the request that was actually forwarded — not what the agent says it did.

Receipt

Each action is signed at the edge (DSSE, Ed25519) and hash-chained. The stored bytes are the signed bytes are the hashed bytes — there is no canonicalization step to argue about.

Verify

A separate offline verifier, in Rust, checks the whole export and exits 0, 1, or 2. Reconstruction of what happened, checkable by a third party who has never spoken to us.

Verified, asserted, broken

A receipt is verified when its signature and chain position check out against the keys in the export. History imported from other tools, and any hop we could not verify at write time, is marked asserted — recorded honestly as a claim, not evidence. A chain that fails is broken, and everything after the break is reported unverifiable.

The offline verifier re-checks four of the six delegation-chain invariants an export carries — authority, depth, expiry, linkage — and prints on every run, including clean ones, the two it does not (capability monotonicity, proof of possession) and that the identity root is not checked offline. A green tick means exactly the checks it names.

The output never lets a green tick imply more than the cryptography delivers. What we explicitly do not claim — and the adversaries we do and do not defend against — is written down in the threat model.

Where it stands

Want the release notes?