Security
Sonata launches other coding agents on your machine. They run as you, with your files and your credentials.
Read the permission tables before dispatching write-capable roles. Codex and reasonix offer a real sandbox (reasonix reports its own
write_roots, which follow--dir). Pi has none, and opencode's is advisory.Sonata never bypasses a harness's own safety flags. It does not pass
--dangerously-bypass-approvals-and-sandboxto codex.Harness credentials stay with the harness. Sonata reads harness config to report health; it does not copy, forward or log API keys. Keys live wherever the harness put them — opencode keeps them in
auth.jsonand thecredentialtable ofopencode.db, for example. Sonata's own gateway keys (sonata auth add, written bysonata init) are separate: they live in sonata's key store and flow store → environment → LiteLLM on the native path, and never intosonata.toml, agent files or argv.An
auth = "session"gateway forwards your Claude Code session credential. It is passed unmodified, and only ever toapi.anthropic.com— a key is only ever sent to the host it belongs to.A stored key is only ever sent to a host bound to its gateway name. Keys are looked up by gateway name, but the
base_urlthey would be sent to comes from a config — including a cloned repo's project config pointing the name at an attacker's host. A host is bound when it is the provider's own endpoint, you confirmed it withsonata auth trust <gateway> <host>, or it is the machine config's own URL for that name.sonata initbinds the URLs you typed in its own run;sonata auth addbinds nothing, because the URL it reads comes from a config a repo can carry. Anything else gets no key:sonata servewithholds it from that tenant and answers a named 502 that names the config setting the URL and the exact trust command, andsonata doctorfails the gateway the same way, for every project it routes. If you did not choose a URL, remove it from that config rather than trusting it. Bindings live in a 0600 file beside the key store; hostnames (with a non-default port) compare exactly, never suffix-matched, and https only, except a loopback http host (localhost,127.0.0.0/8,::1). The machine config's own loopback URL is trusted by definition; in a project config a loopback URL needs an explicitsonata auth trust. Non-loopback plain http is never bindable. The first run after an upgrade binds, once, the hosts of the configs already in use that have a key;sonata auth untrust <gateway> <host>removes any of them, andsonata auth trust-listshows them.Auto-routing sends a limited task excerpt to whatever
base_urlnames. When[auto_route]is set, the first message of each-autosubagent task is sent to that URL — TypeSafe atapi.typesafe.aiby default, OpenRouter atopenrouter.ai/api, or ajev-compatible-serveryou run yourself — after<system-reminder>blocks are removed and the text is capped at 8,000 characters. Nothing else is sent: no system prompt, tool results or later turns. The key follows the host, and a key is only ever sent to the host it belongs to: theopenrouterkey goes toopenrouter.aiand nowhere else. A custom host gets a stored decision key only once you trust it (sonata auth trust auto-route <host>, or give the URL insonata init). With a stored key, an untrusted host gets neither the key nor the task text: the decision is skipped and the tier falls back. With no stored key, a local decision server is sent the task text as any keyless gateway would be, so only pointbase_urlat a server you run. The feature is off unless[auto_route]is written.Prompt injection is a real risk. A foreign model reading a hostile repository can be steered, and it has no classifier between it and your files. For untrusted code, dispatch read-only roles or run in a container.
Please report security issues privately, following SECURITY.md, rather than in a public issue.