---
title: Set up Email Inbound (receive email into Claude sessions)
---

# Set up Email Inbound

## What it is

Omniscio can receive email at a dedicated inbox, run each message through a safety classifier, and either spawn a new Claude session for it or continue an existing email-thread session. Replies the agent generates go back out as email on the same thread. This is the inbound side of email — distinct from [Email Summarizer](email-summarizer.md) (forwarded-newsletter digests) and from [Gmail integration](gmail-integration.md) (triaging your existing Gmail inbox inside Omniscio).

The inbox lives at **AgentMail**, a third-party email-for-agents service. Omniscio polls it every 30 seconds via the AgentMail HTTP API, so Omniscio must be running for new email to land in a session. The polling cadence, concurrency cap (max 10 simultaneous sessions), and new-thread rate limit (max 100 brand-new threads per rolling hour) are all hardcoded — no configuration needed. An email that arrives while the hourly limit is full is not dropped: it waits and is picked up on a later check once the hour frees up.

Three things have to be in place before email starts flowing:

1. An **AgentMail account + inbox** (created at agentmail.to, outside Omniscio).
2. An **AgentMail API key** stored in your **OS credential store** (Windows Credential Manager / macOS Keychain / `AGENTMAIL_API_KEY` env var on Linux). Omniscio has no UI for this — you provision it once with a one-line command.
3. The **inbox address pasted into Omniscio** at Settings → Email Inbound, plus optional sender + token gating.

The split exists because the API key authenticates Omniscio to AgentMail (so Omniscio can poll your messages) while the inbox address tells Omniscio which mailbox to watch. Storing the key in the OS credential store keeps it DPAPI-encrypted at rest and out of `config.json`, so it never lands in a backup or sync target by accident.

## Where to find it

### How to use it

### 1. Create an AgentMail inbox

Sign up at agentmail.to and create an inbox. The address looks like `your-name@agentmail.to`. AgentMail has a free tier that's enough to evaluate the feature. Copy the **API key** from your AgentMail dashboard — it starts with `am_us_...`. You'll paste it into the OS credential store in the next step, never into Omniscio's settings.

### 2. Store the API key in your OS credential store

This is a one-time provisioning step and Omniscio has no UI for it. Pick the command for your platform:

**Windows (PowerShell or Git Bash):**

```bash
echo "am_us_..." | powershell.exe -NoProfile -File "$HOME/.claude/skills/agentmail/credential.ps1" store
```

The `credential.ps1` script ships with the bundled `agentmail` skill at `~/.claude/skills/agentmail/credential.ps1`. It writes to Windows Credential Manager via direct P/Invoke into `advapi32.dll` (CredWrite) — no PowerShell modules needed. The credential is DPAPI-encrypted by Windows and keyed to your login. Verify it landed with:

```bash
powershell.exe -NoProfile -File "$HOME/.claude/skills/agentmail/credential.ps1" exists
```

The `exists` subcommand prints `yes` or `no` and never the value. To rotate the key later, re-run `store` with the new value — it overwrites in place.

**macOS:**

```bash
security add-generic-password -s AgentMail -a api-key -w "am_us_..."
```

This stores the key in your login Keychain. Verify with `security find-generic-password -s AgentMail -a api-key -w` (prints the value).

**Linux:**

There's no credential-store fallback on Linux — set the `AGENTMAIL_API_KEY` environment variable in whatever environment Omniscio inherits (e.g. `~/.profile`, your launcher's `Exec=` entry, a systemd `Environment=` directive). Omniscio reads it on startup.

### 3. Paste the inbox address into Omniscio

1. Open **Settings → Email Inbound** (gear icon, or the Settings virtual project in the sidebar).
2. Flip **Email Inbound** ON.
3. Paste your AgentMail inbox address (e.g. `your-name@agentmail.to`) into the **Inbox ID** field.
4. Click **Test**. Omniscio calls `listMessages` against AgentMail using your stored API key, which simultaneously proves the key works AND that the inbox exists. A green "Connected successfully" confirms both. A red "Could not reach AgentMail service" means the key is missing, invalid, or doesn't own that inbox — see Troubleshooting below.

### 4. (Recommended) Restrict who can send

Two filters run in front of the safety classifier — neither is required, but you should configure at least one for any non-trivial use:

- **Approved Senders**. Empty list = open intake (anyone who emails the inbox is processed). Add specific addresses to restrict; everyone else is silently dropped before the classifier ever runs.
- **Inbox Secret Token**. Click the generate button to create a random token. Approved senders must include `[MC:<token>]` somewhere in the subject line — emails without it are silently dropped. Without a token, sender-address spoofing is your only barrier; with a token, an attacker has to know both the address you allow AND the token. The token applies to this AgentMail inbox only: mail to your Omniscio agent address is checked by that address's own sender rules instead, and the Gmail bug-report channel never looks at the token.

The setting card warns you in amber if no token is configured.

### 5. (Optional) Tune the safety classifier

Below the sender filters is a **Prescreen Rules** textarea — the safety-classifier prompt (run on OpenRouter’s `google/gemini-2.5-flash-lite`, a fixed model pin, auto-falling back to Claude Haiku 4.5) that decides `safe: true / false` on every email before any agent sees the body. The textarea is pre-filled with the default CORE rules (block prompt-injection, exfiltration, financial actions, RCE; approve bug fixes, code review, doc edits, etc.). Edit in place; saves on blur. The JSON output contract is appended at runtime and can't be edited away. Full details: [email-inbound-prescreen.md](email-inbound-prescreen.md).

### 6. (Optional) Route bug reports to a specific project

If you want testers to email bug reports straight into a project's session, use the slug-routing layer on top of email inbound:

1. Click the pencil icon on the project row → **Edit Project**.
2. Toggle **"Auto-triage incoming bug reports"** ON.
3. Set a slug (e.g. `amc`).

Tester emails with subject `[BUG: amc] ...` or `[FR: amc] ...` now spawn a new Claude session directly in that project, with the body fenced as untrusted external data and the agent instructed to investigate read-only and wait for approval before any code change. Full details: [bug-report-intake.md](bug-report-intake.md).

## How it behaves

### Attachments

When you forward an email that has attachments, Omniscio tries to deliver every attachment to Claude **with the same fidelity as the chat composer** — no setup required, no separate toggle. Images arrive as native vision content blocks, PDFs arrive as native document content blocks (also vision-rendered), and text/Office documents land in the session workdir with their paths prepended to the prompt. Anything that can't be delivered (oversized file, format the vision API doesn't accept, download failure) is silently dropped from the payload AND surfaced to Claude in a short "Note: N attachments could not be included:" line at the bottom of the email — so neither the user nor the agent silently loses information.

The three delivery routes match [chat-attachments.md](chat-attachments.md) one-for-one — same partition logic, same Anthropic API content blocks, same workdir-save mechanics — so anything documented there about how Claude perceives a given file type holds for the email path too.

| Attachment type                                                                 | What Claude sees                                                                                    | Where the bytes go                                                                                                                           |
| ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Image (PNG / JPEG / GIF / WebP)                                                 | `image` content block (base64 inline, vision)                                                       | Inline only — not saved to workdir                                                                                                           |
| PDF                                                                             | `document` content block (base64 inline, vision)                                                    | Inline + saved to `<workdir>/.claude/amc-attachments/<sessionId>/` so Claude can also `Read` / edit                                          |
| Text doc (`.md` `.txt` `.csv` `.json` `.html` `.xml` `.tsv` `.log` `.markdown`) | A line in the prompt prefix: "Please examine the following file(s): `<path>`"                       | Saved to workdir; only the path is in the prompt                                                                                             |
| Office doc (`.docx` `.xlsx` `.pptx` + legacy `.doc/.xls/.ppt`, ODF `.odt/.ods/.odp`, `.epub`, `.rtf`) | Markdown extracted in main via the anydoc reader, saved as `<base>.md` next to the original; that `.md` path is in the prompt | The extracted `.md` lives in workdir; the original binary is not preserved (different from the chat path, which keeps both as chip metadata) |

**Caps and the per-attachment ceiling.**

- **30 MB per image / 32 MB per document** — the same Anthropic vision API limits the chat composer enforces. Anything bigger is skipped with reason `oversize_image` or `oversize_document`.
- **10 attachments per email** — a hard cap to keep a single malformed email (most often an HTML newsletter with dozens of CID-embedded signature graphics, tracking pixels, and themed icons) from blowing up the prompt. When the inbound message exceeds the cap, Omniscio sorts by `Content-Disposition` first — explicit `attachment` parts win over `inline` parts, since the user almost certainly meant to forward the real payload rather than the email's chrome — then keeps the first 10. Everything past the cap is skipped with reason `too_many_attachments`.
- **Image format whitelist** — the Anthropic vision API only accepts PNG, JPEG, GIF, and WebP. HEIC (iPhone default), SVG, BMP, TIFF, and AVIF are dropped at this layer with reason `unsupported_image_format` rather than rejected by the model after spawn. If you want Claude to look at a HEIC photo, convert it to JPEG or PNG before forwarding.
- **All office formats extracted** — legacy binary Office (`.doc` / `.xls` / `.ppt`), OpenDocument (`.odt` / `.ods` / `.odp`), `.epub`, and `.rtf` now extract to Markdown via the on-device **anydoc** reader (the same engine + shared partition the chat path uses), not just the modern `.docx` / `.xlsx` / `.pptx`. A forwarded office file the reader can't parse surfaces as `office_extract_failed` (next bullet) rather than being dropped.
- **No-text-extracted Office files** surface as `office_extract_failed` — the chip path on chat would keep the binary as a chip, but the email path has nowhere to surface a chip, so the skip note is the only signal.

**What the agent sees when something is skipped.** The session prompt ends with a literal note listing every skipped attachment, one per line, with the filename and a stable reason token:

```
Note: 3 attachments could not be included:
- big.png (oversize_image)
- old.doc (unsupported_legacy_office)
- art.heic (unsupported_image_format)
```

So if a user forwards an email saying "look at the screenshot I'm attaching" and the screenshot was 50 MB, the agent will know to ask the user to re-send a smaller version rather than guess at what was missing.

The full set of skip reason tokens is: `too_many_attachments`, `download_failed`, `oversize_image`, `oversize_document`, `unsupported_image_format`, `unsupported_legacy_office`, `unsupported_type`, `office_extract_failed`, `workdir_save_failed`.

**One bad attachment never sinks the email.** Each attachment is downloaded and processed independently — a 502 from AgentMail's CDN on attachment #3 surfaces as a single `download_failed` skip entry, and attachments #1, #2, #4… still flow through normally. The session still spawns; the email body still reaches Claude.

### Troubleshooting

| Symptom                                                             | Cause                                                         | Fix                                                                                                                          |
| ------------------------------------------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| **Test button shows "Could not reach AgentMail service"**           | API key missing or invalid                                    | Re-run the `credential.ps1 store` (or `security add-generic-password`) command in step 2. Confirm with the `exists` command. |
| **Test button shows "401 / 403"**                                   | Key is valid but doesn't own that inbox                       | The key and inbox must be from the same AgentMail account. Re-copy the key from your AgentMail dashboard.                    |
| **Email Inbound is on but nothing arrives**                         | Omniscio isn't running, or the polling task hasn't ticked yet | Wait 30 seconds. Confirm Omniscio is open. Check the Omniscio log for `[EmailInbound]` lines.                                |
| **Approved sender's email is dropped silently**                     | Subject doesn't include `[MC:<token>]`                        | Either tell the sender to include it, or clear the Inbox Secret Token field to disable token gating.                         |
| **`forward_email` automations fail with "AgentMail API key" error** | Same credential store; same key                               | Same fix as the Test button — provision the key per step 2.                                                                  |
| **Key was rotated but Omniscio still uses the old one**             | In-process cache holds it until the next 401/403 clears it    | Either restart Omniscio, or wait for the next AgentMail call to fail-then-retry with the new key.                            |

### What this does NOT cover

- **Existing-Gmail-inbox triage.** That's a separate feature — see [gmail-integration.md](gmail-integration.md). Gmail integration uses your Google OAuth, not AgentMail.
- **Forwarded-newsletter digests.** That's [Email Summarizer](email-summarizer.md), which uses the same AgentMail inbox but a different processing pipeline (Haiku summary on a thread, not session spawn).
- **Outbound `forward_email` automations.** They use the same API key, but the rule structure is documented in [forward-email-transport.md](forward-email-transport.md).
- **AgentMail account / billing.** Sign-up, plan, quota, and inbox management all happen at agentmail.to. Omniscio has no UI for any of it.

## For agents

### How it works

**API key lookup chain** ([agentmail-client.ts:357](../../src/main/services/agentmail-client.ts#L357), `getApiKey()`). On first AgentMail call after Omniscio starts, the client checks `process.env.AGENTMAIL_API_KEY` first (used by dev/CI), then falls back to the OS credential store (`credential.ps1 read` on Windows, `security find-generic-password` on macOS, no fallback on Linux). The result is cached in-process. A 401/403 response clears the cache via `clearApiKeyCache()` so a rotated key gets picked up on the next call without an Omniscio restart.

**Polling loop** ([email-inbound-service.ts](../../src/main/services/email/email-inbound-service.ts)). `startEmailInboundService()` schedules a periodic task at `POLL_INTERVAL_MS = 30_000` (30 seconds). Each tick: `listMessages` since the last cutoff (with a 2-minute lookback overlap to catch indexing lag), dedup against `email_inbound_processed`, prescreen, and either continue an existing session by `thread_id` or spawn a new one. Concurrency is capped at `MAX_CONCURRENT_SESSIONS = 10`; brand-new thread IDs are capped at `MAX_NEW_THREADS_PER_HOUR = 100` rolling-window via the `NewThreadRateLimiter`.

**Settings keys** (all in [email-intake-settings.ts](../../src/shared/types/settings/email-intake-settings.ts) — one Zod schema that derives the type, defaults, and update shape, folded into `AppSettings`): `emailInboundEnabled` (`false`), `emailInboundInboxId` (`''`), `emailInboundApprovedSenders` (`[]`), `emailInboundSecretToken` (`''`), `emailInboundPrescreenPrompt` (`''` — empty means use default CORE rules).

**Pre-flight gate**. The CLI control server's `forward_email` automation action also reads the AgentMail key via `isApiKeyAvailable()` — failing with a 409 response if the key isn't in the credential store. So the same provisioning step in this document also unlocks AgentMail-routed outbound automations. See [forward-email-transport.md](forward-email-transport.md).

**UI**: Settings panel in [/src/renderer/src/features/settings/EmailInboundSettings.tsx](../../src/renderer/src/features/settings/EmailInboundSettings.tsx). Test-Connection IPC handler in [/src/main/ipc/email-inbound-handlers.ts](../../src/main/ipc/email-inbound-handlers.ts) — it calls `listMessages(inboxId)` so a successful Test simultaneously proves the key works AND the inbox exists.

**Attachments pipeline**. `processEmailAttachments()` in [/src/main/services/email/email-inbound-attachments.ts](../../src/main/services/email/email-inbound-attachments.ts) is the single entry point. It applies the disposition-priority sort + 10-cap before downloading, fans out the downloads in parallel via `Promise.allSettled` (so one 502 doesn't sink the rest), routes each downloaded attachment through the same [`partitionAttachments`](../../src/main/services/attachment-partition.ts) the chat path uses, validates against Anthropic's image format whitelist + size caps, runs `extractOfficeText` for modern Office files, saves PDFs/text-docs/extracted-Office to workdir via [`saveDocToWorkdir`](../../src/main/services/workdir-attachment-store.ts), and returns `{ images, documents, savedDocPaths, delivered, skipped }`. `createNewSession()` in [/src/main/services/email/email-inbound-service.ts](../../src/main/services/email/email-inbound-service.ts) calls it before spawn, threads `images`/`documents` into the launch call's positional args, prepends `savedDocPaths` to the prompt via [`buildPromptWithAttachments`](../../src/main/services/image-store.ts), and hands the `skipped` array to [`buildSessionPrompt`](../../src/main/services/agentmail-client.ts) so the "Note: N attachments could not be included:" line renders below the email body. AgentMail's `get-attachment` returns a small JSON envelope with a signed CDN URL (not the bytes) — `downloadAttachment()` in [agentmail-client.ts](../../src/main/services/agentmail-client.ts) handles the second-leg fetch with a proportional `AbortController` timeout.

## Related

- [chat-attachments.md](chat-attachments.md) — the parallel desktop-composer attachment routing; same three-tier partition + content-block delivery used by the email path
- [email-inbound-prescreen.md](email-inbound-prescreen.md) — editing the OpenRouter `google/gemini-2.5-flash-lite` safety classifier prompt
- [bug-report-intake.md](bug-report-intake.md) — slug-based routing of bug emails into a specific project's session
- [email-summarizer.md](email-summarizer.md) — auto-summarizing forwarded newsletters (shares the AgentMail inbox + API key)
- [forward-email-transport.md](forward-email-transport.md) — outbound `forward_email` action with `transport: 'agentmail'`
- [gmail-integration.md](gmail-integration.md) — separate feature: triage your Google Gmail inbox inside Omniscio
