Skip to content

Why Seal

The leading AI coding agents all share a baseline shape: a long-lived process with broad access to your filesystem, your network, and your shell, gated by a permission prompt or a coarse sandbox toggle. Seal is built on a different shape. This page covers the design choices that fall out of that — and the failure modes they exist to prevent.

Permission prompts are a security control, not a UX wart

Section titled “Permission prompts are a security control, not a UX wart”

A coding agent that prompts on every action loses the user. The shape that ends up shipping in most agents is the obvious response: prompt once per category, default to “remember my choice”, make the prompt easy to dismiss. The user accepts a broad grant, the prompt stops firing, and from then on the agent runs with whatever blanket permission was implied by that single click.

The failure mode is not theoretical. After a few sessions, the prompt has become muscle memory — Enter, Enter, Enter — and the user is no longer reading what they’re approving. The control still exists in the UI; it has stopped existing in practice.

Seal’s permission model is designed against that drift:

  • Four choices, not two. Every prompt offers Allow (run this once, ask again), Allow always (silent from now on), Deny, and Deny always. The split forces a deliberate choice between “I want this call now” and “I want every future matching call.” Most prompts the user wants to silence are narrow — a specific subcommand, a specific path — and the four-way framing lets them land that narrow grant without widening the surface.
  • The pattern is adjustable. The prompt shows the suggested grant pattern at the top — cargo nextest run:*, src/**/*.rs, api.github.com — and the arrow keys walk it across scale levels. Right narrows; Left broadens; Tab snaps to the recommended level. Whichever level is showing when you commit your choice is what lands in seal.toml. The user can broaden to cargo:* and pick Allow (prompts again next time) — which writes a prompt = true floor that catches every future cargo subcommand at its own specificity. The shape an experienced user converges on is one broad prompt = true per tool family with narrow silent allows on top, so new subcommands always surface before they run.
  • Everything is scoped. A grant is a pattern — a command pattern, a filesystem glob, an env-var name, a network domain — not a coarse “filesystem on” / “shell on” toggle. Two grants for the same tool family stack: a narrow silent allow for cargo build:* sits alongside a broad cargo:* prompt = true without either shadowing the other. The matcher picks the most specific.
  • Every grant is signed. Each accepted prompt re-signs the manifest. The next session compares the on-disk file against the last signed hash; if you’ve hand-edited it (or a process did), the next launch surfaces the diff for re-approval before any agent traffic. A new grant landing during a session goes through the same gate — the diff is shown, you confirm, the file is re-signed in one step. The control surface is the manifest, and the manifest is tamper-evident.

The full mechanics are at Permission model. The point of this section is the design intent: prompt-fatigue is the failure mode the model was built around, and the four-way prompt plus adjustable pattern plus most-specific-wins matching is what stops a single dismissive click from handing the agent the keys.

Most coding agents that ship with a sandbox treat it as a setting. The sandbox is off by default in some configurations, on in others — and even when it’s on, the agent can request to run a command outside the sandbox. The user sees a prompt, says yes, the command runs unsandboxed.

That shape is hard to defend. The point of a sandbox is to make refusal stick at the kernel boundary, even when the agent has been convinced — by prompt injection, by a misread tool result, by a compromised dependency — to do something you didn’t intend. A sandbox the agent can ask its way out of is, at best, a soft hint. At worst, it’s a UX that trains the user to approve the unsandboxed escape hatch the same way they approved every other prompt.

Seal does not ship that escape hatch. Every command_run invocation enters the OS sandboxbwrap on Linux, sandbox-exec on macOS — without exception. There is no flag in seal.toml that disables it. There is no permission prompt the agent can fire that lets a command bypass it. Setting command_fs = "all" removes the per-path filtering but does not remove the namespace isolation; setting command_tools = [] removes the bundle defaults but does not remove the wrapper. The agent cannot run a command outside the sandbox under any circumstances.

If you need to run a command outside the sandbox, you run it yourself, in your own shell. The agent is not a privileged channel for unsandboxed execution.

This is upstream of every other security guarantee on this page. Per-command grants, audit logs, signed manifests — all of them assume the kernel-level envelope is unavoidable. Removing the bypass is what lets them mean what they claim.

Per-command least privilege, not a process-wide sandbox

Section titled “Per-command least privilege, not a process-wide sandbox”

The other common shape: bind the entire user filesystem read-only at process start, optionally bind a project subtree read-write, and call it a sandbox. Every command the agent runs sees the whole tree.

This is a real isolation primitive — it blocks the agent’s children from writing arbitrary files, it can mask a credential file with a deny rule — and on a project where the only useful semantics are “the agent should be able to read everything the user can,” it’s fine. The problem is that the unit of access control is the process, not the command. Every tool the agent spawns — cargo build, git push, python, npm install — sees the same filesystem the others do. If any of them is talked into exfiltrating a file (via prompt injection, via a malicious dependency, via a bug in the tool itself), the surface available to it is the whole tree.

Seal’s sandbox is the other direction: per-command filesystem, environment, and network grants. The unit of access is the command pattern, and a command sees only the surface its pattern authorized.

The mechanism is the curated tool bundle. Each bundle is a pre-baked description of what a specific tool needs to function:

  • Read binds for the tool’s config dirs (~/.gitconfig, ~/.cargo/config.toml, ~/.config/gh).
  • Write binds for caches the tool legitimately needs to populate (~/.cargo/registry, ~/.bun/install/cache).
  • Env-var passthroughs for the variables the tool reads (GITHUB_TOKEN, CARGO_*, NPM_TOKEN).
  • Network allowlist for the tool’s canonical hosts (github.com for git/gh; crates.io for cargo; *.npmjs.org for npm/bun/yarn/pnpm).

You opt a tool family into the bundle with one entry:

[sandbox.os]
command_fs = "system"
command_tools = ["git", "gh", "cargo", "bun"]

The effect: when the agent runs cargo build, the spawn sees ~/.cargo/registry, the CARGO_* environment variables, and connections to crates.io — nothing else. When the agent runs gh pr view, the spawn sees ~/.config/gh, the GH_TOKEN env var, and connections to github.com — nothing else. cargo cannot reach ~/.config/gh. gh cannot reach ~/.cargo/registry. Neither can reach ~/.ssh, ~/.aws, or anything else not in their bundle.

This applies to all three surfaces:

  • Filesystem. Binds are per-command-pattern. The default command_fs = "system" baseline binds /usr, /bin, /etc, and a few other read-only system paths; $HOME is not bound. Beyond that baseline, every path a command can see comes from a bundle grant or an explicit [capabilities.allow.read] / .write entry. A command reading a path outside its grants sees ENOENT — the path isn’t there, from the command’s point of view.
  • Environment variables. The sandbox spawns with an empty environment by default. Each bundle declares the variables its tool needs, and those are forwarded only to commands matching the bundle’s command patterns. GITHUB_TOKEN reaches git and gh; it does not reach cargo, bun, or any command without a grant. Variables not in any allowlist are not forwarded — not silently passed through, not “available but not advertised.”
  • Network egress. Outbound traffic from a sandboxed command is routed through a per-session MITM proxy with a session-issued CA. The proxy enforces a per-command-pattern host allowlist: cargo:* can reach crates.io, git:* can reach github.com, neither can reach an arbitrary attacker-controlled domain. Traffic to disallowed hosts gets a connection refused inside the sandbox and a denial entry in the audit log.

A standard sensitive-file masklist hides /etc/shadow, SSH host keys, ~/.ssh/*, ~/.aws/credentials, ~/.gnupg/*, and cloud-provider credential dirs even when a filesystem baseline would otherwise expose them. Those are unreachable from any sandboxed command by default.

The point of this section is not the catalog — it’s the unit. Coarse sandboxes scope access to the whole process; Seal scopes access to the command. The bundles are how that scope ships ergonomically.

The full bundle list is at [sandbox.os.command_tools]. The mechanism is described in OS sandbox.

The agent itself runs inside a WebAssembly sandbox

Section titled “The agent itself runs inside a WebAssembly sandbox”

Everything above describes how Seal isolates the commands the agent runs. There’s a second isolation tier underneath, around the agent loop itself.

The seal agent is a WebAssembly component compiled to wasm32-wasip3. It imports zero WASI capabilities — no filesystem, no network, no clock, no process access. The only imports are custom Seal WIT interfaces (tools.file-read, tools.command-run, llm.chat, etc.). Every operation the agent wants to perform leaves the WASM envelope through one of those typed calls into the daemon, and the daemon checks the call against the manifest before doing anything.

The agent built this way cannot:

  • Open a file directly. The path it wants to read goes through tools.file-read, which runs the capability check.
  • Open a socket. The host it wants to reach is named in command_run’s domain list, which runs the capability check.
  • Fork a process. There’s no syscall available; spawning a subprocess goes through tools.command-run, which runs the capability check, then the OS sandbox.
  • Read environment variables. They aren’t visible inside the WASM module.

The relevant threat is prompt injection. Tool results — file contents, command output, web fetches — are untrusted input. They can contain text crafted to manipulate the model into “do something malicious.” In a process-as-agent design, “do something malicious” can mean “open a socket to attacker.example.com, read ~/.ssh/id_rsa, fork a curl-pipe-sh.” In Seal it means “ask the WIT surface for a capability that doesn’t exist in the manifest” — which the daemon refuses, or surfaces as a prompt the user has to answer.

The WASM envelope is also where MCP servers will run when that ships — same shape, zero WASI imports, all interaction through a WIT interface — so the isolation guarantees the agent already has extend to any third-party component Seal hosts.

The full mechanics are at Agent runtime.

The two sandboxes don’t duplicate each other:

LayerWrapsThreat
WASM sandboxThe agent loop itself.The agent process is talked into doing something malicious. The WIT surface is the only door; the manifest gates every call.
OS sandboxEvery command_run subprocess.A command the agent spawns is talked (by prompt injection, by a malicious dep) into reading or exfiltrating something. The sandbox bounds what it can see at the kernel boundary.

Between the two sits the capability layer — the permission model — which decides whether a WIT call should run, and what it should be allowed to touch when it does. The signed manifest is the source of truth; the WASM envelope makes sure the agent can only ask through the WIT surface; the OS envelope makes sure the answer sticks for every subprocess.

  • Permission model — the four-way prompt, the adjustable pattern, how grants accumulate and re-sign the manifest.
  • OS sandbox — what bwrap / sandbox-exec do, what the per-command bundles bind, how the per-session MITM proxy enforces network allowlists.
  • Agent runtime — the WASM envelope around the agent, the WIT surface, what lives inside the sandbox today.
  • [sandbox.os.command_tools] — the full bundle catalog.