Receipt Query Cookbook
Filter local and remote receipts, paginate large logs, and export the records an audit asks for.
On this page
Answer an auditor's questions from the signed record: which calls were refused in the last day, which guard refused them, what one agent wrote last week, and where the month's spend went.
Every recipe reads the receipts your configured storage retained, through chio receipt list or the trust-control HTTP routes, and ends in a file you can hand over. Calls outside the mediation boundary and records that were never persisted are outside every query.
1. Open the store with a read boundary
Point the CLI at a local SQLite store with --receipt-db, or at a running trust-control service with --control-url. A local read names its tenant with --tenant <id>, or every tenant with --admin-all; the two flags are mutually exclusive.
# A local store: name the read boundary.
chio --receipt-db ./receipts.db receipt list --tenant "$TENANT"
# A remote store: the control token carries the scope.
export CONTROL_URL=http://127.0.0.1:8940
export CHIO_CONTROL_TOKEN=replace-with-your-control-token
chio receipt list --control-url "$CONTROL_URL" --limit 50You should see one JSON receipt per line. A local read that names no boundary fails closed instead of defaulting to every tenant, and says what to supply:
$ chio --receipt-db ./receipts.db receipt listerror[CHIO-KERNEL-RECEIPT-READ-CONTEXT-MISSING]: a local receipt read needs a read boundary help: pass --tenant <id> to read one tenant's receipts, or --admin-all to read every tenant as the operator code: urn:chio:error:kernel:receipt-read-context-missing docs: https://chio.computer/docs/reference/errors#error-kernel-receipt-read-context-missing
A remote read takes its scope from the bearer token, so it refuses --tenant and --admin-all. The CLI reads the token from CHIO_CONTROL_TOKEN; the HTTP recipes below send it in the Authorization header. The jq projections are optional, and unfiltered local queries need only the CLI.
$ chio --control-url "$CONTROL_URL" --control-token "$CONTROL_TOKEN" \
receipt list --tenant acmeerror[CHIO-CLI-FLAG-CONFLICT]: --tenant and --admin-all apply to a local --receipt-db; a remote read takes its scope from the control token help: drop --tenant, or use a control token scoped to that tenant code: urn:chio:error:cli:flag-conflict docs: https://chio.computer/docs/reference/errors#error-cli-flag-conflict
Read more in Query Audit Receipts, which sets up a local store from the Quickstart.
2. List the last day's denials
Filter on outcome=deny and a start time on the server, then project the fields a triage needs with jq.
NOW=$(date -u +%s)
SINCE=$((NOW - 86400))
chio receipt list \
--outcome deny \
--since "$SINCE" \
--control-url "$CONTROL_URL" \
--limit 200 \
| jq -c '{id, ts: .timestamp, server: .tool_server, tool: .tool_name,
guard: .decision.guard,
denied_by: [.evidence[]? | select(.verdict == false) | .guard_name],
reason: .decision.reason}'You should see guard read kernel on every row, because the kernel is the layer that refused. The denied_by column names the guard, which separates a forbidden path from a shell-command block. A revocation refusal has no guard evidence: the kernel refuses it before the pipeline runs, and its reason reads capability has been revoked: <id>.
The decision object is flat. Decision is an internally tagged enum, so a denial serializes as {"verdict":"deny","reason":...,"guard":...}. Read .decision.guard; the path .decision.deny.guard is valid jq and returns null on every row, which looks like an empty result set.
From application code, the project history answers the same question for the computer you opened. The output below reads the Quickstart project's history.
import { Computer } from "@chio-protocol/sdk";
const project = await Computer.open("quickstart");
const refusals = await project.history.receipts({ outcome: "deny" });
for (const receipt of refusals) console.log(receipt.summary);rcpt-01a8…9c04 deny fixer read /secrets/.env outside the grant rcpt-01b3…77e1 deny fixer write /workspace/tests/test_pricing.py rule tests-are-evidence
from chio import Computer
project = Computer.open("quickstart")
refusals = project.history.receipts(outcome="deny")
for receipt in refusals:
print(receipt.summary)rcpt-01a8…9c04 deny fixer read /secrets/.env outside the grant rcpt-01b3…77e1 deny fixer write /workspace/tests/test_pricing.py rule tests-are-evidence
use chio::Computer;
let project = Computer::open("quickstart").await?;
let refusals = project.history().receipts().outcome("deny").all().await?;
for receipt in &refusals {
println!("{}", receipt.summary());
}rcpt-01a8…9c04 deny fixer read /secrets/.env outside the grant rcpt-01b3…77e1 deny fixer write /workspace/tests/test_pricing.py rule tests-are-evidence
3. Tell a guard denial from a budget denial
The query routes have no guard-name filter. The guard that fired is an entry in evidence[] whose verdict is false, so filter denials on the server and narrow on that array. Policy hashes and reason strings narrow the same way. Two denials from two different guards, read three ways in one line:
$ chio --receipt-db ./receipts.db receipt list --admin-all \
| jq -c '{tool: .tool_name, verdict: .decision.verdict,
$ guard: .decision.guard,
$ denied_by: [.evidence[]? | select(.verdict == false) | .guard_name]}'{"tool":"run_command","verdict":"allow","guard":null,"denied_by":[]}
{"tool":"read_file","verdict":"deny","guard":"kernel","denied_by":["forbidden-path"]}
{"tool":"run_command","verdict":"deny","guard":"kernel","denied_by":["shell-command"]}To select one guard's denials, match its name in the evidence. Swap secret-leak for any name in the guard name list; a guard you built with Custom Guards reports the name it registers under.
chio receipt list \
--outcome deny \
--limit 200 \
--control-url "$CONTROL_URL" \
| jq -c 'select(any(.evidence[]?; .guard_name == "secret-leak" and .verdict == false))
| {id, ts: .timestamp, server: .tool_server, tool: .tool_name, reason: .decision.reason}'A budget refusal is a kernel denial with no guard name. The kernel attaches a financial metadata block to a monetary denial, with cost_charged zeroed and attempted_cost set to what the call would have spent. Select on that block and read the reason for the limit that was hit.
chio receipt list \
--capability cap-workload-xyz \
--outcome deny \
--limit 200 \
--control-url "$CONTROL_URL" \
| jq -c 'select(.metadata.financial.attempted_cost != null)
| {id,
ts: .timestamp,
reason: .decision.reason,
attempted: .metadata.financial.attempted_cost,
remaining: .metadata.financial.budget_remaining,
total: .metadata.financial.budget_total,
currency: .metadata.financial.currency}'You should see one row per monetary denial, with the attempted cost beside the budget remaining. An exhausted call quota is a separate limit: its reason reads invocation budget exhausted for capability <id>, and it can carry no financial block, which is why the selector tests for the field.
4. Follow one agent or one capability
Combine a tool server and tool name with a time window, then keep the rows attributed to one agent. The subject key is the 64-character hex string at .metadata.attribution.subject_key; copy it from a CSV export (step 7), since one wrong character matches nothing.
AGENT=7b0f6f631f6e66207140ead0b6b2e9418916d2c4b3c7448ba5f7ed27f5c8d038
END=$(date -u +%s)
START=$((END - 7 * 86400))
chio receipt list \
--tool-server filesystem \
--tool-name write_file \
--since "$START" \
--until "$END" \
--limit 200 \
--control-url "$CONTROL_URL" \
| jq --arg a "$AGENT" -c 'select(.metadata.attribution.subject_key == $a)'You should see only receipts with explicit attribution metadata. Over HTTP, the agentSubject parameter also resolves agents through capability lineage. The command reads one page of at most 200 rows; a longer week needs the cursor loop in step 6.
To review a capability, collect its receipts and resolve its delegation chain. Together they connect recorded decisions to the capability's issuance history.
CAP=cap-abc123
# One page of this capability's receipts, in sequence order.
chio receipt list \
--capability "$CAP" \
--limit 200 \
--control-url "$CONTROL_URL" \
> receipts-$CAP.ndjson
# The delegation chain (root to leaf).
curl --fail-with-body -sS \
-H "Authorization: Bearer $CHIO_CONTROL_TOKEN" \
"$CONTROL_URL/v1/lineage/$CAP/chain" \
| jq '.[] | {subject: .subject_key, issuer: .issuer_key, depth: .delegation_depth, expires: .expires_at}'The lineage chain is the one response on these routes whose keys are snake_case: CombinedDelegationChainEntry carries no rename_all, so subject_key and delegation_depth are the wire names. The read routes list the two lineage reads.
5. Rank spend by tool and by agent
The analytics route groups recorded costs by tool and by agent. It selects groups by receipt count before applying groupLimit, so this recipe ranks the ten busiest tools by cost.
MONTH_END=$(date -u +%s)
MONTH_DATE=$(date -u +%Y-%m-01)
MONTH_START=$(date -u -d "$MONTH_DATE 00:00:00" +%s 2>/dev/null \
|| date -u -j -f "%Y-%m-%d %H:%M:%S" "$MONTH_DATE 00:00:00" +%s)
curl --fail-with-body -sS \
-H "Authorization: Bearer $CHIO_CONTROL_TOKEN" \
"$CONTROL_URL/v1/receipts/analytics?since=$MONTH_START&until=$MONTH_END&timeBucket=day&groupLimit=10" \
| jq '.byTool
| sort_by(-.metrics.totalCostCharged)
| .[:10]
| map({server: .toolServer, tool: .toolName, cost: .metrics.totalCostCharged, calls: .metrics.totalReceipts})'You should see up to ten tools, highest cost first. The same route groups by agent; this request reuses MONTH_START and MONTH_END and ranks up to 50 agents selected by receipt count.
curl --fail-with-body -sS \
-H "Authorization: Bearer $CHIO_CONTROL_TOKEN" \
"$CONTROL_URL/v1/receipts/analytics?since=$MONTH_START&until=$MONTH_END&groupLimit=50" \
| jq '.byAgent
| map({agent: .subjectKey,
calls: .metrics.totalReceipts,
spend_minor: .metrics.totalCostCharged,
denies: .metrics.denyCount,
compliance: .metrics.complianceRate})
| sort_by(-.spend_minor)'totalCostCharged is in minor currency units, summed without grouping by currency. Total spend only across records that share one known currency, and divide by 100 only once you know that currency is USD. For a global ranking, collect every receipt page and aggregate by tool.
complianceRate and reliabilityScore are omitted from a row whose denominator is zero, so read them with a // default. Both analytics requests need admin read authority; a tenant-scoped token gets 403.
6. Collect every page of a large result
Pagination is forward-only by cursor. The store gives each receipt an increasing seq; each page returns the last one as its cursor, and you pass it back as --cursor. The CLI writes that cursor to stderr as next_cursor=<seq> total_count=<n>, because seq is the store's row number and never appears in a receipt line.
$ chio --control-url "$CONTROL_URL" --control-token "$CONTROL_TOKEN" \
receipt list --limit 2 2>cursor.err 1>page.ndjson
$ echo "stdout: $(wc -l < page.ndjson) receipt lines"
$ echo "stderr: $(cat cursor.err)"stdout: 2 receipt lines stderr: next_cursor=2 total_count=3
Split the two streams and read the cursor out of stderr until a page comes back without one:
$ cursor=""
$ : > all-denies.ndjson
$ while : ; do
$ args=(--outcome deny --limit 1 --control-url "$CONTROL_URL" --control-token "$CONTROL_TOKEN")
$ [ -n "$cursor" ] && args+=(--cursor "$cursor")
$ # Receipts go to stdout; "next_cursor=<seq> total_count=<n>" goes to stderr.
$ page=$(chio receipt list "${args[@]}" 2>page.err)
$ [ -n "$page" ] && printf '%s\n' "$page" >> all-denies.ndjson
$ cursor=$(sed -n 's/^next_cursor=\([0-9]*\).*/\1/p' page.err)
$ [ -z "$cursor" ] && break
$ done
$ rm -f page.err
$ wc -l < all-denies.ndjson2
You should see the two denials collected one page at a time, with the loop stopping on the short page. Over HTTP the cursor is nextCursor in the response envelope. A loop that reads .seq off the last line gets null on page one and stops after a single page. A --limit above 200 is clamped to 200 with exit 0, so compare what you collected against total_count.
New records can arrive while you page, and retention can change the available history. A timestamp upper bound makes the reporting window explicit without creating a snapshot. Record the query and the collection time beside the export, and present it as what the query returned.
7. Export the records and verify them
Save complete receipts as JSON Lines with a redirect. For the month-so-far window, this saves one page of up to 200 records; use the loop from step 6 for more.
chio receipt list \
--since "$MONTH_START" --until "$MONTH_END" \
--limit 200 \
--control-url "$CONTROL_URL" \
> month-page.ndjsonFlatten a page into CSV for a spreadsheet, and keep the signed JSON beside it: a CSV row presents a record, and only the JSON carries the signature. The outcome column reads .decision.verdict with a // default, because a trace or advisory receipt has no decision. The same run against a local store:
$ echo 'id,timestamp,server,tool,outcome,cost_charged,agent'
$ chio --receipt-db ./receipts.db receipt list --admin-all \
| jq -r '[.id, .timestamp, .tool_server, .tool_name,
$ (.decision.verdict // ""),
$ (.metadata.financial.cost_charged // 0),
$ (.metadata.attribution.subject_key // "")] | @csv'id,timestamp,server,tool,outcome,cost_charged,agent "ed6c03fe092c83e54898702714592bc9e196a4120f58616840259b4bf4ce372f",1788610104,"*","run_command","allow",0,"ee7d2c513773e84d23ea9ade9bf0f7ffd9cd0c6c4545135ad0a441d1cb607fc3" "f0dea3dd23ccc35f7a15f6e753b3a095ba04ffa1f4fed79b1593f05666cc0736",1788610105,"*","read_file","deny",0,"e7e09911a3f1f8fbe85d5fe2859dcf4a3ffb56e792e79f097a609e2b9845486c" "406ad0272531e42e4fee6a503fd24e6a0ba8cc705db01852dcaf5fe0de9e227e",1788610107,"*","run_command","deny",0,"8aaa4bfda187af829dcfebd3d702fee1483a0eaaa22bf249be7ee60b3686ac6a"
Avoid (.decision | keys[0]) for the outcome: the decision is internally tagged, so it yields verdict on an allow and guard on a deny. The remote form writes the same columns:
echo 'id,timestamp,server,tool,outcome,cost_charged,agent' > receipts.csv
chio receipt list \
--since "$MONTH_START" --until "$MONTH_END" --limit 200 \
--control-url "$CONTROL_URL" \
| jq -r '[.id,
.timestamp,
.tool_server,
.tool_name,
(.decision.verdict // ""),
(.metadata.financial.cost_charged // 0),
(.metadata.attribution.subject_key // "")] | @csv' \
>> receipts.csvFor an external auditor, export an evidence package for the same window. It holds the filtered receipts, the capability lineage, the checkpoint roots that prove log integrity, and a manifest that hashes each of them; --policy-file adds the policy source and its metadata. The auditor verifies it on an air-gapped machine.
chio evidence export --since "$MONTH_START" --until "$MONTH_END" \
--policy-file ./policy.yaml --output ./pkg
# On the auditor's machine, with no network access:
chio evidence verify --input ./pkgYou should see the verifier report the package's integrity and proof coverage. Verification establishes that these records are intact and signed; apply your own signer trust and completeness requirements separately, since a filtered package covers what the query selected.
For continuous review, stream receipts into Splunk, Elasticsearch, or the SIEM your security team runs. The chio-siem library's ExporterManager opens a read-only connection to the receipt database, pulls with its own seq cursor, and fans out to registered exporters with backoff and a bounded dead-letter queue. A deployment embeds it as a library; SIEM Export sets it up.
Read more in Verify Receipts Offline, and in Receipt Dashboard for a console that runs live queries and evidence checks together.
Query routes, filters, and flags
Search for cursor, agentSubject, or cost-currency.
Read routes
The CLI returns NDJSON, one receipt per line. GET /v1/receipts/query on the trust-control service returns a JSON envelope with a page of receipts, totalCount, and nextCursor. ReceiptQueryClient from @chio-protocol/sdk wraps that route with typed parameters and an async-generator paginate(). The Receipt Query API defines the complete wire contract.
| Method | Path | Handler |
|---|---|---|
GET | /v1/ | handle_ |
GET | /v1/agents/{subject_key}/receipts | handle_ |
GET | /v1/ | handle_ |
GET | /v1/lineage/{capability_id} | handle_ |
GET | /v1/lineage/{capability_id}/chain | handle_ |
/v1/lineage without a capability id is a POST that records a snapshot, so a GET against it answers 405. The two readable lineage routes take the capability id in the path. GET /v1/agents/{subject_key}/receipts accepts only limit and cursor and is equivalent to passing agentSubject on the general query route.
Filters
Filters combine with AND semantics; an omitted filter leaves that dimension unconstrained. The HTTP route and the SDK accept agentSubject, which has no receipt list flag.
| Dimension | CLI flag | HTTP param | Values |
|---|---|---|---|
Tenant scope | --tenant <id> / --admin-all | from token | Local reads only |
Agent | none | agentSubject | Hex Ed25519 subject key |
Tool server | --tool-server | toolServer | Exact, e.g. filesystem, shell |
Tool | --tool-name | toolName | Exact, e.g. write_file |
Outcome | --outcome | outcome | allow, deny, cancelled, incomplete |
Capability | --capability | capabilityId | Exact capability token ID |
Time range | --since / --until | since / until | Unix seconds, inclusive |
Cost floor | --min-cost | minCost | Minor units of cost_charged |
Cost ceiling | --max-cost | maxCost | Minor units of cost_charged |
Cost currency | --cost-currency | costCurrency | ISO 4217 code |
Page size | --limit | limit | Default 50, cap 200 |
Cursor | --cursor | cursor | Last seq seen |
- Tenant scope. Required whenever
--receipt-dbis used, and omitting both flags refuses the read. Remote--control-urlreads derive scope from the bearer token and reject both flags. - Agent. Resolved from receipt attribution metadata when present, otherwise through capability lineage.
- Cost band. A floor or ceiling requires
--cost-currencyand excludes receipts without financial metadata. The CLI parser and the server-side query builder each refuse a band without a currency. - Cursor. Pagination is forward-only and ordered by
seq. A response withoutnextCursor, or with it null, is the last page for the filter set;totalCountcounts every match, independent oflimitandcursor.
$ chio --receipt-db ./receipts.db receipt list --admin-all --min-cost 0error: the following required arguments were not provided: --cost-currency <COST_CURRENCY> Usage: chio receipt list --cost-currency <COST_CURRENCY> --admin-all --min-cost <MIN_COST> For more information, try '--help'.
Guard names
These are the names guard.name() returns, which is what lands in evidence[].guard_name. They are kebab-case, distinct from the snake_case rule-block keys in a policy: the secret_patterns block compiles to the secret-leak guard, and path_allowlist compiles to path-allowlist. Every kernel-built deny receipt sets decision.guard to kernel, and its reason names the pipeline.
agent-velocity, behavioral-sequence, browser-automation, code-execution, computer-use, content-review, data-flow, egress-allowlist, embedding-anomaly, forbidden-path, input-injection-capability, internal-network, jailbreak, mcp-tool, memory-governance, patch-integrity, path-allowlist, prompt-injection, remote-desktop-side-channel, response-sanitization, secret-leak, shell-command, velocity.
Decision encoding
Decision carries #[serde(tag = "verdict")] in crates/core/chio-core-types/src/receipt/decision.rs, so the verdict, reason, and guard sit side by side under decision. A trace or advisory receipt has no decision object.
chio receipt list flags
| Flag | Value | Scope |
|---|---|---|
--capability | Capability ID | One capability token |
--tool-server / --tool-name | Exact names | A server, optionally one tool |
--outcome | allow, deny, cancelled, incomplete | One verdict |
--since / --until | Unix seconds | Inclusive time window |
--min-cost / --max-cost | Minor units | Cost band; needs a currency |
--cost-currency | ISO 4217 code | Required with a floor or ceiling |
--limit / --cursor | Count / seq | Page size (default 50, cap 200) and resume point |
--control-url / --control-token | URL / token | Trust-control service and bearer token |
--receipt-db | SQLite path | Local store |
--tenant / --admin-all | Tenant ID / none | Local read boundary, mutually exclusive |