::docs :: concepts

the trust stack

The egress ledger and air-gap, untracked processes, the URL hold, prompt-injection quarantine, terminal escapes, control files, invented recipients, the trust receipt, and secrets.

The privacy claim isn't a policy promise. You can inspect it in the code and observe it on the network. Saturn's own code reaches the network through four chokepoints: a web search or page fetch, a remote MCP call, a text sent through Messages, and a remote Ollama if OLLAMA_HOST points off the machine. A test in the suite fails if a network client is imported anywhere else, so a new path can't land unnoticed.

::egress ledger and air-gap

Every network exit (channel, host, bytes) is recorded and renders live in the rail. /policy egress lists what left this session. /policy airgap on seals the boundary: web tools refuse, send_message refuses, remote MCP calls refuse, a remote Ollama refuses to run, and each blocked attempt shows in the ledger.

commands
/policy                      what CAN leave, and what runs without asking
/policy egress [clear|n]     the per-event ledger of what DID
/policy airgap [on|off] [--save]

::what the ledger can't see

A shell command, one of your Shortcuts, and a stdio MCP server are separate processes. git pull or curl can reach the network without Saturn seeing it. So every such run is recorded as untracked: the receipt counts it, and /policy egress lists it. The ledger never claims the boundary stayed closed over one. While the air-gap is on, these calls always ask you first, even with a matching allowlist entry or an open gate, and a headless run refuses them even with --yolo.

::the url hold

web_extract is read_only and normally runs without asking, but its URL is something it sends. After outside content (a file, note, email, or web page) has entered the conversation, this turn or an earlier one, a URL the model composed itself, one that appears nowhere in what you typed or any tool returned, faces the gate first. A page can't make Saturn carry your data out in a query string. Only what you typed, what a completed tool call returned, an attachment, and the grounding count. The model's own words never vouch for a URL, not even the preamble of the message that makes the call. An address on this machine or your local network that you didn't type is always held, shorthand spellings like 127.1 or 0x7f.0.0.1 included, and a public page can't redirect a fetch onto one. A URL you typed, or one a search returned, runs as usual. --yolo doesn't approve a held URL.

::prompt-injection quarantine

Web pages and searches, files, shell and Shortcut output, mail, notes, messages, contacts, reminders, calendar events, the browser tab, the knowledge base, and MCP results are untrusted, and so is the error text of a failed call. Content that tries to steer the agent ("ignore your previous instructions", tool coercion, role overrides, hidden terminal escape sequences) is flagged in the trace and fenced between data-not-instructions markers before the model sees it. In the default gate mode, the next call that can send or change something faces the gate regardless of its tier: one fresh look at a call whose arguments may come from injected text. @file attachments and piped stdin warn when they look instruction-shaped, but never block.

runtime.quarantine: off

no scanning

warn

scan, fence, and flag, but never change gating (also turns the URL hold off)

gate (default)

warn, plus the escalation and the URL hold

::text can't take over your terminal

Text from a page, an email, or a file can carry escape sequences that write your clipboard, make a link point somewhere else, or move the cursor and erase lines so what you see isn't what happened. Saturn shows them as visible symbols (␛) everywhere it prints: the rail, answers, /trace and replays, slash commands, headless output, and the debug log. Colour codes from shell commands are removed. Content that carries such a sequence is flagged like an injection attempt, so the next action asks first. A link in an answer prints as text (address), so the address you'd open is the one you can read.

::files Saturn won't write

Some files control Saturn itself. write_file, edit_file, move_file, and delete_file refuse them even when you approve, and refuse to move a folder that holds one: config.yaml, permissions.json, ~/.saturn/hooks.yaml, the memory file and its review queue, both SATURN.md files, and anything in your skills folders (~/.saturn/skills and a folder's .saturn/skills). A write to one could loosen the gate, plant a command that runs ungated, plant an instruction into every later turn, or plant a "fact" past memory review. The check uses file identity, so a different capitalization on macOS's case-insensitive disk is refused too. Edit these by hand, through /memory, with /init, or, for a skill, through create_skill and /skills.

::invented recipients

A text or a Messages lookup names a person by phone number or email address. If that handle appears in nothing you typed and nothing a tool returned, the call is refused before it reaches you, and the model is told to look the person up in Contacts. The model's own earlier words never vouch for a number, and neither does the text of a refused or failed call. A recipient given as a name, where a number or address belongs, is refused too. A group chat reference gets the same check: it must have come from a tool, such as find_group_chats.

::a remote ollama is egress

When OLLAMA_HOST points off the machine, every model call is network egress: it's recorded, refused under the air-gap, and shown on the posture line and in /policy. The host is parsed as an address, never matched as a string, so 127.evil.example.com can't pass as loopback. Memory facts marked sensitive (--sens) are withheld from any prompt bound for a remote host.

::the trust receipt

The stats line under each answer carries a trust segment whenever something actually happened: ⇅ N sends · bytes → host in yellow, ⊘ N blocked for air-gapped attempts, N untracked for shell, Shortcut, and stdio MCP runs, and N calls gated. If the ledger was cleared mid-turn, the segment says egress unknown instead of going quiet. A fully local turn adds nothing to the line. Silence means nothing left. runtime.receipt: false turns the segment off.

::secrets

  • –At the gate, each gated call's arguments are scanned for keys, tokens, and private-key blocks, and a hit warns inline.
  • –In shell children, run_shell strips secret-shaped environment variables (API_KEY, SECRET, TOKEN, PASSWORD, CREDENTIAL, ANTHROPIC, OPENAI, AWS_, GITHUB_ by default) from the child's environment. The list is shell.env_scrub.
  • –Nowhere else. No Saturn feature takes an API key. MCP secrets are plain environment variables expanded from your shell or .env.

::trust settings are session-only unless saved

runtime.auto_approve, runtime.airgap, runtime.quarantine, runtime.grant_scope, and shell.env_scrub apply for the session only, whether you set them through /config or /policy, unless you pass --save. A loosened posture is never written to disk silently.

::what the benchmark measures

python benchmark.py (from a source checkout, with Ollama running) runs the trust benchmark. It checks approval-gate coverage (every non-read-only call must have faced the gate), the injection-quarantine flag rate (a planted instruction-shaped document must be fenced), and the memory tasks (recall across runs, supersession, and a planted memory that must face the gate). --strict exits 1 on any graded failure. --loop runs the loop benchmark instead, grading everyday requests on passes, tool choice, and actions described but never performed. --think <mode>, --tier <tier>, --off <toolkits> (to measure a smaller catalog; a loop task that needs a toolkit that is off is skipped and counted apart), and (with --loop) --runs N change the run in memory only; config.yaml isn't written. A benchmark run never acts on your real Mac: it declines every gated call into the apps.