Sealrdocs
Installation

Agent surfaces

Attach Sealr to an MCP server, to Claude Code hooks, or to a wrapped CLI command, and choose which one to enforce on.

Sealr does not hook into the model. It attaches to the three places where an agent's intent turns into an operation: the MCP server it calls, the tool-use hook the agent harness fires, and the command line it shells out to.

Every surface below assumes the recorder is initialized on the machine. Run sealr init (offline) or sealr enroll (connected) first — without it, each command exits with recorder is not initialized. See the quickstart.

MCP stdio proxy

Configure sealr in place of the MCP server. It spawns the real server as a child process and relays stdio between the agent and the server.

.mcp.json
{
  "mcpServers": {
    "prod-db": {
      "command": "sealr",
      "args": [
        "proxy", "mcp",
        "--enforce",
        "--name", "prod-db",
        "--resource", "db:prod-main",
        "--",
        "npx", "-y", "@modelcontextprotocol/server-postgres",
        "postgresql://[email protected]:5432/app"
      ]
    }
  }
}

Everything after -- is the target server, launched exactly as you would have launched it yourself. The database URL stays in the MCP config; Sealr never opens a connection of its own.

For an ALLOW verdict the wrapped server's behaviour is byte-identical. The request line is forwarded unchanged and the response is relayed back unchanged. Only tools/call requests are inspected at all — initialize, tools/list, notifications and every other method pass through untouched.

For a BLOCK verdict in enforce mode, the request is never forwarded. Sealr answers the agent with a protocol-correct JSON-RPC error carrying the same request id:

{
  "jsonrpc": "2.0",
  "id": 7,
  "error": {
    "code": -32000,
    "message": "Blocked by Sealr: SQL_UNBOUNDED_WRITE",
    "data": { "sealr": true, "verdict": "BLOCK" }
  }
}

What the guard reads: if the tool arguments contain a query, sql, statement, command or cmd string, that string is the operation and goes to the matching guard. Otherwise the whole arguments object is serialized as JSON and the integration is inferred from the tool name. So postgres_query({sql: "DELETE FROM users"}) is parsed as PostgreSQL; a tool whose arguments carry SQL under some other key is recorded but evaluated by the generic guard, which allows everything.

--resource is what binds the traffic to a policy. Without it, records land on mcp:<name> — usable, but your policy bindings have to name that instead of db:prod-main. Set SEALR_PRINCIPAL in the agent's environment to attribute the session to a human.

Claude Code hooks

Hooks cover the agent's own tools — Bash, Edit, Write — which never pass through an MCP server.

.claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [{ "type": "command", "command": "sealr hook pre-tool-use --enforce" }]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [{ "type": "command", "command": "sealr hook post-tool-use" }]
      }
    ]
  }
}

sealr hook pre-tool-use reads the hook JSON on stdin and prints a decision on stdout:

{ "decision": "deny", "reason": "Blocked by Sealr: SQL_UNBOUNDED_WRITE" }

The reason string renders in the agent UI, so the agent sees the reason code and can choose a different approach instead of retrying blindly. The hook process itself always exits 0 — the decision is the JSON, not the exit code.

Drop --enforce and the hook records every tool call and returns allow every time. That is the observe-only configuration.

Two limits worth knowing before you rely on this surface:

  • Operations are bound to tool:<tool_name> (for example tool:Bash) unless the hook payload carries an explicit resource field, so write your bindings against that.
  • Classification is by tool name and payload shape. A Bash call running git push --force is recorded, but it reaches the generic guard, not the git guard, because neither the tool name nor the payload identifies it. To get real classification on shell commands, have the agent call them through sealr run (below).

sealr hook post-tool-use currently accepts the payload and exits 0 without writing an outcome record. It is wired up so the configuration does not change later; today the evidence from this surface is the PreToolUse record alone.

CLI wrapper

sealr run records any command and guards the ones it recognizes.

sealr run --resource db:prod-main -- \
  psql -h db.internal -U agent -c "DELETE FROM sessions WHERE last_seen < now() - interval '90 days'"

sealr run --enforce --resource cloud:aws-prod -- terraform apply tfplan

sealr run --enforce --resource repo:acme/api -- git push --force origin main

Recognition is argv-based and deliberately narrow:

CommandWhat the guard sees
psql -c / --command / --command=the SQL text, parsed with the PostgreSQL grammar
terraform / tofuthe full argv, not the plan
git with a push argumentthe full argv
anything elsenothing — recorded, not evaluated

The Terraform guard analyzes terraform show -json plan output, but sealr run passes it argv, not a plan. So a wrapped terraform apply yields TF_HCL_UNANALYZED (WARN) unless the argv contains a state-manipulation subcommand — state rm, state mv, import, taint, untaint, force-unlock, apply -replace — which produces TF_STATE_COMMAND and REQUIRE_APPROVAL. To evaluate a real plan today, feed the JSON to sealr policy test policy.yaml --integration terraform --input "$(cat plan.json)" --resource cloud:aws-prod. That is a dry-run against a policy file on disk, it records nothing, and it always exits 0 — so it cannot gate a job by exit code on its own.

Unrecognized commands are not ignored — they become a generic_exec record holding the redacted argv, the exit code, the wall duration, and BLAKE3 digests of stdout and stderr. Never the output content, and never the raw command line. You get proof that the command ran and what it returned, without the recorder becoming a place your secrets accumulate.

sealr run passes the wrapped command's exit code through unchanged, with two exceptions: 20 when the operation was blocked (or an approval was denied or unavailable), and 127 when the command could not be spawned at all.

Choosing a surface

SeesCan stop itRuns where
MCP proxyevery tools/call to that serveryes, before the server sees itthe agent's machine, per MCP server
Claude Code hooksevery tool call the agent makesyes, the agent gets a denythe agent's machine, per project
CLI wrapperone commandyes, before execanywhere, including CI

They compose, and you generally want more than one. The proxy is the highest-fidelity surface — it parses real SQL — but it only covers agents that use MCP. Hooks cover the agent's own tools but classify weakly. The wrapper covers scripts, humans and CI, which is where the credentials that actually matter usually live.

An agent that reaches a database by some path you did not wrap produces no records at all. Coverage is a deployment property, not something the recorder can assert for you.

Observe before you enforce

Every surface takes --enforce. Without it, nothing is ever stopped: the operation runs, the verdict is computed and recorded in observe mode, and you can read what would have happened.

Deploy every surface without --enforce. Let it run for a week of real agent work.

Read the recorded verdicts. Find the operations that carry a BLOCK decision in observe mode — those are exactly what enforcement would have stopped, including the false positives you have not thought of yet.

Turn on --enforce where the blast radius is smallest and the credentials are biggest, which is usually CI rather than a developer's laptop.

Two behaviours to know before you flip the switch:

  • A resource with no matching policy binding is evaluated in observe mode by default, so adding a policy never silently starts blocking traffic you did not name.
  • In enforce mode, sealr run holds a REQUIRE_APPROVAL operation while a human decides, but the wait is bounded at 120 seconds. If nobody decides in time, or the Console is unreachable, the binding's on_approval_unavailable applies — the default is to block, giving exit code 20.

On this page