# Configuration reference

Settings live in `~/.config/inboxminder/config.toml`. Setup writes it,
Preferences and the sources editor update it through a validated funnel,
and it is safe to edit by hand. Every key is optional; a missing key takes
the default shown here. Secrets never go in this file. They live in the
macOS Keychain (see [Secrets](#secrets)).

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.

```toml
[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.

```toml
[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]

```toml
[engine]
enabled = true              # false = triage and labels only, no drafts
```

## [email]

```toml
[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](knowledge-sources.md).

```toml
[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-and-labels.md).

```toml
[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.

```toml
[labels]
enabled = true
autoResolve = false         # flip Pending to Resolved when your own reply is observed
pending = "Pending"
resolved = "Resolved"
```

## [drafts]

```toml
[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](drafting.md#per-sender-rules).

```toml
[[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](follow-ups.md).

```toml
[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](linear.md).

```toml
[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](drafting.md#canonical-answers).

```toml
[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]

```toml
[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](knowledge-sources.md#docs-gap-drafting).

```toml
[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.md).

```toml
[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](profiles.md).

## 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.
