Repository navigation
Concepts Evidence Format
Tracekit's evidence is a log of signed records. Each record is linked to the one before it, every record is a leaf in
a Merkle tree, and signed checkpoints of that tree are cosigned by witnesses. Content the agent handled is kept out of
the records as keyed commitments. One run, with the proofs needed to check it, exports as a .tkb bundle. This page
explains each layer and why it's there; the byte-level specification is format v2.
flowchart TB
subgraph run ["One run's chain"]
R1[record run_seq 0] --> R2[record run_seq 1] --> R3[record run_seq 2]
end
R1 & R2 & R3 --> L["Merkle tree<br/>(every record in the log is a leaf)"]
L --> C["Checkpoint<br/>size + root, signed by the log key"]
C --> W["Witness cosignatures"]
A record is one line of JSON with exactly six keys:
{"v": 2, "event": {...}, "hash": "sha256:...", "alg": "ed25519", "kid": "sha256:...", "sig": "..."}-
eventis the content: a decision, a result, an approval, a gap. Its fields follow a closed JSON Schema (tracekit.event.v2.json), apart from one deliberately open slot,ext(compatibility). -
hashis SHA-256 over the event in canonical JSON. -
kididentifies the signing key: the SHA-256 of its public key, which also binds the algorithm. -
sigis the Ed25519 signature described below.
Every event says where it came from (source: the hook, the SDK, a gateway, the signer itself, an import) and, for tool
calls captured outside the agent's process, its tier. Fields only the signer may set (sequence numbers, isolation,
gaps, tamper records) can't be supplied by a client.
The signature doesn't cover the event body directly. It covers a small canonical object that ties the event's hash to its place in both chains:
JCS({alg, kid, log_id, seq, prev_hash, tenant, run_id, run_seq, run_prev_hash, hash, "t":"tracekit.record.v2"})
- The body is bound through
hash, and the record's position through the two chains, so a genuine record can't be moved to another place in the log or into another run. -
tis a domain-separation tag. Checkpoints, approvals, key certificates, key retirements and exports each sign under their own tag, so a signature made for one purpose can't be replayed as another. - Verification is strict Ed25519: a canonical
S, no small-order or non-canonical public keys. Thealgmust be in the verifier's allowed list and must be the key's own algorithm, which rules out algorithm-confusion attacks.
A signature only checks out if signer and verifier serialise the same bytes. Tracekit uses JCS (RFC 8785), and parses
strictly first: it refuses duplicate keys, NaN and Infinity, integers outside ±(2^53−1), lone surrogates, invalid UTF-8
and nesting deeper than 128 levels. Arguments that fail are denied (TK-ARGS-INVALID) rather than recorded ambiguously.
Test vectors pin the encoding for other implementations: tests/vectors.
| Chain | Fields | What it protects |
|---|---|---|
| The log chain |
seq, prev_hash
|
The order of every record in the log |
| The run chain |
run_seq, run_prev_hash
|
The order and completeness of one run's records |
Only the signer's single writer assigns these, so the agent can never leave a hole or reorder records. Because each
event contains run_prev_hash, the last record of a run commits, through the chain, to every record before it.
Every record's hash is a leaf of an RFC 6962 Merkle tree (leaf = SHA-256(0x00 ‖ hash), node =
SHA-256(0x01 ‖ left ‖ right); the prefixes stop a node being passed off as a leaf). The tree gives two kinds of
proof:
- Inclusion proofs show a record is in the tree a checkpoint signed. A bundle needs them only for each run's first and last record (plus key records and records the registry points to): the run chain covers everything between.
- Consistency proofs show a newer tree extends an older one. Witnesses use them to refuse forks and rollbacks.
The tree is stored as C2SP tlog-tiles, which is storage, not evidence.
A checkpoint is a C2SP signed note: the log's origin, tree size and root hash, signed by the log key.
tracekit.local/<log_id>
<tree size>
<base64 root hash>
— <key name> <base64 key id and signature>
A witness cosignature is a further line carrying the witness's own timestamp. Because the format is a standard, existing witness software (omniwitness, litewitness) can cosign Tracekit logs without knowing anything about agents.
Records are meant to be shared, so they don't contain the agent's arguments or results. They contain keyed commitments:
args digest = sha256(JCS({"tool", "args"}))
salt = HMAC-SHA256(args_salt.key, a label unique to the record)
commitment = "hmac-sha256:" + HMAC(salt, args digest)
A plain hash of a short value (a PIN, "yes", an account number) could be guessed by trying values; a keyed commitment
can't. Each record has its own salt, so tracekit signer reveal --record N opens exactly one record for an auditor and
nothing else. Verification never needs a salt, and the policy still decides on the real arguments: only the records
hold commitments. What still shows (timing, sizes, counts): privacy.
To keep the content itself for investigations, turn on the vault (content: {retain: true} in the signer config).
Content is encrypted with AES-256-GCM under a key per run, bound to its record so a blob can't be moved to another one.
Every read needs a grant agents never get and writes a signed content.accessed. Erasing a run (content_erase, or
the retention period) deletes its key and writes content.erased. Bundles never contain vault content.
| Key | Signs | Notes |
|---|---|---|
| Log key | Checkpoints | Stable for the life of the log. A file, or AWS KMS |
| Record keys | Records | Announced in the log (signer.epoch) and ended with key.retire{kid, last_seq}. Validity is positional: a record signed after the key's last sequence fails |
| Issuer certificates (experimental) | Short-lived record keys | Each certificate is a leaf in the issuer's own log, cosigned by a witness before the signer gets it, so a secretly issued key can't verify (issuer) |
| SLH-DSA (experimental) | A second checkpoint line | A post-quantum signature (FIPS 205) beside Ed25519. Records stay Ed25519 |
The signer also holds HMAC and encryption keys for run tokens, commitments, approval copies and the vault. The agent holds none of them.
A bundle for one run can't show runs that were left out. Each tenant therefore has a registry log with a fixed-size
leaf for every run registered, run finalised, key retired and tenant-wide gap. A run-set is a range of registry
leaves between two registry checkpoints with a consistency proof: it proves which runs exist in that window, so a
deleted run, a second run.final or a hidden key retirement shows.
A .tkb file is a zip in one strict layout: the run's records, the key records, inclusion proofs, checkpoints,
optional Rekor and timestamp anchors, the policy snapshots its records cite, and registry files for a run-set. A
manifest lists every file's SHA-256 and the verifier version the records need, but is only an index. Anything outside
the layout (comments, zip64, extra fields, symlinks, sizes over the limits) makes the bundle UNUSABLE BUNDLE.
Two things are deliberately missing: verification code (the party being checked must not supply the checker) and the trust configuration (the person verifying brings their own).
Evidence has to stay checkable for years. Formats only grow, each format's verifier is frozen, and the test bundles
for old formats are never regenerated. Schemas are closed, so an older verifier would otherwise reject a record type
it has never seen; instead each bundle names the minimum verifier version it needs, and an older verifier reports
UNVERIFIABLE (needs tracekit >= x), never FAILED. From 1.4.0 that report also prints the commands that verify the
bundle: uvx, npx or the browser page.
What each version added:
| Version | Added |
|---|---|
| 1.1.0 |
tracekit why findings, content vault records, observed inputs, provenance on decisions |
| 1.2.0 | Agent approvers, containment (incidents, holds, quarantine, suspensions), effect.observed, the coverage matrix, more gap kinds |
| 1.3.0 |
reconcile.result_mismatch and the result_unchecked gap (verified tool results) |
| 1.4.0 |
constraint.added and the constraint_refused gap (live constraints), review records with cites (signed findings), kernel sensor and OpenShell fields on effect.observed and capture.gap, said_only on model exchanges, the ext slot, and a run exported in parts (past 64 MiB of records) |
The ext slot. From format 1.4.0, an addition an older verifier can leave unread doesn't need a new version. Any
record may carry ext, an object of names to strict JSON values, and a record of type ext holds data: {name, ...}.
They are signed, chained and included like every other record, so editing one still fails verification. A verifier
from 1.4.0 interprets the names it knows (today amended, the fields an LLM gateway changed in a request, and
openshell.policy, OpenShell's sandbox policy changes); for any other it prints a not interpreted warning, and the
bundle stays VERIFIED. A name may go in ext only if every line an older verifier prints stays true without it;
anything a verifier must read to judge a run stays critical. The full table:
format v2 §9.
Format v2 · privacy · issuer · records and signatures:
tracekit/format/ · bundle export: tracekit/bundle_v2.py
Previous: The life of a tool call · All concepts · Next: Verification and assurance
Generated from docs/ at v1.4.1. Edit the docs in the repository, not here.
Get started
- Quickstart: the v2 signer
- Quickstarts
- Quickstart: Claude Agent SDK
- Quickstart: a custom agent
- Quickstart: LangChain and LangGraph
- Quickstart: MCP client
- Quickstart: OpenAI Agents SDK
- Coding agents other than Claude Code
- tracekit doctor
- Walkthrough: Tracekit end to end
How it works
- How Tracekit works
- Overview
- The life of a tool call
- The evidence format
- Verification and assurance
- Witnesses and monitoring
- Policy
- Approvals
- Detection and containment
- Capture and completeness
- Custody
- Provenance and tracekit why
- Deployment and isolation
- Storage, durability and performance
- Integrations
- Security model
- Architecture
- tracekit why: architecture
Integrations
- Integrations
- Adapters
- OpenTelemetry
- Remote clients
- Kubernetes agent-sandbox
- agentgateway
- agentsh and Tracekit
- Browser Use
- Claude Agent SDK
- Claude Code's sandbox
- Codex's sandbox
- CrewAI
- Docker MCP Gateway
- E2B
- Falco
- Gemini CLI's sandbox
- Google ADK
- Haystack
- LangChain v1
- LiteLLM
- LlamaIndex
- MCP client
- microsandbox
- Microsoft Agent Framework
- OpenAI Agents SDK
- NVIDIA OpenShell
- Pydantic AI
- sandbox-runtime's proxy on its own
- Strands Agents
- Tetragon
- Vercel AI SDK
- Wassette
Policy and approvals
- Policy reference
- Approvals
- Findings
- Reading the model's reasoning
- Incidents and containment
- Webhooks
Deploy and operate
- Deploying the signer
- The signer as a container
- The signer on Kubernetes
- The compose stack
- Identity
- Tool executor
- MCP gateway
- LLM gateway
- Egress observation
- Witnesses
- Log monitor
- Record-key issuer
- Signer observability
- Viewer
- tracekit why: causal investigation and replay
- Migrating
- Platforms
Verify the evidence
- Reading a v2 verify report
- Evidence format v2
- Auditor guide
- Sample evidence bundle
- Signing
- What Tracekit guarantees
Security and privacy
- Threat model: the v2 signer
- Threat model: the v1 laptop signer
- HTTP security checklist
- Threat-model review packet
- Privacy
- Known limits and trade-offs
- What's stable
Compliance
- Compliance mapping
- EU AI Act
- ISO/IEC 42001
- NIST AI RMF and the Generative AI Profile
- OWASP Top 10 lists for LLM and agentic applications
Evaluation
Demos
- Demos
- Agent approvers within bounds
- Completeness and tamper evidence
- Detection and containment
- Network coverage: an egress observer
- Guardrails: allow, deny, or ask a person
- MCP tools, gated and recorded
- Prompt injection held by provenance
- Why: which input caused the action
Help