Coco

How a call is decided

The ordered path from a proposed tool call to a verdict, a receipt and an answer the runtime acts on.

How a call is decided

The model has already finished deciding by the time the gate runs. Everything after that point is machinery, not persuasion.

The order of work does not vary.

  1. The mandate's status is checked first. A suspended mandate stops every call before a single contract is loaded, so revocation is one field.
  2. The tool name is routed to a family. Bash is a command, Edit is a file write, mcp__memory__create_entities is an MCP call, payment.send is a payment.
  3. The contracts that govern that family are selected, and each one's applies_when decides whether it runs on this particular call.
  4. Pre-conditions run. These are typed checks against a dotted path into the state, and they are how a contract refuses to judge on state it could not read.
  5. Constraints run. These are boolean rules in a deliberately small expression language.
  6. The most severe finding wins. BLOCK over ESCALATE over ALLOW.
  7. The mode decides what happens to the call. The verdict recorded in the ledger is always the verdict the contract produced.
  8. One receipt is written, allows included.
  9. The session counters advance, but only if the call was not stopped.

That last step is easy to get wrong. A blocked read never happened, so it must not count towards what this session has seen.

What the gate reads

Six namespaces, and every one of them is a fact rather than a model's opinion.

NamespaceWhat it holds
toolThis call, flattened out of the runtime's tool input
sessionWhat this session already did, before this call
caseLive workflow state, read off disk for the case this call touches
platformLive ledger state supplied by the runtime that embeds the gate
envPermission mode, working directory, mandate identity
mandateThe ceilings from your mandate file

tool alone is what a stateless gate sees, and a check written against tool alone can be defeated by splitting one bad action into two allowed ones. session, case and platform are the parts that make a verdict depend on where the work actually sits. The full field list is in the state reference.

The trust boundary for platform is the payload's author. In a Claude Code install that is the harness, and over HTTP it is the platform service calling the gate. It is never the agent whose call is being judged, for the same reason the agent does not write its own case folder markers.

Why the same call can be decided twice differently

case and platform are read at the moment of the call. A write into one customer's folder is judged against that customer's evidence and nobody else's, and the answer changes as the case progresses. That is the whole difference between a static check and a live one, and it is why the same call can be blocked at nine and allowed at ten.

Routing, and what happens to a tool nobody wrote a rule for

Routing is data rather than a switch, so a customer can add a route without touching the evaluator. A handful of tools carry no action worth governing, Glob and Grep and Ls among them, and those pass through without a receipt so the ledger stays readable. The list is fixed and short, and it trades a quieter ledger for not recording searches, which is worth knowing when what was searched for is itself sensitive.

Everything else routes to a family. The table lives in gate/mapping.py as an ordered list of name and prefix rules where the first match wins, so it reads top to bottom, and a harness sending its own tool names can either match an existing rule, payment.* and wallet.* route to the payment family for example, or add a line to the table. A tool that routes to no family at all is denied by default, which means a new tool arriving in a runtime release is denied until someone decides about it. Your mandate can set unknown_tool to something else, and BLOCK is the honest default because an action nobody wrote a contract for has not been authorised.

How the verdict reaches Claude Code

Inside Claude Code the gate is a PreToolUse hook, and the three verdicts map onto the three things a hook can say.

VerdictHook outputExit code
ALLOWnothing0
ESCALATEpermissionDecision: "ask"0
BLOCKpermissionDecision: "deny"2

Two rows there are load-bearing.

ALLOW stays silent. Emitting permissionDecision: "allow" would approve the call and skip Claude Code's own permission prompt, so a gate that said allow out loud would wave through things the operator would otherwise have been asked about. A guardrail that widens permissions is not a guardrail. Coco allows by declining to interfere.

ESCALATE exits 0. Exit code 2 blocks a call whatever the JSON says, so an escalation that exited 2 would be a block wearing an escalation's label.

How the verdict reaches any other runtime

Over HTTP the verdict comes back named rather than as an exit code, because the caller is a workflow step and not a shell.

{"verdict": "BLOCK", "reason": "Coco: The command is a destructive operation with no safe reading in a governed workflow", "mode": "enforce"}

The service runs the same gate file as a subprocess with the same payload, the same contracts and the same ledger. Nothing about the verdict is reimplemented for the network, because a checker that behaves differently over HTTP than it does on disk is not the same checker.

A payload decided on a laptop can be replayed against the service and produce the verdict it produced there. That property is what makes the two sets of receipts comparable.

What happens when the gate itself fails

On the npx road.

FailureWhat happens
The check errorsBLOCK, and the receipt says the gate failed rather than pretending a rule fired. on_error can make it ask or allow, and the choice is recorded
The check hangsThe call runs. Past a 30 second timeout Claude Code stops waiting and proceeds as though no gate were installed

Fail-closed on error, fail-open on timeout, and the timeout is Claude Code's own behaviour rather than a Coco setting. The receipt is written by the check itself, so a timed-out call leaves no row at all, and that hole is in an observe-mode report as much as an enforcing one.

The SDK road is fail-closed throughout. The client answers BLOCK when the gate is unreachable, times out or refuses the key, and the service answers BLOCK when the check errors or hangs inside it. The full matrix is in what happens when the gate breaks.

The gate itself makes no network call and no model call while deciding, which is what keeps a hang unlikely rather than impossible.

On this page