Start free

How HESO works

Every action your agent takes runs the same four-stage loop: intercept, decide, sign, verify. This page walks the whole loop end to end.

The loop

HESO sits between your agent and the world. The moment your agent is about to do something — call a model, hit an API, move money — HESO catches the call, runs it past your policy, records the verdict as a signed Action Receipt, and chains that receipt onto the one before it. The same four steps happen for every single action, in the same order, with no exceptions.

  1. Intercept — capture the action before it runs.
  2. Decide — evaluate it against your policy and pick one of four paths.
  3. Sign — sign the verdict and the exact action bytes, then chain the receipt.
  4. Verify — let anyone re-check the receipt later, offline.

1 · Intercept

The SDK captures the action before it executes, never after. This is the whole point: a receipt for an action that already happened proves nothing, so HESO records intent first and lets the action through only if the verdict allows it. When you wrap a function with @heso.tool, the SDK intercepts the call and builds an ActionDetail from it.

The captured ActionDetail records what the action is and where it points:

  • verb — the kind of action, one of the seven verbs below.
  • tool_name — the specific tool or function being called.
  • workflow and account — which agent run and which tenant the action belongs to.
  • fields — the call’s arguments, as a string map.
  • target_host — the host the action reaches out to (omitted for llm_call and other internal actions).

Every action carries exactly one verb. The verb set is fixed:

  • llm_call — a model completion.
  • tool_call — a function or tool invocation.
  • http_request — an outbound HTTP call.
  • payment — moving money.
  • data_export — sending data out.
  • account_change — changing an account.
  • delete — destroying a resource.

The minimal capture is three lines: import the SDK, call heso.init(), and decorate the tools you want gated. From that point on, every call to a decorated function runs the loop.

agent.py
import heso heso.init() @heso.tooldef transfer_funds(payee: str, amount_usd: int) -> str:    # the real call runs only after policy says it may    return stripe.transfers.create(payee=payee, amount=amount_usd)

2 · Decide

With the action captured, the policy engine evaluates it first-match-wins: it walks your ordered list of rules and the first rule whose subject, verb, scope, and conditions all match decides the outcome. The check runs locally in under 2 ms, so gating adds no meaningful delay to your agent.

Every decision resolves to one of four paths:

  • allow — the action proceeds as written.
  • block — the action is stopped and never runs.
  • redact — sensitive fields are replaced before the action proceeds.
  • require_approval — the action is routed to a human and waits for a decision.

When a field is redacted, its value is replaced by a hash before anything leaves the process — the cleartext never travels to HESO. If no rule matches a dangerous lane, the default is require_approval, so the unknown case stops and waits instead of going through. To write and shape these rules, see Policy & decisions and Policy files.

3 · Sign

Once a verdict exists, HESO commits it. The verdict and the exact action bytes are signed with an Ed25519 operator key, and the action’s BLAKE3 action_hash is chained onto the previous receipt — so changing any earlier entry breaks every receipt that came after it. The result is a tamper-evident audit chain: one signed receipt per action.

If the decision was require_approval, the action pauses until a human acts on it. When an authorized approver signs off, they co-sign with their own device-held key — the cloud holds no signing key — and the receipt records two signatures instead of one. That co-signed receipt is L1; an operator-only receipt is L0. The trust level is not a claim you set; it is derived from which signatures are actually present.

For the mechanics of each piece, see Trust levels, Human approval, and The audit chain.

4 · Verify

A receipt is only useful if someone other than you can check it. So verification needs nothing from HESO: anyone can drop a receipt into a browser, and the Ed25519 signatures and BLAKE3 hash are re-run locally — no network call, no account. The verifier walks a fixed seven-gate order and stops at the first gate that fails, so a tampered receipt is caught the moment its math stops adding up.

Crucially, the trust level is re-derived on every verify from the signatures that actually pass. A receipt that claims L1 but carries only an operator signature fails with a trust mismatch — you cannot dress up an L0 action as a human-approved one. See Offline verification for the full gate order, and Verify in the browser to try it.

What it proves

What this proves

A verified receipt proves the operator authorized this action under a known policy, and — at L1 — that a person approved it with a device-held key.

It records what was authorized — the decision and who made it — not whether the action succeeded downstream. HESO records authorization, cryptographically; the outcome is a separate question.

Where each stage runs

Three of the four stages run in your process. Interception, policy evaluation, and signing all happen locally, inside the SDK, on top of one Rust core — there is no round-trip to HESO to decide or sign an action. The control plane receives the finished receipt and re-verifies it before accepting it, but it never sits in the path of the decision itself.

Verification runs wherever the receipt goes. The same Rust core is compiled to Python, Node, and browser WASM, so the result is byte-identical whether it runs on your server or in a reviewer’s browser — the crypto is never rewritten in JavaScript or Python. That is what makes a receipt portable: the proof travels with the receipt, not with your infrastructure.

Next steps