Skip to content
Notes on execution evidence

Thirty days in observe mode: what your agents actually do

9 minute readSealr Team

  • observe mode
  • exposure report
  • evidence
  • coverage
  • ai agents

Deploying the Recorder read-only across three interception surfaces, what it records, and what the Exposure Report establishes after thirty days — including what it cannot establish.

Ask your platform team how many operations your agents executed against production last month, how many were destructive, and which ones trace back to a named person. In most organizations the honest answer is three words: we don't know. Application logs are mutable, partial, and scattered across the agent, the MCP server, the database, and CI jobs; none of them was built to answer the only question that matters after an incident — what was executed, under which rule, approved by whom.

Observe mode exists to turn that missing answer into a number. You put the Recorder in the execution path, read-only, you change nothing about how the agents behave, and you let thirty days of real traffic write the finding.

Putting a recorder in the path without changing anything

The Recorder is a single Rust binary. In observe mode it computes a verdict and writes a record; it never enforces anything. A BLOCK stays an annotation. Three interception surfaces, no change to agent code.

MCP proxy. In the agent's MCP configuration, you replace the target server with the Recorder, which spawns the real server as a child process and relays all traffic:

{
  "mcpServers": {
    "prod-db": {
      "command": "sealr",
      "args": ["proxy", "mcp", "--name", "prod-db", "--resource", "db:prod-main",
               "--", "npx", "-y", "@modelcontextprotocol/server-postgres", "postgres://..."]
    }
  }
}

Messages that are not tools/call pass through untouched; on an ALLOW verdict the wrapped server behaves byte-for-byte as if it had been called directly.

Agent hooks. Claude Code exposes PreToolUse and PostToolUse; the Recorder plugs straight in, without --enforce:

{
  "hooks": {
    "PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "sealr hook pre-tool-use" }] }],
    "PostToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "sealr hook post-tool-use" }] }]
  }
}

CLI wrapper. For CI and one-off use, you prefix the command:

sealr run --resource db:prod-main   -- psql -h prod -c "..."
sealr run --resource cloud:aws-prod -- terraform show -json tfplan
sealr run --resource repo:acme/api  -- git push origin main

Recognized commands (psql -c, terraform, git push) go to the matching guard; everything else becomes a generic_exec record with argv, exit code, duration, and stdout/stderr digests — never their content.

Surface What it sees What it does not see
MCP proxy the wrapped server's tools/call traffic, request and response MCP servers that are not wrapped
Agent hooks every tool call from that agent, with its outcome anything that does not go through that agent
CLI wrapper argv, cwd, exit code, duration, Terraform plan anything launched without the prefix
GitHub App pushes, force-pushes, ref deletions diff content (V1)

The added-latency budget in observe mode is 3 ms p99 per intercepted call, decided locally: no DNS, no TLS handshake, no network wait on the decision path. And the default is fail-open: if the Recorder can neither evaluate nor record, the operation proceeds and a signed coverage_gap record is written. A safety tool that causes outages gets uninstalled.

What gets recorded, and what never leaves your infrastructure

Every intercepted operation produces a record chained to its predecessor. Schematically:

{
  "record_type": "operation",
  "seq": 41208,
  "ts_wall": "2026-07-14T09:12:44.310Z",
  "subject": { "agent_kind": "claude-code", "agent_session": "01J…",
               "human_principal": "[email protected]", "attribution": "attributed" },
  "action": { "integration": "sql", "operation": "psql -c", "verb_class": "delete",
              "target_resource": "db:prod-main" },
  "payload_commitment": { "alg": "blake3-salted", "commitment": "9f2c…" },
  "verdict": { "decision": "BLOCK", "mode": "observe", "risk_class": "CRITICAL",
               "reason_codes": ["SQL_UNBOUNDED_WRITE"], "guard": { "name": "sql", "version": "1.0.0" } },
  "prev_hash": "7ab1…", "record_hash": "c40e…"
}

Three things deserve attention. Structure without literals: a DELETE FROM users WHERE email = '[email protected]' becomes a WHERE email = <string> skeleton, and that is not a display choice — the types of the projection sent to the Console make the literal structurally inexpressible. The cryptographic commitment: a salted digest stands in for the payload, with the salt staying client-side, which later lets you prove that a given payload matches the record. And mode: observe: the verdict was computed, not applied.

The stream also records what the fleet could not do: Recorder starts and stops, configuration and policy changes, spool shedding, clock anomalies, reconciliation misses. All of it is BLAKE3-chained, sealed by Ed25519-signed checkpoints at least every 1,000 records or every 10 seconds, countersigned by the Console, then anchored with RFC 3161 timestamps and an eIDAS qualified timestamp on the daily root.

What the Exposure Report answers after thirty days

The Console then produces the Exposure Report. It does not tell you what "agents in general" do; it tells you what yours did, and it answers four questions plus one.

How many operations did your agents execute? The total volume, broken down by surface, by integration, by protected resource, and by team — enrollment labels (team:payments) travel with the Recorder, not with the resource.

How many were destructive against production? Not an estimate: a count of operations classified by deterministic guards that parsed the operation itself. SQL Guard parses with the real PostgreSQL grammar and separates a bounded write from a write with no WHERE or a tautological WHERE; Terraform Guard counts the delete and replace actions in the plan JSON; Git Guard reads argv, refs, and secret patterns.

How many would have required approval, or would have been blocked? That is the simulation: thirty days of verdicts computed and never enforced. You read what enforce mode would have produced on your own history, including the approval queue it would have created — that is, the real load on your on-call rotation.

How many are untraceable to any human principal? Every record carries the best available attribution and its quality level: attributed, inferred, or unattributed. The number that matters is the third: the operations nobody can claim, the ones an auditor or opposing counsel will look for first.

And where the gaps are. Coverage is published as two figures that are deliberately not summed: gaps the fleet admitted (restarts, spool shedding, guard errors) and events it never saw — typically the pushes GitHub recorded that no Recorder did. Every figure drills through to the underlying records, because a figure you cannot open is a claim, not evidence.

The report measures only what the fleet saw

This has to be said plainly, because it is the most credible part of the whole thing. The evidence produced is tamper-evident, not tamper-proof: it proves the integrity, ordering, timing, and origin of the recorded stream. It cannot prove that unrecorded events did not happen.

Coverage is a deployment property, not a cryptographic one. Anyone who opens a direct connection to the database, or pushes from a machine with no Recorder, is not in the stream, and no signature changes that. What the system does is make the bypass visible: the GitHub App ingests events server-side, reconciliation matches them against the fleet's records, and a push with no match becomes a timestamped coverage_gap with repository, ref, and actor. So the guarantee is not "nobody can bypass the Recorder" — it is "bypassing the Recorder is visible".

Why your own traffic beats a vendor benchmark

A vendor benchmark asserts that some percentage of agent operations is destructive "across the industry". Nobody recognizes their estate in an average, and nobody signs off a budget on one. The Exposure Report, by contrast, is written by the customer's own traffic, with their resource names, their teams, and their agents. It produces a sentence of the form "your agents executed N operations; M were destructive against production; K would have required approval; J are untraceable to any human principal". The values are yours; we do not publish any others.

The objections change in kind. You are no longer arguing about how representative a study is: you open the row, you read the record, you find that the DROP TABLE on 14 July at 09:12 came from an agent session nobody claims. It also happens to be the format the incoming European obligations call for — logging and retention under the AI Act, evidence disclosure under the revised Product Liability Directive — but the argument holds without them.

Moving from observe to enforce without breaking production

The point of observe mode is not to stay there. The move happens resource by resource. First you register the protected resources — db:prod-main, repo:acme/api, cloud:aws-prod — because policies bind to them by pattern. Then you write the policy and simulate it against the thirty days of real history before publishing: how many operations would have been blocked, how many approvals requested, on facts rather than assumptions. Then you enable a single resource.

schema: sealr/policy/v1
name: prod-databases
bindings:
  - resources: ["db:prod-*"]
    mode: enforce                    # observe | enforce
    critical: true                   # a guard error fails CLOSED here
    stale_action: keep_enforcing     # if the policy stops being refreshed
    on_approval_unavailable: block   # if nobody can approve
rules:
  - id: block-unbounded-writes
    match: { guard: sql, reason_any: [SQL_UNBOUNDED_WRITE, SQL_TAUTOLOGY_WHERE] }
    decision: BLOCK
  - id: approve-destructive-prod
    match: { guard: sql, risk_at_least: HIGH }
    decision: REQUIRE_APPROVAL
    approval: { approvers_group: dba-oncall, min_approvers: 1, ttl: 1h, scope: operation }
  - id: default
    match: {}
    decision: ALLOW

Four properties make the switch survivable. The enforce-mode latency budget is 15 ms p99, verdict and durable write included, with a 25 ms timeout per guard. Failure stays open by default, except on resources marked critical: true. Policies are signed and evaluated from a local cache: an unreachable Console does not suspend decisions, and stale_action describes what happens after 24 h without a refresh. Finally, the approval wait is bounded, and on_approval_unavailable states what happens when nobody answers — block by default, because an operation important enough to require a human should not proceed just because a network went down. A break-glass path, recorded and alerted, remains available for emergencies.

Only then do you widen: second resource, second team, CI. Every mode change is itself a record.

What thirty days leave you with

Not a guarantee. A baseline measured on your own traffic, a list of named and dated gaps, and a policy already tested against history before it blocked anything. The rest — judging whether the number of unattributable operations is acceptable, deciding which resource goes into enforce first — remains an engineering and governance decision no product will make for you.

To see the report as it comes out, request a demo: we walk through it on test data, and we show the pages that display what is missing too.

Also worth reading

Logs are not evidence

A log index proves what your pipeline chose to keep. Here is what a hash-chained, checkpointed, countersigned and timestamped stream adds — and where the guarantee honestly stops.

A guardrail must parse the operation, not the prompt

Prompt filtering polices an assumed intent. A deterministic guardrail reads the operation actually emitted: PostgreSQL grammar, Terraform plan JSON, git argv. How Sealr returns a local verdict in a few milliseconds, and why the verdict is itself recorded.