This policy covers Agent, the daemon and agent CLI in this repository,
the agent-client crate in client/, and Sonny,
the terminal client built on them. Agent and Sonny release together under
one version tag.
Agent and Sonny are experiments with no users yet. Security fixes land on
main and ship in the next release. Earlier releases are not patched
separately; upgrade to the latest tag.
Report vulnerabilities privately through GitHub private vulnerability reporting. Do not open a public issue for an undisclosed vulnerability.
Include the affected revision, the access the attack needs, its realistic impact, and the smallest reproduction you can give. Leave out real credentials, tokens, transcripts and unrelated private data.
Agent is one daemon per person that runs many model-driven bots, each a
durable conversation with tools: a shell, file reads, writes and edits,
pictures, web_fetch, and whatever a client declares. Programs drive it
over a Unix socket beside its SQLite store (~/.agent/state.sqlite.sock
by default) with a JSON Lines protocol; the agent CLI is one such
program. The daemon holds the conversation history, calls the model
providers, runs the tools, and keeps the approval gates a client sets.
Sonny is a client of that socket. It makes a lead bot for a folder and
tasks under it, runs the daemon's companions (the automatic approver, the
lookout, the tool host), schedules triggers with the system's scheduler,
keeps memory, skills and the commons under ~/.agents, reaches the
person's MCP servers, and drives a browser. Everything Sonny adds runs as
the person, on the person's machine.
What this means for anyone running them:
- Bots act with your permissions. A bot's tools run as your user on your machine. A sandbox narrows what a bot's commands and writes may touch; nothing isolates the daemon itself, and nothing it runs is a different user.
- Models read untrusted text. Tool output, fetched pages, other bots' messages, MCP results, lookout sources and files in a checkout all reach the model. A model can be talked into acting on them. The sandbox, the approval gates and the marks on outside content limit what that can do; they do not make a model immune.
- Conversation history is kept in the clear. The store holds every
turn, tool call and result, and it is not encrypted. Protect
~/.agentas you protect your shell history and your keys.
The following properties must hold. A revision that breaks one is a vulnerability, whether or not the code's own documentation agrees.
- The daemon listens on a Unix socket only. It opens no TCP port and no
WebSocket listener; the WebSocket in docs/WEBSOCKET.md
is an outbound provider transport. The one TCP listener in the
repository is
agent login chatgpt's sign-in callback, bound to127.0.0.1in the CLI process for the minutes the sign-in takes. - Anyone who can connect to the daemon's own socket has full control of
every bot: the daemon checks no peer identity and no token on it, by
design. The daemon binds the socket
0600, whatever the umask, by setting the umask around the bind, so there is no instant in which the name exists with a looser mode.~/.agentis made0700when the daemon or the CLI makes it; one that already exists keeps the mode its maker gave it, so on a shared$HOMEcheck it yourself. When the store's path is too long for a socket address, the socket moves to a folder under/tmpthat is made0700and refused unless the caller owns it alone. - A sandboxed bot's commands never reach the daemon's own socket. Each
such bot gets a socket of its own (
SOCKET.d/BOT_ID, in a folder made0700) that answers as that bot and refuses what would widen its confinement:configure,answer,tool_result,approve_step,step_result, serving approvals,companions,resume,shutdown, creating or forking a bot it does not own, submitting as the person or another bot, and deleting or interrupting a bot it did not make. The folder of those sockets is read-only inside every sandbox, so no bot can put a listener in another's place. A command that runs outside the sandbox, allowed to escalate or by anallowcommand rule, gets the daemon's own socket. - The daemon collects and sends no usage analytics. Its outbound
connections are to the model providers a bot's model names, the ChatGPT
sign-in, and the pages
web_fetchis asked for.
- A bot made with
sandbox: read-onlyorworkspace-writeruns its shell commands, writes and edits under bubblewrap on Linux or Seatbelt on macOS, with the mode names, writable folders and profiles ported from Codex (sandbox). Reads stay open in every mode: a confined command can read any file the person can,~/.agent/envand the store included, and its model provider sees what it reads. - A sandboxed bot never runs unconfined for want of a sandbox: creating one
fails with
sandbox_unavailablewhen bubblewrap orsandbox-execis missing or cannot make its namespaces. Bubblewrap is found at fixed system paths or the one a package build fixed, never onPATH. - Inside a writable folder,
.git,.agentsand.codexstay read-only and cannot be created, and.awsstays read-only where it exists, since git runs hooks and config from there, agents trust what is there, and an AWS profile can name a program to run. A git folder a bot may commit to keeps its hooks, config, modules andcommondirread-only. The daemon's store, the files SQLite keeps beside it, its socket and the bots' sockets are read-only inside every sandbox. - A write or edit is checked by the target's real path and then carried out inside the sandbox, so a symlink swapped in after the check cannot carry it outside. A protected folder that is a symlink refuses the call.
- Network is off unless the bot's
networkis set. Off, a bot reaches only the Unix sockets itsunix_socketsnames, since a program on a socket runs what it is asked outside any sandbox: Seatbelt holds commands to those paths on macOS, and on Linux a seccomp filter refuses every Unix socket a command would make, so a bot that names any reaches them all. With the network on, a bot reaches every Unix socket but the daemon's and the other bots', as Codex's full network does. Either way it never reaches one in a walled folder (serve --wall), where clients keep the sockets of programs they run for bots outside the sandbox: the CLI walls~/.agent/run, where Sonny's agent-desktop host and agent-browser sessions listen, as Codex hides its own app-server's sockets. Seatbelt denies them by path; on Linux the folder is hidden. A writable root inside a walled folder is refused. The Linux filter also refusesio_uringand foreign-architecture system calls. - A call that would leave the sandbox (a shell call asking
require_escalated, a write outside the writable folders) waits on the bot's approval gate and runs unconfined only when allowed. With no gate it is refused. A command runs outside without asking only when anallowcommand rule, which the bot's client gives it (Sonny's are in~/.agent/rules), matches every command in a plain script; any script with substitution, redirection or an unmatched command stays inside. Aforbiddenrule refuses a command even when it escalates, and apromptrule makes it wait on the gate, or refuses it without one, even when the sandbox would confine it. The rules folder is read-only inside every sandbox (serve --protect-path), so no bot widens its own rules; rules its client couldn't read refuse every command. - A bot made or forked by a sandboxed bot inherits its creator's sandbox and writable folders, may only narrow them, reaches the network only if both do. It goes by its creator's command rules, and can't be given others.
- A bot made with
danger-full-access, or with nosandbox, runs unconfined with the caller's full permissions. That is the daemon's default; Sonny's default is a sandbox. - Only
shell,write,edit,apply_patchandwrite_stdinare confined.read,historyandweb_fetchrun in the daemon itself, soweb_fetchworks for a bot whose network is off.
- The daemon holds the mechanism and no policy: a gate names tools whose calls wait for a verdict from whoever serves its tag. A call needs an allow from every gate, the first deny denies it, and who decided is recorded on the call. A gate's expiry denies a call nobody judged. The daemon's default is no gate.
- Only a bot's client can set or replace its gates; a sandboxed bot's own commands cannot. A fork keeps its source's gates, and a bot another bot makes takes its creator's.
- Under a sandbox, only a call that leaves it is judged; a confined call runs without a verdict. Sonny's served tools wait for one too, since they act outside the sandbox.
- Sonny's automatic approver (
auto, the row a new lead starts in) sends every gated call to a judge model with Codex's guardian policy and denies what the judge calls dangerous or unclear, what it could not assess, and a round the judge never answered. Known secrets in what the judge is shown are redacted before they leave the machine. Three denials in a row, or ten among the last fifty, trip a breaker that interrupts the turn. The judge readsAGENTS.mdfiles as the person's standing instructions, so a checkout'sAGENTS.mdis as trusted as the checkout. - Any agent can pull the andon cord (
pause), which cancels running turns and ends background processes and is attributed to the agent that pulled it; only a client on the daemon's own socket canresume.
- Provider keys come from the environment or
~/.agent/env, which Sonny writes0600and refuses to read when group or others can. Nothing writes a key into a shell profile or intoconfig.toml. - The ChatGPT sign-in keeps its tokens in
~/.agent/chatgpt.json, written0600through an exclusive temporary file, and refreshes them itself. Codex's~/.codex/auth.jsonis read only to see that a sign-in exists; its tokens are never read out. Bedrock uses staticAWS_*variables oraws configure export-credentials. No keychain is read. - A bot's shell gets the daemon's environment without the keys its
providers use, and the provider keys and ChatGPT tokens are redacted
from what tools return, as are values after fields named like
password,api_keyorsecret_access_keyand the bodies of PEM private-key blocks. Redaction matches the exact value in tool output only; an encoded copy, or a secret in model text, is not caught. - Other secrets in the daemon's environment reach a bot's shell unless
[shell_environment_policy]inconfig.tomlleaves them out: as in Codex,*KEY*,*SECRET*and*TOKEN*are withheld only withignore_default_excludes = false, andexcludenames more. The daemon's companions (Sonny's approver, lookout and tool host) inherit its environment whole, provider keys included. - A secret found while importing an MCP server goes to
~/.agent/env, never toconfig.toml. Sonny's home backup leaves~/.agent/env, logins, the store and anyconfig.tomlthat holds a secret off the remote repository. - The daemon never logs a credential's value, and this repository holds
none. A custom
--providerspec may name anhttp://URL; the key then travels in the clear to it.
- The store,
~/.agent/state.sqlite, holds every bot's history, prompts, pictures, tool output, artifacts and approvals in the clear. The daemon makes it0600before SQLite first opens it, so the-waland-shmSQLite makes beside it take that mode too, and tightens a store or sidecar an earlier daemon left looser. The daemon's log, the companions' folder and logs, and what the lookout keeps beside the store (its log, cursor, lock and marker, the sources', subscriptions' and goals' folders) are made0600and0700the same way. There is no encryption at rest. Tool output spilled to files goes in folders made0700, as0600files. rm,pruneand--keep-turnsdelete rows; SQLite's free pages and WAL are not scrubbed. Compaction keeps the original history.- A resume never silently makes a fresh bot. A fork is a new identity; a bot's continuation has one owner.
- A message one bot sends another reaches the model with a line naming the sender, and a lookout source's event arrives with the lookout's own origin, never as the person's words. A tool's result, a fetched page and an MCP result reach the model as tool output.
web_fetchfetches onlyhttpandhttpsURLs, follows at most ten redirects, reads at most 4 MiB, and sends no credentials. It does not refuse private, loopback or link-local addresses: a bot withweb_fetchcan read what the daemon's host can reach on its own networks, with the bot's network on or off. Setweb_search = "disabled"to leave it out of new bots.- Sonny marks a memory fact or commons post saved in a turn that read
outside content through web search,
web_fetch, the browser or an MCP server asexternal, and its cleanup removes a fact that reads as an instruction to agents. Content a shell command fetched (curl,gh, a cloned repository) is not marked. - Skills, roles,
AGENTS.mdand memory are instructions the model reads. Sonny writes its own only under~/.agentsand leaves alone any the person edited. A folder'sAGENTS.md,.agents/skills,.agents/agentsand.agents/setupare read from the checkout and are as trusted as the checkout:.agents/setupruns as the person, outside any sandbox, when a task starts, behind the lead's approval gate.
Sonny's tool host answers some tools outside every agent's sandbox, since
they are things the sandbox cannot do: triggers, memory, start_task,
run_in_pane, browser, exec for MCP, subscriptions, goals. Each that
can change something outside the agent's folder waits on the agent's
approval gate as a call that leaves the sandbox would; memory and
show_agent wait for none.
- Triggers are jobs of the system's scheduler (a LaunchAgent, a
systemd user service) that run as the person. Adding one, and its
ifcommand, wait for a verdict; theifcommand runs with the provider keys stripped. Nothing of a trigger runs between its fires. A goal's check command runs the same way. - Lookout sources are commands the person sets up in
config.toml, run withsh -cas the person outside any sandbox. - MCP servers run as the person outside every sandbox, started from
~/.agent/config.tomlwith~/.agent/envin their environment. An agent reaches them only throughexec, a QuickJS script with no files, network or timers, and each call a script makes is checked on its own by the server'sapproval_mode; the default asks only for a tool the server itself marks destructive. An agent's network setting does not stopexec. A folder's own MCP servers are never imported without the person ticking each, since whoever wrote the folder wrote them. - The browser tool wraps page text in boundary markers the model is
told never instruct it, and refuses what would reach past the browser:
flags that launch another program or load an extension, plugins, the
clipboard, the DevTools address, and writes outside the agent's folder.
eval,download,upload,stateand vault sign-ins wait for a verdict. - The desktop tool drives the person's desktop with agent-desktop, which holds the person's Accessibility and Screen Recording grants. What only looks runs at once; a call that acts waits for a verdict once, and only the flags it lists pass. It starts agent-desktop responsible for itself on macOS, so the grant to it is the one that counts, never the terminal's that started the daemon.
- agent-browser and agent-desktop run only from copies in
~/.agent/sonny/programs/, refused while the program, or that folder, is in a folder a sandboxed agent writes, and checked again whenever it changes. They run with~/.agent/run/sonny/as their temporary and socket folder, checked the same way and walled off from every sandboxed command, so agent-desktop's host and agent-browser's sessions are ones Sonny started, reached only through the tools and their gates. - The commons is a board shared by one person's agents across
projects, as files under
~/.agents/commons; each post's author is set by Sonny from the calling agent, never from what it wrote. A new thread starts turns in agents of other projects it invites, on a token budget, and invites do not chain through idle agents. It leaves the machine only through the home backup. - Home backup pushes
~/.agents(memory, skills, profiles, the commons) and a secret-freeconfig.tomlto a private repository the person names, with the person's own git credentials, every ten minutes. Sonny names no host for it. Restoring a backup restores the MCP servers' commands, which then run.
Cargo.lockis committed and every release builds--locked.rust-toolchain.tomlpins the toolchain. Workflow actions are pinned by commit SHA, and Dependabot keeps crates and actions current.- A tag push runs a workflow that holds no secrets and does nothing;
the release and Homebrew workflows run from
main's own definitions on its completion, verify the tag points to a commit onmainand that both manifests carry the tag's version, and refuse a tag that moved during the build. - Release assets (the source archive, the formulas, the static Linux
builds) carry a GitHub build attestation from the release workflow.
Publishing to the Homebrew tap verifies it, so assets swapped on the
draft afterwards publish nothing.
install.shchecks the download's sha256 and, whenghis signed in, the attestation; it may run the system's package manager throughsudoto install bubblewrap and ripgrep. - The Homebrew formulas build from source on the person's machine. The Linux tarballs are static builds and are not code-signed.
- The repository has secret scanning, Dependabot alerts and Dependabot security updates on.
These are not vulnerabilities to report; they are the boundaries of what is built.
- A bot without a sandbox, and every daemon companion, trigger, lookout
source, goal check,
.agents/setupand MCP server, runs with the person's full permissions. A workspace snapshot or a worktree is not a sandbox. - The sandbox confines commands, writes and edits, not reads: a confined bot can read any file the person can and send it to its model provider.
- With the network on, Sonny's default, a sandboxed bot reaches every Unix socket on the machine. One that runs a program outside the sandbox on request, such as Docker's or ssh-agent's, is a way out of it, unless it listens in a walled folder.
- The programs' copies and agent-desktop's temporary folder are checked
against the agents' writable folders when a copy is made or changes,
not on every call. An agent given write access to
~/.agentafter that (it is read-only in every sandbox) could leave a host there that answers Sonny's desktop calls with made-up results. It runs in that agent's sandbox, so it cannot act with the person's grants, and an agent that writes~/.agentcan already rewrite Sonny's settings. - One allowed escalation, or one
allowrule, gives a command the daemon's own socket and so full daemon authority. A rule matches a command's first words, soallowongitlets out everygitsubcommand, and the program is found on the daemon'sPATH. - A judge model is a model. The automatic approver reduces, and does not remove, the chance that an injected instruction is carried out.
web_fetchreaches the daemon host's own networks.- Nothing in the store is encrypted at rest, and nothing on the daemon's own socket authenticates beyond the socket's file permissions, which the daemon sets to its owner alone.
- Multi-daemon operation, moving bots between machines and remote socket access are not implemented (docs/MULTI_DAEMON.md).