Config
Location
Section titled “Location”mxr resolves a runtime identity first, then places config, data, socket,
token, and bridge files under that identity. Release builds default to
mxr; debug cargo run defaults to mxr-dev; demo mode uses
mxr-demo. Override with MXR_INSTANCE only when you want an explicit
profile.
To inspect the resolved path:
mxr config pathmxr status --format jsonFor the active <instance>, the default roots are:
| Kind | Linux / XDG | macOS |
|---|---|---|
| Config | $XDG_CONFIG_HOME/<instance>/config.toml | ~/Library/Application Support/<instance>/config.toml |
| Data | $XDG_DATA_HOME/<instance>/ | ~/Library/Application Support/<instance>/ |
| Socket | $XDG_RUNTIME_DIR/<instance>/mxr.sock | ~/Library/Application Support/<instance>/mxr.sock |
MXR_CONFIG_DIR, MXR_DATA_DIR, MXR_TOKEN_DIR, MXR_SOCKET_PATH,
MXR_BRIDGE_TOKEN_PATH, and MXR_BRIDGE_PORT_PATH override individual
paths when needed.
Top-level sections
Section titled “Top-level sections”[general][render][search][search.semantic][snooze][logging][appearance][bridge][notifications.chimes][llm][llm.overrides.answer_coverage][safety.recipients][safety.tone][deliveries]
[accounts.work]Annotated example
Section titled “Annotated example”A working config that exercises every section. Drop into the path
that mxr config path prints, then adjust:
[general]editor = "nvim" # or "code -w", "subl -w", "$EDITOR"default_account = "personal" # used when commands omit --accountsync_interval = 60 # seconds; how often the daemon pollshook_timeout = 30 # seconds; max time a shell hook may runsafety_policy = "full" # full | restricted | draft-only | read-onlyattachment_dir = "~/mxr/attachments" # optional internal attachment cache overridedownload_dir = "~/Downloads" # default destination for user-initiated saves
[render]html_command = "w3m -dump" # how plain-text HTML view is renderedreader_mode = true # default to reader on openshow_reader_stats = truehtml_remote_content = true # allow remote images in HTML view; tracking pixels are still stripped
[search]default_sort = "date_desc"max_results = 200default_mode = "lexical" # lexical | hybrid | semantic
[search.semantic]enabled = trueauto_download_models = trueactive_profile = "bge-small-en-v1.5"max_pending_jobs = 256query_timeout_ms = 1500
[snooze]morning_hour = 9evening_hour = 18weekend_day = "saturday"weekend_hour = 10
[bridge]enabled = truebind = "127.0.0.1"port = 42829 # stable local web URL portcors_allowlist = []host_allowlist = []auto_local_token = true # loopback callers can auto-fetch the token# token_path = "/absolute/path/to/custom-bridge-token"
[notifications.chimes]enabled = false # opt-in audio feedback from the daemonvolume = 0.35 # 0.0 .. 1.0new_mail = "bell"sent = "sent"archived = "archive"trashed = "thud"spam = "alert"snoozed = "pop"unsnoozed = "glass"reminder = "bell"error = "alert"
[llm]enabled = falsebase_url = "http://localhost:11434/v1" # Ollama defaultmodel = "qwen2.5:3b-instruct"api_key_env = "" # name of env var; empty for Ollama / LM Studiocontext_window = 8192request_timeout_secs = 120
[llm.overrides.answer_coverage]# Optional per-feature LLM override. Same shape as [llm] above. Useful# when answer-coverage benefits from a different model than the default# (e.g. a smarter cloud model for the only LLM-backed safety check).# enabled = true# model = "gpt-5-mini"
[safety.recipients]internal_domains = ["company.com"] # paired with internal markers in bodysensitive_domains = ["competitor.com"] # always Blockerwarn_on_first_time_external = true # warn on never-seen-before domains
[safety.tone]formality_delta_threshold = 0.25 # 0.0 = always warn, 1.0 = never
[accounts.personal]name = "Personal"email = "me@example.com"enabled = true # per-account on/off; survives across daemon restarts[accounts.personal.sync]type = "gmail"credential_source = "bundled" # bundled | custom[accounts.personal.send]type = "gmail"
[accounts.work]name = "Work"email = "me@work.example.com"enabled = true[accounts.work.sync]type = "imap"host = "imap.work.example.com"port = 993username = "me@work.example.com"auth_required = trueuse_tls = true[accounts.work.send]type = "smtp"host = "smtp.work.example.com"port = 587username = "me@work.example.com"auth_required = trueuse_tls = truegeneral
Section titled “general”| Key | Type | Default | Purpose |
|---|---|---|---|
editor | string | $EDITOR | Used by mxr compose, mxr config edit, and the TUI compose flow |
default_account | string | first enabled | Account selected when commands omit --account |
sync_interval | integer (seconds) | 60 | How often the daemon polls each account |
hook_timeout | integer (seconds) | 30 | Max wall-time for a shell hook |
attachment_dir | path | <data_dir>/attachments | Internal cache for opened/inline attachments |
download_dir | path | platform downloads dir | Default destination for user-initiated attachment saves |
safety_policy | enum | full | Daemon-wide guardrail — see below |
safety_policy
Section titled “safety_policy”Caps which IPC categories the daemon will service. Useful when running mxr inside a CI sandbox or behind an agent you don’t fully trust:
| Value | Allows |
|---|---|
full | All categories (default) |
restricted | All except destructive / mutation IPCs |
draft-only | Read + compose; no provider sends |
read-only | Read-only IPCs only — no mutations, no sends |
“Read-only” covers every request that doesn’t change state: listing and
reading mail and threads, search and aggregation, exports, draft and reply
previews, local AI analysis (summaries, briefings, sender/relationship
profiles, humanizer scoring), and read access to the calendar-invite list,
the activity log, saved searches, and saved activity filters. Requests that
fetch remote content (HTML image assets, attachments) are not read-only
— they trigger network egress, so an agent under read-only can’t be used
to load tracking pixels. Every request is assigned its category by a single
exhaustive classifier in the daemon, so a new request type can never slip
through a policy by being forgotten in an allowlist.
The TUI greys out mutation actions when the policy disallows them.
agents.profiles
Section titled “agents.profiles”Profiles constrain non-human daemon clients by IPC origin. The daemon selects
[agents.profiles.agent] for IPC messages with source agent and
[agents.profiles.mcp] for messages from mxr mcp serve. If the matching
profile is missing, the daemon rejects the request before any provider call.
[agents.profiles.agent]safety_policy = "draft-only"allowed_accounts = ["work"]allow_send = falseallow_destructive = false
[agents.profiles.mcp]safety_policy = "read-only"allowed_accounts = ["personal@example.com"]allow_send = falseallow_destructive = falseFields:
| Key | Meaning |
|---|---|
safety_policy | Same enum as [general].safety_policy; default profile value is read-only |
allowed_accounts | Account keys, account ids, or emails this origin may touch |
allow_send | Required for SendStoredDraft, scheduled sends, and non-dry-run RSVP sends |
allow_destructive | Required for mutations outside read/draft/send buckets |
allowed_destructive_actions | Optional fine-grained allowlist within the destructive gate. When set, a destructive request is permitted only if its action is listed (and allow_destructive is still true). Empty/omitted = no per-action restriction. |
allowed_destructive_actions values: archive, trash, spam, move,
delete_label, remove_account, unsubscribe, redact_activity,
prune_activity. Benign mailbox mutations (star, mark read, label tagging)
are never restricted by this list — only the coarse allow_destructive
gate applies to them.
# An agent that may archive and unsubscribe, but never trash or delete.[agents.profiles.agent]safety_policy = "full"allowed_accounts = ["work"]allow_send = falseallow_destructive = trueallowed_destructive_actions = ["archive", "unsubscribe"]MCP tools also require explicit confirm=true before send or mutation tools
apply changes.
accounts
Section titled “accounts”Each [accounts.<key>] is a TOML subtable. Required:
name— display name in the sidebaremail— primary addressenabled(defaulttrue) — whenfalsethe daemon skips the account on sync. Useful for keeping a dormant account configured without paying its sync cost.
Sub-tables:
[accounts.<key>.sync]— inbound provider;type = "gmail" | "imap" | "outlook_personal" | "outlook_work" | "fake"[accounts.<key>.send]— outbound provider;type = "gmail" | "smtp" | "outlook_personal" | "outlook_work" | "fake"
The fake provider is a deterministic in-memory adapter used by mxr demo
and the test suite — useful when you want a dry-run install without real credentials.
Gmail sync provider
Section titled “Gmail sync provider”[accounts.personal.sync]type = "gmail"credential_source = "bundled" # bundled | customclient_id = "..." # only when credential_source = "custom"client_secret = "..." # only when credential_source = "custom"token_ref = "gmail:personal" # keychain entry name; auto-setcredential_source = "custom" is the official Gmail v1 recommendation: use
your own Google Cloud project’s client ID/secret. bundled uses the OAuth
client mxr ships when present; treat it as an unverified fallback that may show
Google’s warning screen.
IMAP sync provider
Section titled “IMAP sync provider”[accounts.work.sync]type = "imap"host = "imap.example.com"port = 993username = "me@example.com"auth_required = true # set to false for relays that pre-authenticateuse_tls = truepassword_ref = "imap:work"SMTP send provider
Section titled “SMTP send provider”Same shape as the IMAP block but with the SMTP host/port and
password_ref. auth_required = false is the rare relay case where
the SMTP server accepts the message without credentials.
render
Section titled “render”| Key | Type | Default | Purpose |
|---|---|---|---|
html_command | string | w3m -dump | Shell command to render HTML to plain text |
reader_mode | bool | true | Default to reader mode when opening a message |
show_reader_stats | bool | true | Display word/reading-time on opened messages |
html_remote_content | bool | true | Allow remote image fetches in HTML view; tracking pixels are still stripped |
search
Section titled “search”default_sortmax_resultsdefault_mode
Example:
[search]default_sort = "date_desc"max_results = 200default_mode = "lexical"default_mode may be lexical, hybrid, or semantic.
search.semantic
Section titled “search.semantic”[search.semantic]enabled = trueauto_download_models = trueactive_profile = "bge-small-en-v1.5"max_pending_jobs = 256query_timeout_ms = 1500enabledauto_download_modelsactive_profilemax_pending_jobsquery_timeout_ms
Current runtime meaning:
enabled = false- sync still prepares semantic chunks for changed messages
- embeddings are not generated
- dense retrieval stays off
enabled = true- mxr installs the active local model if needed
- generates embeddings from stored chunks
- rebuilds/uses the dense ANN index
- hybrid/semantic search falls back to lexical ranking if dense retrieval is unavailable or errors
The built-in default is enabled = true, but default_mode remains lexical. Semantic retrieval is used only for requests that ask for hybrid or semantic mode.
Current profiles:
bge-small-en-v1.5multilingual-e5-smallbge-m3
Notes:
- embeddings stay local
- OCR is not used for semantic indexing
- semantic readiness is opportunistic and must not block sync, read, send, or lexical search
max_pending_jobsandquery_timeout_msare currently parsed config fields, not active runtime guarantees yet
snooze
Section titled “snooze”morning_hourevening_hourweekend_dayweekend_hour
logging
Section titled “logging”levelmax_size_mbmax_filesstderrevent_retention_days
activity
Section titled “activity”Controls the user-activity log (user_activity table). Strictly local; never transmitted off-device. See the Activity Log guide for the full design.
[activity]enabled = truetrack_link_clicks = false # opt-in; URLs reveal a lottrack_subjects = truetrack_recipient_handles = truetrack_search_queries = true
[activity.retention]ephemeral_days = 30standard_days = 90important_days = 365enabled— global switch. Whenfalse, the recorder is spawned but everyrecord()call is a no-op.MXR_ACTIVITY=offis the env-var equivalent.track_link_clicks— recordlink.clickrows with the URL incontext_json. Defaultfalsebecause URL history is sensitive.track_subjects— keep email subjects incontext_json. Defaulttrue. Flip off if you’d rather not retain message subjects in the audit trail.track_recipient_handles— keepname/emailrecipient blocks incontext_json. Defaulttrue.track_search_queries— keep search query text verbatim. Defaulttrue. Flip off for high-sensitivity work.retention.ephemeral_days/standard_days/important_days— daily prune sweep hard-deletes rows older than this. Defaults 30 / 90 / 365.
Verify it’s wired:
mxr activity status # paused state + recent-30d row countmxr activity prune --before 30d --dry-runHard kill at startup:
MXR_ACTIVITY=off mxr daemon # recorder is spawned but writes are droppedappearance
Section titled “appearance”themesidebardate_formatdate_format_fullsubject_max_width
bridge
Section titled “bridge”HTTP bridge configuration.
enabled— start the bridge alongside the daemon (defaulttrue).bind— bind address (default127.0.0.1).port— fixed TCP port for the local web URL (default42829).mxr webfails onEADDRINUSEby default and can opt into walking up with--auto-port. The actual bound port is written to<config_dir>/bridge-portfor clients to discover.cors_allowlist— additional origins (defaults already cover loopback).host_allowlist— additional hostnames for non-loopback binds.auto_local_token— whentrue(default),GET /api/v1/auth/local-tokenreturns the bridge token to callers whose TCP peer is a loopback IP. Lets the web SPA bootstrap on the same machine without a paste prompt. Set tofalsefor paranoid setups that want strict bearer auth even on loopback. Non-loopback peers never receive the token regardless of this setting.token_path— path to the auth token file. Omit it to use<config_dir>/bridge-tokenfor the active runtime identity.
notifications.chimes
Section titled “notifications.chimes”Daemon-side audio feedback for local events and successful actions. Chimes are off by default. Manage them without restarting the daemon:
mxr chimes status --format jsonmxr chimes enablemxr chimes set archived glassmxr chimes test archivedmxr chimes disableSupported events: new-mail, sent, archived, trashed, spam,
snoozed, unsnoozed, reminder, error.
Supported sounds: none, bell, glass, pop, sent, archive,
thud, alert.
Optional LLM features (thread summarisation, draft assist). Disabled by default. Speaks the OpenAI Chat Completions schema, so any of these backends works:
- Ollama (local):
http://localhost:11434/v1, no API key - LM Studio (local):
http://localhost:1234/v1, no API key - OpenAI:
https://api.openai.com/v1,OPENAI_API_KEY - Groq:
https://api.groq.com/openai/v1,GROQ_API_KEY - OpenRouter:
https://openrouter.ai/api/v1,OPENROUTER_API_KEY - Together AI, Mistral La Plateforme, Anthropic via OpenAI proxy, etc.
[llm]enabled = truebase_url = "http://localhost:11434/v1"model = "qwen2.5:3b-instruct"api_key_env = "" # name of the env var; empty = no auth headercontext_window = 8192request_timeout_secs = 120allow_cloud_relationship_data = falseThe API key is read from api_key_env at runtime and is never persisted
to the config file. Empty api_key_env means no Authorization header
is sent — correct for Ollama and LM Studio.
allow_cloud_relationship_data = false blocks relationship/profile context
from being sent to non-local LLM endpoints. Set it to true only when you want
cloud providers to receive that context for relationship-aware summaries,
briefings, and draft assistance.
Use mxr llm status to inspect the running provider, model, context
window, timeout, and whether the configured API-key environment variable
is present. Daemon config reloads rebuild the LLM provider, so changing
base_url, model, or api_key_env is reflected after reload without
restarting the process.
The web app’s Settings > LLM panel edits this same section through the daemon and reloads the provider immediately after save.
Per-feature overrides
Section titled “Per-feature overrides”Every LLM-backed feature can override [llm] independently. Useful
when one feature benefits from a different model — for example, you
might run thread summaries through a local 3B model but want
answer-coverage on a smarter cloud model.
[llm.overrides.answer_coverage]enabled = truebase_url = "https://api.openai.com/v1"model = "gpt-5-mini"api_key_env = "OPENAI_API_KEY"Any field omitted from an override falls back to the top-level [llm]
section. Feature keys: summarize, draft_assist, draft_new,
draft_refine, voice_match, answer_coverage, commitments,
delivery_extraction (confirm/enrich for delivery tracking).
safety
Section titled “safety”Pre-send safety pipeline tuning. See the Pre-send safety guide for the full check inventory and the override-token flow.
safety.recipients
Section titled “safety.recipients”[safety.recipients]internal_domains = ["company.com"]sensitive_domains = ["competitor.com"]warn_on_first_time_external = true| Key | Type | Default | Purpose |
|---|---|---|---|
internal_domains | string[] | [] | Domains flagged as internal. Body markers like INTERNAL or CONFIDENTIAL paired with a recipient OUTSIDE this list trigger a Blocker. |
sensitive_domains | string[] | [] | Any recipient at one of these domains is a Blocker. Common use: known competitors, regulators, journalists, ex-employers — anywhere a misfire would be expensive. |
warn_on_first_time_external | bool | false | Warn when sending to a domain you have no prior history with. Cheap nudge that catches “did I get the right alice?” cases. |
safety.tone
Section titled “safety.tone”[safety.tone]formality_delta_threshold = 0.25| Key | Type | Default | Purpose |
|---|---|---|---|
formality_delta_threshold | float [0.0, 1.0] | 0.25 | How different the draft’s formality score must be from the recipient’s baseline before the warning fires. 0.0 = always warn (every send vs. baseline). 1.0 = never warn. Lower it if you want stricter checking, raise it to mute noise. |
Tone match needs at least 3 prior messages to a recipient — fewer than that and the check silently skips (no point warning when there’s no stable baseline to compare against).
deliveries
Section titled “deliveries”Controls package/shipment detection. See the Deliveries guide for the full pipeline.
[deliveries]enabled = trueenabled— scan new mail for deliveries during the post-sync pass. Defaulttrue. Detection is local and cheap; turn it off to skip it entirely. The optional LLM confirm/enrich step is gated separately by[llm](and itsdelivery_extractionoverride) — with no LLM configured, detection still runs heuristics plus checksum-valid tracking numbers.
mxr deliveries scan --since-days 30 --dry-run # preview detection without writingmxr deliveries list # what's been foundCustom keybindings
Section titled “Custom keybindings”Default TUI keybindings can be overridden via keys.toml next to the active config file. Run mxr config path and put keys.toml in that directory. The file is split into three view contexts that match the TUI’s input router:
[mail_list]"j" = "move_down""k" = "move_up""6" = "open_tab_6" # bind Analytics to a digit (default is unbound)"Ctrl-Shift-A" = "open_tab_6""e" = "archive"
[message_view]"R" = "toggle_reader_mode""H" = "toggle_html_view"
[thread_view]"E" = "export_thread"Anything you don’t list keeps its default. To remove a binding, set it to "".
Key-string grammar
Section titled “Key-string grammar”- Bare characters:
"j","E","#",";","," - Modifier prefixes:
Ctrl-andCtrl-Alt-; bare uppercase letters imply Shift. - Special keys:
Enter,Esc,Escape, andTab. - Chords: concatenate (no separator) —
"gg","gi","zz". Chords are limited to two characters today.
Action names
Section titled “Action names”Bindings reference actions by name. Most TUI actions have a serializable name registered in crates/tui/src/keybindings.rs. As of today, ~47 of the ~122 internal Action variants are exposed as user-rebindable. The reachable names cover the common surfaces: navigation, mail mutations, screen switching (open_tab_1–open_tab_6), reader-mode toggles, and the standard chords.
If you bind a string the system doesn’t recognise, the daemon logs a warning at startup and silently keeps the default for that key. Run with mxr daemon --foreground while iterating to see the warnings.
Reading what’s bound
Section titled “Reading what’s bound”mxr --help # command surface# In the TUI:? # context-aware help modalCtrl-p # command palette searches by action nameThe palette is the fastest way to discover a binding while you’re using the TUI; the keybindings reference is the printable cheat sheet.
- Runtime account inventory is not identical to config entries.
- Gmail browser-auth accounts may exist at runtime without being editable config-backed entries.
- IMAP/SMTP entries are the main editable config-backed account type.