Skip to content
Docs menu

Docs Security and approvals

Security and approvals

The four control promises in detail. Policy decides every call, only a person records a decision, contracts pin what you approved, secrets stay in your keychain, and code runs with no network.

The hub makes four promises, and each one is a mechanism you can inspect.

Promise Mechanism
Reads run Policy allows a tool that declares itself read-only, unless you say otherwise
Writes ask Every other tool, including one that declares nothing, asks a person
Only a person approves No MCP-reachable path can record a decision
Secrets never reach the model Credentials are resolved only at the moment of an upstream request

Policy decides every call

A policy rule attaches to a source or a tool, optionally narrowed to one connection, and says allow, ask or deny. For each call:

  1. Any matching deny wins. A narrower rule can never loosen a deny.
  2. A tool that is new, or changed since you reviewed it, asks.
  3. Otherwise the most specific rule decides: tool and connection, then tool, then source and connection, then source.
  4. With no rule, a tool annotated readOnly runs and anything else asks. An unannotated tool counts as possibly destructive, as the MCP specification advises.

A denied call never touches a credential. Policy is checked first, then approval, and only then is a secret resolved.

Only a person approves

When a call asks, the person sees the tool's address and connection, the first sentence of what the tool says it does, the arguments (redacted, capped at 4 KB), why it asks, and when the request expires. There are four choices:

Choice Effect
Allow once This call runs
Allow for this run This tool runs for the rest of the run, so a loop of 20 posts asks once
Always allow A policy rule pinned to the tool's current contract
Deny The call fails with approval_denied inside the model's code

A decision can come only from host code that handles a person's input:

  • Your client's own prompt, for clients that support MCP 2026-07-28 input requests. The answer counts only with the state the server signed for that pending call, bound to the run, the call and a nonce, and only once.
  • The approval page. A local hub opens it in your browser itself, on loopback, with a one-time token the model never sees, a CSRF token, and Host and Origin checks against DNS rebinding.
  • capabilities hub approve <runId> in a terminal.
  • The dashboard inbox, behind a login, on the hosted hub (planned, M3).

No MCP tool, no execute or resume argument, and no personal access token can record a decision. In the code, exactly one module may call the function that records one, and an import-graph test holds that line.

Configuration is not a tool either. No MCP tool can add sources, change connections, edit policies or decide approvals, so a prompt-injected model cannot grant itself access.

Contracts pin what you approved

When you approve a tool, the hub records a digest of its name, description, input and output schemas and annotations. If a refresh finds that digest changed, the tool goes back to ask, rules pinned to the old contract stop applying, and the review shows what changed. New tools upstream also ask until you review them. The hub never widens its own authority.

Secrets stay out of reach

  • On your machine, values live in the OS keychain under the service capabilities.sh. Without a keychain the hub falls back to a mode-600 file and says so every time it does.
  • A connection holds references to secrets, never their values.
  • Every string that crosses into the sandbox, a journal, a log, telemetry or a result is scrubbed. The run's secret values, and their base64 and URL-encoded forms, become [secret], which covers an API that echoes a token.
  • Credentials are entered only by a person: at a masked prompt, on the connection page, or in the dashboard. Never in chat, and never through an MCP tool.

Code runs with no way out

The model's code runs in QuickJS compiled to WebAssembly, inside a worker thread that the hub can terminate. The sandbox has no network, no filesystem and no secrets. Its only way out is the tool bridge, and every call through it passes policy.

Upstream traffic goes only to the hosts fixed when you reviewed the source. Credentials are never sent across a cross-origin redirect, and only a short allowlist of response headers reaches the code.

Resume never repeats a call

Every call is written to the run's journal when it settles. A run that pauses for approval waits for its in-flight calls to finish and records them first. Resuming replays the journal with the same seed and clock, so a call that already happened is never made twice, and a run whose code takes a different path fails with divergence instead of guessing.

The invariants

Each rule has a test that fails the build when it breaks. The local suite is plan unit LH-18. I5 belongs to the hosted hub.

# Invariant
I1 A secret value never enters the sandbox, a journal, a log, telemetry or a result
I2 Only a person records a decision; no MCP-reachable path records one
I3 Authority never widens without a person
I4 The sandbox has no network; upstream traffic goes only to allowlisted hosts; credentials stay on their origin
I5 Tenants are isolated (hosted)
I6 Resume never repeats a call
I7 Every channel has a budget
I8 The model cannot change configuration

What the hub cannot protect against

An agent with shell access on your own OS account can act as you. It could open the approval page, run capabilities hub approve or edit policy files. Your harness's own permission prompts are the boundary there. The hub never hands the model an approval token or a page URL, and opens the page itself.

Telemetry

The CLI's telemetry is on by default, announced on first run, and honors DO_NOT_TRACK. Events carry ids, counts, durations and error codes from a closed, published catalog. They never carry names, paths, code, arguments or results.

Updated 2026-11-06