Security & Privacy
mxr is local-first by design.
Mail syncs from the provider into SQLite on your machine. Search runs against the local index. The daemon, TUI, CLI, and agent workflows all operate on that local state. There is no hosted mxr relay in the middle.
What stays local
Section titled “What stays local”- SQLite is the canonical store
- Tantivy index is local and rebuildable
- The daemon runs on your machine
- The TUI and CLI talk to the daemon over a local Unix socket
- The web app talks to the same daemon through a loopback HTTP/WebSocket bridge
What still talks to a provider
Section titled “What still talks to a provider”- Sync
- Send
- Provider-side mutations like archive, trash, labels, and spam
- Browser handoff for HTML or unsubscribe pages when needed
That is the intended boundary. The network is for talking to your provider, not to a hosted mxr service.
Guardrails that exist today
Section titled “Guardrails that exist today”--dry-runon risky mutation commands (includingmxr sendandmxr unsnooze --all)- Interactive confirmation for destructive and batch mutation flows unless
--yesis set - Undoable mutations:
archive,trash,spam,read,read-archiveprint amutation_idyou can pass tomxr undofor ~60s - Persisted mutation history through
mxr history - Event and log views through diagnostics and CLI commands
- Plain-text-first reader mode, with browser escape hatch for original HTML
- Daemon IPC socket permissions are set to
0600on Unix. - The bridge requires bearer auth for every authority-bearing route. Only
/api/v1/health,/api/v1/auth/local-token, and/api/v1/i18nare unauthenticated bootstrap/read-only routes. - The bridge checks Host/CORS allowlists and adds frame, content-sniffing, and referrer-policy headers.
- Saved attachments and rendered HTML assets are written
0600on Unix. - User-initiated attachment downloads are limited to the configured downloads directory, the current directory, or the system temp directory.
- Remote HTML assets are capped before writing, even when the server omits
Content-Length.
Where credentials live
Section titled “Where credentials live”IMAP/SMTP passwords are stored on disk, keychain-optional. They live in
<config_dir>/secrets.toml — a plaintext TOML file at mode 0600 (owner
read/write only), keyed by password_ref + username. This is the same model
normal CLIs use (~/.aws/credentials, ~/.config/gh/hosts.yml): the deliberate
tradeoff is plaintext-at-rest, protected only by filesystem permissions rather
than OS encryption. mxr chose disk-first because an ad-hoc-signed release binary
loses its OS-keychain read access on every upgrade — which used to hard-fail
daemon startup for password accounts. A 0600 file survives upgrades untouched
and is readable only by your own processes.
The OS-native secret store is an optional fallback:
- macOS: Keychain (Keychain Access)
- Linux: Secret Service (e.g. GNOME Keyring, KWallet)
On the first read after upgrading from a keychain-only version, an IMAP/SMTP
credential found in the keychain is automatically migrated (mirrored) into
secrets.toml and served from disk thereafter. mxr accounts add and
mxr accounts repair NAME write secrets.toml (disk-authoritative) with a
best-effort keychain mirror that never blocks the operation. The on-disk
config.toml only references credentials by password_ref; it never stores the
password itself.
Password lookup happens when an account connects or syncs, not while the daemon
is being constructed. An unreadable password should fail that account’s
operation without preventing the daemon or other configured accounts from
starting. Set MXR_KEYCHAIN=off to disable keychain reads and writes for
IMAP/SMTP passwords; with that setting, secrets.toml is their only source.
The setting does not affect Gmail or Outlook OAuth token storage.
Gmail OAuth refresh tokens are stored in the OS keychain with a private disk
fallback under the active token dir, so a noninteractive keychain failure does
not strand an otherwise valid account. Outlook OAuth tokens are JSON files under
the active token dir (<data_dir>/tokens by default, MXR_TOKEN_DIR when set).
secrets.toml lives in the config dir, so mxr reset and mxr reset --hard
preserve it — your credentials survive a runtime-state wipe. Override its
location with MXR_SECRETS_PATH. Protect it like any dotfile secret: keep the
0600 mode and do not commit it to version control.
Backup and restore
Section titled “Backup and restore”mxr does not run a hosted backup service. Your local profile is the recovery boundary.
To find the active paths:
mxr status --format json | jq -r '.config_path, .data_dir'For a clean backup, stop mxr processes first, then copy:
- the config directory containing
config.tomlandsecrets.toml(your IMAP/SMTP passwords — keep its0600mode and treat it as sensitive) - the data directory containing
mxr.db,attachments/,logs/,tokens/,search_index/, andmodels/ - any OS keychain entries for Gmail if you are moving to a different
machine (IMAP/SMTP secrets travel in
secrets.toml)
search_index/ and models/ are rebuildable, so you can omit them from
space-constrained backups. Keep mxr.db, attachments/, tokens/, and
config.toml together. Do not copy mxr.db while the daemon is writing
unless your filesystem backup tool provides a consistent snapshot.
To restore, install the same or a newer mxr version, stop the daemon,
put the config/data directories back at the resolved paths (or set
MXR_CONFIG_DIR / MXR_DATA_DIR). If you restored secrets.toml, your
IMAP/SMTP passwords are already in place; otherwise run
mxr accounts repair NAME (and restore any Gmail keychain entries), then
run:
mxr doctormxr syncBridge and local IPC boundary
Section titled “Bridge and local IPC boundary”The Unix socket is a local user boundary. Any process that can connect as the same OS user can drive the daemon with that user’s authority, so mxr keeps the socket owner-only and expects it to live under a user-owned runtime directory.
The HTTP bridge is broader because browsers cannot open Unix sockets. It binds to loopback by default, uses a bearer token stored under the active profile config directory, rejects DNS-rebinding-shaped Host headers, and keeps API docs behind the same auth gate as the rest of the API.
Attachments and remote content
Section titled “Attachments and remote content”Attachment names are sanitized before mxr writes local files, including
Windows reserved names such as CON and LPT1. Explicit downloads are
constrained to safe destination roots. Inline and remote HTML assets live
under mxr’s attachment cache, get private file permissions, and remote
asset fetches have a fixed body-size cap.
Agent and MCP boundaries
Section titled “Agent and MCP boundaries”The first-party MCP server and agent-origin permission profiles now run through
the daemon’s normal IPC boundary. Configure read-only, draft-only,
restricted, or full policy per origin; limit accessible accounts; and gate
send or destructive operations separately. MCP send and mutation tools also
require confirm=true.
[agents.profiles.mcp]safety_policy = "draft-only"allowed_accounts = ["work"]allow_send = falseallow_destructive = falseThese controls reduce blast radius. They do not make email trustworthy. Every email field and attachment remains untrusted data, so an agent must never treat mail content as permission or instructions. See For agents for the operating rule and worked examples.
Practical advice
Section titled “Practical advice”- Use
--dry-runbefore any batch mutation. - Use app passwords or provider-specific credentials where your provider recommends them.
- Keep
secrets.tomlat mode0600, and keep any fallback keychain entries scoped to the accounts you use. - If an agent is involved, prefer workflows that search, read, export, and draft before workflows that mutate.
Supplied HTML
Section titled “Supplied HTML”mxr accepts a designed HTML email and preserves it exactly. That means it does not sanitise it either, so validation is a gate rather than a filter:
- Active content —
<script>,<object>,<embed>,<applet>,<iframe>,<form>, inlineon*handlers,javascript:/vbscript:URLs,<style>blocks containingexpression(), and anything of the sort hidden inside a conditional comment — is reported and refused. The draft is not created and the document is not modified. data:URLs are allowed only fordata:image/*.- The check runs in the daemon, not only in the CLI, so an IPC client cannot route around it.
- Validation parses with html5ever to inspect the document. A hostile document could in principle be parsed differently by a specific mail client, so this is a strong gate rather than a proof.
Content IDs supplied via --inline are restricted to letters, digits and
. _ - +, which keeps a CR/LF out of a Content-ID header.
Web previews of an HTML draft render in a sandboxed frame with a policy that blocks remote loads, so opening a draft does not phone home to a sender’s server.
What HTML drafts put in the activity log
Section titled “What HTML drafts put in the activity log”The activity log records the content kind (markdown or html) and the number
of inline assets. It does not record the HTML, the text alternative, inline
asset paths, recipient addresses, or any template property value. See
the activity log.