# Security model

Read this before giving a bot to anyone but yourself. An assistant that can read your files can be talked into reading them aloud.

- **Who can reach the agent.** Only chats listed in `routing.yaml`. A stranger who finds the bot and writes to it gets no reply; the daemon log gets one line with the chat id. In a group, only the owners are heard unless `allow_from` names others or says `"*"`, and only when they mention the bot (or reply to it). A group paired with `angelia init` admits only the person who paired it unless you say otherwise, and `check-config` names every group open to everyone.
- **Who can act.** Tool permission replies, `/sh`, `/restart`, and any command that changes the session (`/new`, `/stop`, `/resume`, `/model`, `/effort`) are accepted from owners only. So are the CLI's own slash commands (`/clear`, `/add-dir`, a custom command): from anyone else, a message that starts with a slash reaches the agent as ordinary text. `agent_commands: [compact]` on a profile opens chosen ones to every member. `/help` and `/status` are open to anyone in the chat. In a DM the owner is the chat's user; in a group it is the `owners` list. A group member's typed "yes" is refused.
- **What anyone the agent listens to can make it do.** Whatever its permission mode allows, with your files and your accounts: read a file and put it in the reply, run a tool the mode lets through, browse. The owner-only gates above keep the router's controls, not the agent's abilities. For a group, give the profile a cautious mode and list `allow_from`. An approval shows the whole command with its spacing collapsed; a long one is cut in the middle with the count of what is left out, and sent in full just before. In tmux mode too: the request comes from Claude Code's permission hook, never from the screen, where a command can draw a dialog of its own, and a yes answers that request and no other. A dialog only the screen shows is never answered from the chat.
- **What the agent can do** is the profile's `permission_mode`, exactly as in the CLI. `acceptEdits` asks before shell and risky tools; `bypassPermissions` never asks. Codex runs every command inside its own sandbox, enforced by the operating system: Angelia passes it the profile's deny rules and writable folders at launch, so a shell command cannot read a denied path either; it asks in the chat only for more access, such as one more folder for that turn, and a yes adds just that: the deny rules still hold. `sandbox: false` on the profile turns the sandbox off. `check-config` refuses a group open to every member on a bypass profile without the sandbox, warns when a group has no `allow_from`, when `/sh` is enabled on a group without owners, and when `mention: any` puts every group message in front of the agent.
- **Secrets.** The bot token and the secrets your capabilities use live in `~/.angelia/env` (mode 600). The daemon keeps them in memory: no agent, `/sh` or job gets the bot token, and each gets only the variables its own capabilities name. Every compiled profile denies its CLI's file tools the env file, the WhatsApp login (`wa/`, which is as good as the WhatsApp account), the API token, the tmux host files and the session map; your own credentials (`~/.ssh`, `~/.aws`, `gh`'s login, the Keychains folder, Mail and Messages, `.netrc` and the like, unless a profile names one in `add_dirs`); every CLI's record of past conversations, whatever the profile's own CLI (Claude Code's, Codex's, Grok Build's and pi's transcripts and prompt history, from every folder, your own terminal sessions included) and their user-level configs, whose MCP entries often hold a token; every other profile's folder (unless its own folder, `add_dirs` or a directory capability holds it); and edits to the files that decide its next launch. A deny stops the file tools and a plain `cat`, not a program that opens the file itself: an agent with a shell can read what you can read, so keep the shell asking in any profile other people can talk to, or set `sandbox: true` on it (below). `bypassPermissions` is refused on a directory containing `.env` or `secrets/` unless the profile says `unsafe_ok: true`.
- **The sandbox.** `sandbox: true` on a Claude Code profile compiles the CLI's own sandbox on, with no unsandboxed escape. Measured with Claude Code 2.1.280, in `bypassPermissions` too: the deny rules then hold for every program the agent runs (a script that opens `~/.angelia/env` gets "Operation not permitted"), writes stay inside the profile folder and the temp folder, and the CLI's own settings files, skills and `.mcp.json` cannot be written at all. Network from the shell is blocked except the domains you list in `sandbox.network.allowedDomains` in the profile's `settings.local.json`; the CLI's web tools are not affected. Every compiled Claude profile already lets the sandbox reach Angelia's socket, so `angelia send` keeps working when you turn it on by hand.
- **A loosened profile is not launched.** `angelia compile --write` keeps what it wrote for each profile in `~/.angelia/compiled/`, outside every profile. Before a session starts, the daemon checks the profile's settings against that: a profile never compiled, a deny rule that is gone, a settings file that no longer parses, or a sandbox turned off stops the launch, and the chat hears how to fix it (`angelia compile --write <profile>`). A rule a newer version adds is not a refusal: `check-config` names the profiles to compile again.
- **The daemon runs on the table the owner accepted.** The routing table sits in the workspace, which an agent can edit when the workspace is in its `add_dirs`: it could give itself a shell, add owners to a chat, point a capability at any program, or have onboarding hand strangers a profile. So a copy of the accepted table is kept in `~/.angelia/compiled/`, outside every profile, and the daemon and every job run on that copy. A table that changed since waits: `/restart` lists every change and restarts only on `/restart confirm`, which takes exactly the list shown (an edit made after you looked shows a new list). On the host, `angelia accept` takes the table (`--check` lists the changes). `angelia compile --write` works only from an accepted table and accepts nothing itself, so compiling one profile cannot carry in a change to another. Onboarding and `/backend`, which write the table themselves, refuse while it holds changes nobody accepted, and take only the change they made. `angelia jobs run` and `angelia pair` read the accepted table too. Only `model`, `effort`, `shell_timeout_seconds`, a route's `mention` and the idle, permission and stall timeouts need no confirm, and only when nothing else is waiting. The first start of a version with this takes the table as it stands. Not covered yet: what a profile's own `.claude/settings.local.json` allows (`permissions.allow`, `hooks`, `env`); Claude Code itself asks before an agent edits that file.
- **Profiles talk to each other, and only by message.** An agent can ask another profile with `angelia turn <its chat> "<text>"` (or post there with `angelia send`); the message arrives labelled with the asking profile, never as you, and at most 30 an hour go from one chat to another. They share files through `workspace/_common/`, never by reading each other's folders. `workspace/_shared/` (the doctrine their instructions import) and `workspace/_capabilities/` (the skills and tools they run) reach every profile, so agents may read them but not edit them, except a profile with `shared_write: true`: give it to the one that keeps the instance. Claude Code's auto memory is off in every session Angelia starts: Claude keeps one memory folder per git repository, and every profile shares the workspace's, so each would load what the others saved; a profile keeps its memory in its own folder. `isolated: true` on a profile cuts it off from both: set it on a profile other people talk to, and `check-config` reminds you when a group is open to everyone.
- **The local API.** Each agent gets a token in `ANGELIA_API_TOKEN` that posts, attaches and types into its own chat, and messages other profiles as above; a prompt it types is plain text, never a CLI command. The owner token in `~/.angelia/api.token` reaches every chat. The file tools are denied it, but without a sandbox a program the agent runs can still open it, as with every file you can read. With the sandbox on, the deny floor holds for every command the agent runs, so its own token is the only one it has. That token attaches only what the agent's own file tools may read: the daemon opens an attachment itself, outside every sandbox, so a request made with an agent's token (`angelia send-media`, or a `MEDIA:` line) is held to that profile's deny rules, however the path is spelled. The reads a desktop app uses (chat history, settings, jobs, the event stream) and answering a permission prompt from it work with the owner token only; history is read by the CLI itself or from its own files, so Angelia keeps no copy of your messages.
- **Logs** hold chat ids, the ids of senders an allow list refused, session ids and counts, never tokens. A chat that is not allowed leaves at most one line per ten minutes, and `daemon.log` moves to `daemon.log.1` at 10 MB. One exception can carry message text: the last line of a failing CLI's error output. A direct chat sees it in the failure line too; a group sees only that the agent exited. The screen of a tmux pane that never reached a prompt goes to `~/.angelia/tui/<session>/screen.txt`, which agents cannot read.
- **Files from a chat.** Only a routed chat that passes its gate gets a file downloaded, up to 20 MB, under a name Angelia picks. Each profile's `.inbox/` is kept under 500 MB and 300 files, oldest out, and one sender gets 30 files an hour; past that the agent is told a file came and was not fetched.
- **Recent outgoing WhatsApp messages.** The daemon keeps its last hour (up to 500) in memory, never on disk, so a device that could not decrypt one gets the real message again.
- **Control characters.** Every incoming message is reduced to plain text before it reaches an agent, so no byte sequence in a message can act as a key press in a tmux pane.
- **What leaves your machine.** Every turn goes to the model provider of the CLI you chose (Anthropic for Claude Code, xAI for Grok Build, OpenAI for Codex), under that CLI's own terms. Telegram keeps bot messages on its servers; WhatsApp messages are end-to-end encrypted to the linked device. The workspace goes to a git remote only if you gave it one. Angelia itself sends nothing anywhere else: no telemetry, no update check, no account.
- **What it does not protect against.** Prompt injection in what the agent reads: a web page, a file or a forwarded message can still talk the agent into something its permission mode allows. Keep cautious modes for profiles that browse. Anyone you list as an owner has the agent's full powers in that chat. Sender display names are user-controlled and are passed to the agent in the message envelope, cleaned to one line. Another program running as your user can read everything the daemon can.
