judge
The judge slot. State plus typed questions in, typed answers out, held to a threshold and signed as judgement.v1. The built-in judge is deterministic rules; --judge-url sends the same request to any judge that speaks the contract.
treeship judge asks a judge typed questions about a tool call and signs what came back and what was done about it. The contract is the same three primitives for every judge: a noul (a yes/no probability), a choice from a fixed set, a score on an ordered rubric. A decision model, an LLM prompted to judge, a classifier and a rules engine all fit it, so one receipt shape, judgement.v1, covers all of them.
treeship judge --tool Bash --input '{"command":"rm -rf /"}'
treeship judge --tool WebFetch --input '{"url":"https://x.example/"}' --attest --subject art_…
treeship judge --tool mcp__pay__charge --input '{"amount":250}' --bound 100
treeship judge --tool Bash --input @call.json --judge-url https://judge.internal/v1/judge --threshold 0.9The built-in judge: rules
No model is in the decision path unless you put one there. The default judge is treeship-rules/<version>: deterministic pattern rules over the call, each answered with probability 1 or 0, confidence 1. Because it is deterministic it is replayable: a verifier holding the receipt's state can re-run it and get the receipt's answer, which no sampled model can offer.
| Question | Answers yes when |
|---|---|
path_outside_workspace | a path-like field (file_path, path, notebook_path, paths, target, destination, …) resolves outside the workspace root, lexically: .., absolute paths elsewhere, anything under ~ |
shell_destructive | the command removes a broad target with -r/-f (/, ~, *, .., a path outside the workspace), or writes disks, makes filesystems, hard-resets or force-pushes git, drops tables or databases |
shell_exfiltrates | the command uploads with curl or wget, copies to a remote with scp, rsync or sftp, opens nc to a port, or pipes a secret-looking file (.env, id_rsa, credentials, .aws/, …) into a network tool |
network_off_scope | a url field or a URL inside the command names a host outside the declared network scope; with no scope declared the answer is 0 |
amount_above_bound | an amount-like field (amount, total, price, value, cost, …) exceeds --bound; with no bound the answer is 0 |
unsafe | any of the above |
The rules judge does not guess: a question it has no rule for is an error, not a low-confidence answer.
judge: treeship-rules/0.31.6 (rules, replayable)
tool: Bash (shell.exec)
threshold: 0.5 (set by default)
amount_above_bound 0.00 acted (allow)
network_off_scope 0.00 acted (allow)
path_outside_workspace 0.00 acted (allow)
shell_destructive 1.00 refused (deny)
shell_exfiltrates 0.00 acted (allow)
unsafe 1.00 refused (deny)
⚠ refused: shell_destructive, unsafeHolding an answer to the bar
--threshold (default 0.5) is the bar. A noul at or above it refuses the call (deny, outcome refused); below it the call proceeds (allow, acted). A choice whose option is one of allow, warn, deny, ask is taken as that effect when its confidence meets the bar and escalated to ask when it does not. Any other choice, and every score, is recorded with outcome ignored: the caller has no rule that turns it into an effect, and the receipt says so instead of inventing one. Across several questions the strongest effect wins (deny over ask over warn over allow) and decided_by names the questions that carried it. --set-by records who set the bar (a policy id, a card, an operator).
Signing the judgement
--attest signs one judgement.v1 receipt per question, by system://treeship-judge with the ship key: the judge's model, kind and replayability, digests of the state and the questions, the question and its typed answer, the threshold and who set it, the outcome and effect. Inside a session the receipts chain onto its head in order, so the sealed package shows them beside the call they decided and package verify reports them in the judgements row, flagging any acted on below its own bar. --subject <art_…> names the action the judgement is about. verify last after --attest is the newest judgement.
What the receipt proves: that the caller committed to this typed answer to this question about this state, held it to this bar, and did this. The receipt is signed by the caller, not by the judge, so on its own it is the caller's claim about what the judge said. Two fields narrow that: response_digest is the hash of the exact bytes the judge returned, computed as received, so a fabricated answer has to come with a body that hashes to it; and judge.request_id is the judge's own id for the answer when it gives one (Jev's x-typesafe-request-id), which a judge that keeps its responses can be asked about. A judge signing its own answer is the next step; it is not this receipt. For the rules judge a verifier can re-run the rules on the state and check the answer. For a sampled judge (replayable: false) nothing can be re-run, and the receipt never proves the judgement was right.
Who holds the state
The receipt commits to the state by digest and does not carry it: the state can hold a command line, a file path, or a payment, and the caller decides who sees it. --state-out <file> writes the canonical state the digest was computed over. A verifier given that file checks it hashes to state_digest, re-runs the rules judge on it for a rules receipt, or reads what an outside judge was shown. Without the file, the digest still binds the receipt to one state; it just cannot say which.
Any judge: the HTTP contract
--judge-url sends the request to an HTTP judge and holds its answer to the same checks. The judge is anything that speaks this contract: a decision model such as Jev behind a small adapter, an LLM judge, a classifier, a rules service of your own.
Request, POST as JSON:
{
"state": { "tool": "Bash", "capability": "shell.exec", "input": { "command": "rm -rf /" },
"workspace_root": "/work/proj", "network_scope": ["api.example.com"], "amount_bound": 100 },
"questions": {
"unsafe": { "type": "noul", "instructions": "Any of the above." },
"verdict": { "type": "choice", "instructions": "allow or deny", "options": ["allow", "deny"] }
}
}Response, 200 with JSON:
{
"judge": { "model": "jev-1.13.0", "provider": "typesafe", "kind": "decision-model", "replayable": false },
"answers": {
"unsafe": { "noul": 0.97, "probabilities": { "yes": 0.97, "no": 0.03 }, "confidence": 0.97 },
"verdict": { "choice": "deny", "probabilities": { "allow": 0.1, "deny": 0.9 }, "confidence": 0.9 }
},
"latency_ms": 41
}Every question must be answered with the value its type calls for (noul for a noul, choice among the options for a choice, score for a score); every probability and confidence must lie in 0 to 1. An answer that fails those checks is an error, not an allow. A non-2xx status, a timeout (10 seconds) or a body that is not an answer is judge unavailable, also an error: the caller decides what an unavailable judge means, and the gate's answer is to fail open and say so in the timeline.
--question <key> asks a subset by key (an outside judge may be asked keys the rules do not know; they are sent as noul questions). --questions-file <file> sends typed questions of your own, {key: {type, instructions, options}}.
The decision contract
--contract ticket-router@3 records, in every receipt, the versioned decision contract the judgement ran under: the definition of state fields, question, options or rubric, threshold and allowed action, versioned separately from the application so a change to the question is reviewable and reversible like a code change. The receipt carries contract: {id, version} beside questions_digest; the digest proves which questions were asked, the contract says which version of the design they belong to.
Escalation and the human's decision
An answer that sends the call to a person is outcome escalated, and the judgement stays an open question until a person answers it. The answer is its own signed artifact, not a field the machine fills in:
treeship judge --resolve art_9c1e… --by human://alice --decision allow --reason "reviewed the diff"
treeship judge --resolve art_9c1e… --by human://alice --decision route --route fact_checkThat signs a judgement.resolution.v1 receipt under the decider's URI, with the judgement as its subject, chained onto the session. decision is allow, deny or route (with --route); when it contradicts what the judge's answer led to, the receipt records what it overrides. A refused judgement can be resolved the same way, which is how a human allows what the judge refused.
package verify reads the pair back in the judgements row: 1 escalated, 1 resolved (art_… allow by human://alice), or 1 OPEN with no signed resolution in this package, which is a warning. An override of a non-escalated judgement is named too. A resolution signed in a later session resolves the escalation for a verifier holding both packages; in its own package it is a resolution the row does not need to match.
Why a separate artifact: a machine writing "a human overrode this" into its own record is the self-report a receipt exists to replace. The resolution is a claim the decider made, under the decider's name, that verifies on its own. The Claude Code gate's ask reaches the operator through the harness prompt, which leaves no resolution; a harness that can call the CLI after the operator answers records one.
Into Reason
--format json also prints reason_facts: the same answers as Zerker Reason premises under the model-judged authority class, one per question, with the signed receipt as the fact id when --attest ran.
{ "id": "art_1f3c…", "predicate": "judged_unsafe", "arguments": ["art_action…", "no"],
"authority": "model-judged", "observed_at": "2026-09-23T12:00:00Z" }A Reason program admits model-judged per predicate; the convention is to admit it for judged_* predicates only and to let a judge's answer deny but never stand in for a human approval or a tool report. See Reason's docs/AUTHORITY.md, "Model-judged evidence".
In the gate
The Claude Code plugin's gate asks the judge when TREESHIP_JUDGE=1 (rules) or TREESHIP_JUDGE=<url> is set, after the card has allowed the call and never instead of it. A deny refuses the call and signs a blocked.v1 with reason_class: policy_threshold_exceeded naming the judge and the questions; an ask asks the operator; the answers are signed either way. TREESHIP_JUDGE_THRESHOLD and TREESHIP_JUDGE_BOUND set the bar and the amount bound. See Claude Code.
Two different rules apply to a judge that cannot answer, and both are deliberate. To treeship judge it is an error, never an allow: the command's one job is the judgement, and no judgement is a failure. To the gate, by default, it is a note: the agent's card already allowed the call before the judge was asked, the judge is a second layer on top of that decision, and the gate's standing rule is that a broken Treeship never blocks a tool call by accident. The timeline records that the call went unjudged. TREESHIP_JUDGE_STRICT=1 changes the gate's answer: an unanswered call is escalated to the operator, so nothing runs unjudged in a session that asked for a judge.
Options
| Option | Description |
|---|---|
--tool <NAME> | The harness's tool name (required) |
--capability <CAP> | The card's name for it, recorded in the state |
--input <JSON> | The tool's input, or @<file> |
--question <KEY> | Ask this question (repeatable); default: every rules question |
--questions-file <FILE> | Typed questions of your own |
--judge-url <URL> | An HTTP judge instead of the built-in rules |
--threshold <0..1> | The bar (default 0.5) |
--set-by <WHO> | Who set the bar, into the receipt |
--bound <AMOUNT> | The amount bound for amount_above_bound |
--subject <ART_ID> | The action this judgement is about |
--attest | Sign each answer as judgement.v1 |
--contract <ID@VERSION> | The decision contract the judgement runs under, into every receipt |
--state-out <FILE> | Write the canonical state the receipt's state_digest commits to |
--enforce | Exit 2 on deny and 3 on ask, for shell gates |
--resolve <ART_ID> | Resolve a judgement instead of judging; needs --by and --decision |
--by <URI> | Who decided (human://alice, or an agent:// for a stronger judge) |
--decision <DECISION> | allow, deny or route; route takes --route <name> |
--reason <TEXT> (requires --resolve) | Why, in the decider's words |
--format json | Machine output: effect, outcome, decided_by, answers, receipts, response_digest, request_id, digests |
Exit code
By default the exit code says whether the judge answered, not what it answered: 0 for any decision, non-zero only when the judge could not run or gave a malformed answer. The decision is in the output (effect, decided_by), which is what the gate reads. That is deliberate, and different from verify, where a failed mandate fails the command: a judgement is a query the caller acts on, a verification is a verdict. A shell gate that wants the decision in the exit code passes --enforce.