How it works
The path of one agent operation from interception to signed evidence, and what does and does not cross the boundary to the Console.
Sealr sits in the execution path. An agent's operation reaches the recorder before it reaches your database, your cloud account or your remote. The recorder parses it, decides on it, and writes a record about it. Everything else in the product is downstream of that one path.
This page follows a single operation end to end.
The operation
An agent running under Claude Code proposes a SQL statement:
DELETE FROM users WHERE created_at < now() - interval '30 days'The Claude Code hook is configured to call the recorder before the tool runs:
sealr hook pre-tool-use --enforceThe hook reads the tool call as JSON on stdin and writes a decision as JSON on stdout. The
same pipeline serves the other two surfaces — sealr proxy mcp -- <server command> wraps an
MCP server over stdio, and sealr run -- <command> wraps any command line.
The recorder extracts the operation
The surface adapter pulls the operation bytes out of the tool call and routes them to a guard,
either from an explicit --resource hint or by inferring the integration from the tool name
and the payload shape. SQL text goes to the SQL guard, terraform show -json output to the
Terraform guard, git argv to the git guard. Anything unrecognised is still recorded, as a
generic_exec record with no classification.
The guard parses the real grammar
The SQL guard parses the statement with libpg_query — the actual PostgreSQL parser, not a
regex. It sees a DELETE with a WHERE clause, evaluates the clause for tautology at the AST
level, and classifies it as a bounded write. Had the WHERE been absent, or 1=1, the class
would be unbounded write and the base risk CRITICAL.
Guards are pure libraries: no network, no filesystem, no clock. Each runs inside a pipeline
wrapper with a 25 ms CPU budget and a 1 MB input cap. Input over the cap is not parsed and
carries INPUT_TRUNCATED; a panic is caught and becomes a guard_error record. Nothing
non-parsing is silently allowed — unparseable SQL yields SQL_PARSE_ERROR and, by default,
REQUIRE_APPROVAL.
The guard returns a decision, a risk class, reason codes, and a structure: statement kinds,
schema-qualified relations, and a WHERE skeleton with typed placeholders such as
created_at < <timestamp>. Literals do not appear in the structure.
Policy decides locally
The recorder holds a signed policy bundle in a local cache. Evaluation is an in-process
function call over that cached bundle: no network request is on the decision path, and the
elapsed time is written into the record as eval_ms.
Policy binds rules to resources and turns the guard's classification into one of ALLOW,
WARN, REQUIRE_APPROVAL or BLOCK. Each binding also carries a mode. In observe mode the
recorded verdict is the guard's own decision and nothing is stopped. In enforce mode the
policy decision is recorded, and BLOCK makes the hook return deny — the tool never runs.
When sync is enabled the recorder refreshes the bundle from the Console at process start. That
refresh is best-effort: an unreachable Console does not stop the recorder, it falls back to the
cached signed bundle. A cache older than 24 hours is stale, and each binding declares its own
stale_action (for example fail_closed) for that case.
The record is built, chained and spooled
The engine assembles one record. The payload — the SQL text — is never stored. It is reduced to
a salted BLAKE3 commitment. The structural detail from the guard goes into action_detail,
which stays on your side of the boundary.
The record is canonicalised with JCS (RFC 8785), hashed with BLAKE3-256, and its prev_hash is
set to the hash of the previous record in the stream. It is appended to an NDJSON segment file
in the spool. In enforce mode the write is fsync'd before the decision is returned, so a
committed record survives a crash at the moment of the operation.
A checkpoint signs a Merkle root
Individual records are not individually signed — that cost is paid once per batch instead. The
recorder emits a CheckpointNote every 1,000 records and at clean shutdown. The note carries
the sequence range, the chain head hash, a Merkle root over the covered records' hashes
(RFC 6962 leaf and node prefixes, BLAKE3), the tree size, and a link to the previous
checkpoint. It is signed Ed25519 with the recorder's identity key.
A record is attested once a valid checkpoint covers it.
Metadata syncs to the Console
If the recorder is enrolled, it pushes the signed checkpoints and a projection of the
records to the Console over gRPC, authenticated with the recorder's own credential. The Console
verifies the recorder signature, checks that seq_from equals its stored head plus one, and
only then countersigns — recording its view of how far the stream has advanced.
Sync is best-effort. A failure is logged, never fatal; the records are already durable locally and sync resumes later.
The recorder / Console split
The recorder is where evidence is produced and where decisions are made. The Console is where metadata is aggregated, policy is authored, approvals are routed and checkpoints are countersigned. The split is deliberate: the Console can be unreachable, and the recorder still records and still enforces.
The recorder also runs with no Console at all. sealr init with no endpoint generates a local
identity chain so that exported bundles verify fully offline.
What crosses the boundary
Exactly one record shape may leave the customer boundary: ConsoleSafeRecord. It is a separate
type with no fields for the sensitive parts, so leaking them is structurally impossible rather
than merely forbidden. Every string it carries is sanitized — control characters stripped,
length capped at 512 characters.
| Projected to the Console | Kept local (Ledger only) |
|---|---|
| Ids, sequence, dual timestamps, record type | action_detail — the statement skeleton, resource addresses |
| Recorder identity, host fingerprint, platform | payload_ref — vault location and key reference |
| Subject: agent kind, session, principal, attribution quality | The payload itself, in any form |
| Action: integration, operation, verb class, target resource, labels | |
payload_commitment — the salted hash, never the payload | |
Verdict: decision, mode, risk class, policy version, rule and reason codes, guard, eval_ms | |
prev_hash, record_hash |
So the Console can tell you that a bounded_write against db:prod-main was allowed by rule
x at 14:02, and can prove that record's place in the chain. It cannot tell you which rows the
WHERE clause matched, because it was never sent that.
The commitment makes this reversible on your terms rather than not at all: holding the payload and its salt, anyone can prove the payload matches the record. See the evidence format for how that works and what V1 currently retains.
Where the path does not apply
A recorder sees what passes through it. An agent invoked outside the hook, an engineer using
psql directly, a script with its own database credentials — none of those produce records,
and no property of the chain changes that. Coverage is a deployment property, and the
coverage reconciliation exists to make bypass visible rather than to
claim it is impossible.
Read what Sealr proves next.