The schema in src/config/schema.ts is the authority; a value it rejects fails loudly when the agent starts, with the reason in the log.
A settings change applies to the agent after a restart: Reinstall agent in the menu bar, or inboxminder agent install.
[llm]
The model that classifies mail and writes drafts.
[llm] provider = "anthropic" # anthropic | openai | google | openai-compatible model = "claude-sonnet-5-5" # baseUrl = "http://localhost:11434/v1" # openai-compatible only
openai-compatible covers Groq, OpenRouter, Ollama, LM Studio, vLLM and anything else that speaks the OpenAI API. baseUrl defaults to a local Ollama. Local servers need no key.
[embedding]
The model that turns your docs and sent mail into vectors for the index.
[embedding] provider = "openai" # openai | google | openai-compatible (not anthropic) model = "text-embedding-3-small" dimensions = 1536 # baseUrl = "http://localhost:11434/v1"
Changing any of these invalidates every stored vector. Edit the file, then run inboxminder index --full; content hashes are keyed on the embedding config, so everything re-embeds. A fully local setup pairs an OpenAI-compatible embedding model (for example nomic-embed-text on Ollama, dimensions = 768) with a local chat model.
[engine]
[engine] enabled = true # false = triage and labels only, no drafts
[email]
[email] pollIntervalSec = 45 # minimum 10 skipSenders = ["mailer-daemon"] # address substrings ignored with no classifier call notifications = true # macOS notifications; subjects only updateCheck = true # daily notify-only version check against the release feed maxContextChunks = 8 # knowledge passages per draft (1 to 100) maxStyleExamples = 3 # past replies used as style reference (1 to 100) maxThreadMessages = 4 # thread history in the prompt (1 to 100) classifyBeforeDrafting = true draftSelfNotifications = false # draft replies to mail your own address sends itself (contact forms) indexRefreshHours = 24 # periodic source re-index; 0 = startup only (max 720) sentBackfillLimit = 200 # sent messages per index run; older history backfills batch by batch (max 2000) automatedSent = "skip" # skip | flag | off: automated sent mail as a tone example
skipSenders matches on the address, never the display name.
[connectors]
What the index is built from. See Knowledge sources.
[connectors.files]
enabled = true
dirs = ["/Users/you/notes"]
[connectors.llmstxt]
enabled = false
urls = ["https://docs.example.com/llms-full.txt"]
[connectors.github] # cloned with your own git credentials; private repos work
enabled = false
repos = [{ url = "https://github.com/you/your-docs", branch = "main" }] # branch optional
[connectors.notion] # token: inboxminder set-key notion
enabled = false
[connectors.linear] # key: inboxminder set-key linear
enabled = false
[connectors.slack] # user token with channels:history: inboxminder set-key slack
enabled = false
channels = ["C0123456789"]
Notes written in the window and canonical answers are indexed too, with no connector section of their own.
[triage]
The gatekeeper. See Triage and labels.
[triage] enabled = true archive = [] # categories that also skip the inbox, e.g. ["newsletter", "marketing"] coldOutreachHint = "" # free text added to the cold-outreach criteria (max 500) [triage.labels] newsletter = "Newsletter" notification = "Notification" marketing = "Marketing" "cold-outreach" = "Cold Outreach" # the hyphen needs quotes in TOML fyi = "FYI" important = "Urgent" # urgent reply-worthy mail; "Important" is a Gmail system label and is rejected
Categories are fixed: newsletter, notification, marketing, cold-outreach, fyi. A "/" in a label name nests it in Gmail's sidebar. Any label name that collides with a Gmail system label (inbox, spam, trash, unread, starred, important, sent, draft, drafts, chat) is rejected at config load.
[labels]
Thread-state labels.
[labels] enabled = true autoResolve = false # flip Pending to Resolved when your own reply is observed pending = "Pending" resolved = "Resolved"
[drafts]
[drafts] signature = "" # appended after generation; the model never sees it (max 1000) style = "" # free-text tone, joins the standing instructions (max 2000) coverage = "all" # all | confident-only (skip only an explicit "unsure" verdict) polish = "tone" # tone | light: how far Polish in my tone may go deleteOnReply = false # your own reply deletes the app's unsent draft on that thread maxAgeDays = 0 # sweep the app's unsent drafts older than N days; 0 = never (max 90)
Only drafts the app itself created, and that Gmail confirms are still unsent, are ever deleted.
[instructions]
Per-sender steering. See Drafting.
[[instructions.rules]] match = "@acme.com" # substring of the sender ADDRESS (max 200) note = "Enterprise customer; always offer a call." # max 2000 [[instructions.rules]] match = "billing@" note = "Never quote prices; leave a [TODO]."
Up to 100 rules. Global standing instructions live in ~/.config/inboxminder/instructions.md, not in TOML.
[followup]
See Follow-ups.
[followup] enabled = false mode = "label" # label | time | both intervalDays = 4 # time and both modes maxFollowups = 1 # nudges per thread; 0 disables drafting scanMinutes = 15 # how often the label scan runs markUnread = false # mark a resurfaced thread unread (noisy on a shared inbox) readyLabel = "Ready for follow-up" # applied to every drafted nudge until it is sent, answered or swept [followup.labels] # label you add in Gmail -> quiet days; at most 8 "Follow up/3d" = 3 "Follow up/1w" = 7 "Follow up/2w" = 14
[tickets]
See Linear.
[tickets] enabled = false teamId = "" # the Linear team; Preferences fills it in resolutionDrafts = true # "it's fixed" draft when a linked ticket completes checkMinutes = 30 # how often linked tickets are polled [tickets.assignLabels] # Gmail label -> Linear account email; at most 16 "Assign/Priya" = "priya@example.com" [tickets.assigneeSuggestions] # topic label -> name written into the ticket description as a suggestion "Billing" = "Dan"
An empty assignLabels means no hand-offs. An owner is never inferred.
[answers]
See Drafting.
[answers] enabled = false dir = "" # a local folder of Q&A markdown snippets, e.g. "~/inboxminder-answers" maxPerDraft = 2 # 1 to 5 escalate = true # write <slug>.suggested.md for unanswered questions
[feedback]
[feedback] enabled = false # learn from the edits you make before sending maxCorrections = 2 # correction pairs per draft prompt (1 to 10) maxStored = 300 # corpus cap; oldest pruned (max 2000)
[docs]
See Knowledge sources.
[docs] enabled = false repoPath = "" # a local docs repository working tree; ~ allowed sources = ["llmstxt"] # which indexed sources count as "your docs" for the gap check instructions = "" # voice, structure, components, topics never to document (max 4000)
[mcp]
Config-file only. See Live lookups.
[mcp.servers.billing]
transport = "stdio"
command = "npx"
args = ["-y", "@acme/billing-mcp"]
envSecrets = { BILLING_API_KEY = "mcp-billing" } # env var name -> Keychain entry name
readOnlyAnnotationsOnly = true
allowTools = ["get_customer", "list_invoices"]
[mcp.servers.crm]
transport = "http"
url = "https://mcp.example.com"
headerSecrets = { Authorization = "mcp-crm" } # header name -> Keychain entry name
Store each referenced secret with inboxminder set-key <entry name>.
Secrets
Everything secret is in the macOS Keychain under the service inboxminder (a profile uses inboxminder.<profile>). Store or rotate one with inboxminder set-key <name>, then restart the agent.
| Entry | What |
|---|---|
anthropic, openai, google, openai-compatible | AI provider keys, by provider name. openai also serves OpenAI embeddings |
gmail-client-id, gmail-client-secret | Your Google Cloud OAuth client |
gmail-tokens | Gmail access and refresh tokens, written by auth |
license | Your license key, written by license activate |
notion, linear, slack | Connector tokens |
any name referenced by [mcp] | MCP server secrets |
In a profile, the Google client and provider keys fall back to the base service when the profile has none of its own; Gmail tokens, the license and connector or MCP tokens never do. See Profiles.
Files
| Path | What |
|---|---|
~/.config/inboxminder/config.toml | Settings (this file) |
~/.config/inboxminder/instructions.md | Standing instructions for every draft |
~/.config/inboxminder/notes/ | Notes written in the window |
~/.inboxminder/state.db | SQLite: threads, drafts, queue, dedup, activity |
~/.inboxminder/index/ | LanceDB vector index |
~/.inboxminder/status.json | The versioned status file the app renders |
~/.inboxminder/logs/watch.log | The log (pino JSON); watch.err.log holds crashes |
~/Library/LaunchAgents/com.inboxminder.agent.plist | The launchd agent |
INBOXMINDER_CONFIG_DIR and INBOXMINDER_DATA_DIR relocate the two directories for testing. A profile nests under profiles/<name> in each.