Sealrdocs
Concepts

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 --enforce

The 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 ConsoleKept local (Ledger only)
Ids, sequence, dual timestamps, record typeaction_detail — the statement skeleton, resource addresses
Recorder identity, host fingerprint, platformpayload_ref — vault location and key reference
Subject: agent kind, session, principal, attribution qualityThe 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.

On this page