Writing a contract
The full shape of a contract, what the compiler refuses, and how to test a rule before it costs anyone anything.
Writing a contract
Contracts live in ~/.coco/packs/<set>/, one file per contract. The mandate names
which sets load, so a contract can sit in the directory unenforced until someone
opts it in.
The shape
id: kyc.approve_customer
action: approve_customer
family: file.write # or families: [egress, command]
description: >
What this governs and which live state it reads. Write it for the person who
will have to defend the rule, not for the engineer who installs it.
applies_when: '"04_Approvals" in tool.path'
pre_conditions: # typed checks against a dotted path into the state
- id: case_folder_readable
type: equals
path: case.present
value: true
on_fail: BLOCK
reason: The case folder could not be read
constraints: # boolean rules in the safe subset
- id: screening_is_done
rule: case.screening_sanctions == true
on_fail: BLOCK
reason: Sanctions screening evidence is missing from the case folder| Field | What it does |
|---|---|
id | Names the contract. A receipt carries it, so keep it stable |
action | What this governs, in your own vocabulary |
family or families | Which action families this contract runs on. "*" runs on all |
description | Required. The compiler refuses a contract without one |
applies_when | Narrows the contract to the calls worth checking |
pre_conditions | Typed checks. How a contract refuses to judge on state it could not read |
constraints | Boolean rules in the expression language |
Families
A contract declares which actions it governs rather than which tools.
| Family | Covers |
|---|---|
command | Shell commands |
file.read | Reads |
file.write | Writes, edits, deletes |
egress | Anything that fetches or searches over the network |
browser | Browser control |
agent | Spawning subagents, skills, messages between agents |
task | Creating and stopping background work |
mcp | Any MCP tool |
payment | Payment and wallet calls |
Pre-conditions and constraints
A pre-condition is a typed comparison against a dotted path into the state. Use it when the question is whether the state is there at all.
| Type | Passes when |
|---|---|
exists | The value is not missing |
not_empty | A string, list or mapping has something in it |
equals, not_equals | The value matches, or does not |
greater_than, less_than, gte, lte | The numeric comparison holds |
in_list, not_in_list | The value is in the given list, or is not |
matches_regex, not_matches_regex | The pattern is found, or is not |
A constraint is a boolean rule. Use it when the question is what the state says.
constraints:
- id: monthly_budget_holds
rule: 'platform.monthly_spend_usd + tool.amount <= mandate.monthly_budget_usd'
on_fail: BLOCK
reason: >
This payment would carry the wallet past its monthly budget, so it is
stopped before the signer whatever the merchant or the amountRules take comparisons, boolean operators and arithmetic. No function calls, no lambdas, no comprehensions, nothing private. A rule an auditor cannot read out loud in a meeting is a rule nobody will defend when it blocks something.
The whole language fits in one table.
| You can write | Examples |
|---|---|
| Comparisons | == != < <= > >= |
| Membership | in, not in, against a list or a string |
| Boolean | and or not |
| Arithmetic | + - * / %, so a rule can say spend + amount <= budget |
| Literals | numbers, quoted strings, lists like [1, 2], and true, false, null |
| State | any dotted path from the state reference |
A name the state does not carry reads as nothing rather than raising, so the check fails instead of the gate crashing, and a comparison against nothing fails too. The regex types in pre-conditions use Python's regular expression dialect.
Choosing the verdict
on_fail takes ALLOW, BLOCK or ESCALATE, and the most severe finding across every
contract that ran decides the call.
Reach for BLOCK when no person inside this mandate should be able to approve the action. Reach for ESCALATE when a person legitimately can, and there is somebody to ask. The shipped spending contract uses both on purpose. A single payment over the per-payment limit escalates, because that is a decision a person can take. A payment that would carry the month past the wallet's budget blocks, because it is not anyone's to approve inside that mandate.
An ESCALATE that cannot reach a person is not an escalation. In a session running
in a permission mode that never prompts, the gate falls back to whatever the
mandate's escalate_fallback says, and deny is the only answer that keeps an
escalation meaningful.
Write the reason for a person
Every check needs a reason, and the compiler refuses a check without one. That sentence is what the agent receives, what the operator sees on the prompt, and what sits on the receipt an auditor reads.
Name the state, not the rule id. "Sanctions screening evidence is missing from the case folder" tells someone what to do. "screening_is_done failed" does not.
Compile after any change
coco compileThe compiler refuses a contract that will not evaluate. A rule with a syntax error, a verdict that is not ALLOW, BLOCK or ESCALATE, a check with no id, a check with no reason, a duplicate contract id, a mandate with no id. Each is reported by name at compile time rather than discovered by a block in the middle of someone's work.
Restart Claude Code afterwards, or restart the container.
Test it before it costs anyone anything
The safest test is observe mode on real traffic, and the fastest is a payload through the gate directly.
echo '{"session_id":"t1","cwd":"/tmp","permission_mode":"default",
"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' \
| python3 ~/.coco/gate/coco_gate.py{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Coco: The command is a destructive operation with no safe reading in a governed workflow"}}Over HTTP the same test is one POST /check. Either way the verdict is written to
the ledger, so your test rows are in the report alongside real traffic. Use a
session id you can recognise.
Next
- State reference for every field a rule can read
- The mandate for ceilings and case state
- Drafting from a policy to start from a document