Agent-First Mail

Let your AI agent work your inbox — email pulled into plain files it reads, sorts, and drafts on your machine, with nothing sent until you confirm.

Ask your agent: “Sort this week’s inbox and draft replies to anything urgent.”

Agent-First Mail — the afmail command — isn’t an email client, a helpdesk app, or an auto-replying bot. It’s the workspace your agent operates in. It borrows the git shape — pull from a remote, work in a local worktree, push deliberately — so there is always a clear line between reading your mail and changing it.

Supported platforms: macOS, Linux, Windows.

How it works: one email, start to finish

You don’t run afmail — your agent does. You ask for what you want in plain language, and afmail is the tool it reaches for. The commands below are the agent’s tool calls, not something you type.

Say a mailbox is already configured (one folder per account, like a git worktree). You ask:

You: Check my latest email, go through it, and handle anything urgent.

The agent decides how to do that on its own. It starts by investigating — all read-only and within the explicit request to check current remote mail: it pulls the mailbox into local files, sees what came down, opens a case for the refund request, and reads what it actually says:

afmail status                                            # local counts and latest pull/push progress
afmail push list                                         # pending real-mailbox effects
afmail pull                                              # bounded remote read → local files
afmail triage list                                       # locators for what came down
afmail case create --name "Refund request" --group support \
  --message message_inbox_88213_4 --summary "refund request for late order" --reason "wants a refund"
afmail message show message_inbox_88213_4                # read the request: order #4471, arrived late

Fresh pulls default to the last 90 days, at most 500 new messages and 512 MiB across all selected mailboxes. Use pull --dry-run to estimate candidates without downloading bodies or changing workspace bytes; use --all-history only for an explicitly reviewed full-history import. Explicit --max-new-messages and --max-total-bytes remain hard command-wide budgets.

The mailbox workspace is designed to be kept in your own private Git repository. Raw .eml evidence, canonical metadata, generated read views, attachments, queued effects, and audit files are normal workspace state that private Git can version, sync, and back up. That is a Git trust boundary, not an encryption boundary: anyone with repository access, host backups, local machine access, or retained Git history may still be able to read the mail evidence. Deleting files from the worktree does not erase old commits. Service credentials are different from mail evidence; private Git support for mailbox state does not mean passwords or API secrets should be committed.

To restore or move a mailbox workspace, clone or fetch the private repository, then run afmail status from the workspace to verify the workspace_uid, local counts, and storage.*_bytes summary. Run afmail doctor before making new changes or pushing remote effects; it checks canonical files, incomplete local transactions, pending idempotency records, contact index conflicts, and retained push/audit recovery state. This is a validation flow over the existing workspace, not a separate restore command.

Reading the email tells it what’s being claimed. To check whether the claim holds, it steps outside afmail to another skill — your store’s transaction system — and looks the order up. That’s still read-only, so it still needs no permission. afmail neither knows nor cares what that skill is: it holds the mailbox while your agent brings the rest of the toolbox.

# the agent's store / transaction skill — not afmail:
get order #4471      → 9 days late, $79.90, refund-eligible

Now, before changing anything, it comes back with what it found and what it recommends:

Agent: One email needs a decision. A customer wants a refund on order #4471. I pulled it up: it arrived 9 days late, it’s $79.90, and it’s refund-eligible. I’d refund the full amount, reply to confirm, then archive the thread. Go ahead?

You: Ok, go ahead.

With that approved, the agent issues the refund — which never touches afmail, just the store’s own API:

refund order #4471   → refunded $79.90 to original card (transaction rf_8c21)

With the money back, the agent turns to afmail. It drafts the reply confirming the refund and queues it — still local, nothing sent yet:

afmail case draft reply c20260606001 message_inbox_88213_4 --body-file refund-reply.txt
afmail case draft send c20260606001 reply-message_inbox_88213_4.md # queue the reply

Replies go out under an explicit send identity you’ve configured, and any persona footer is written into the draft for review rather than appended secretly at send time.

If the queued reply needs correction before sending, edit the draft Markdown or use draft change; the queued item still points at the latest draft content:

afmail case draft change c20260606001 reply-message_inbox_88213_4.md --body-file revised-reply.txt

Edits are allowed until preview. Once afmail push returns a plan_id, changing the draft, attachment bytes, identity, queue, or action configuration invalidates that reviewed plan; afmail requires a new preview instead of silently sending newer content under an older approval.

Draft Markdown has a marked compose section so afmail can distinguish the text you wrote from template headings, quoted history, and the identity footer. An unfinished draft may be uploaded to the remote Drafts mailbox, but it cannot be queued for sending until that compose section contains authored text. The local state is then queued_send, not sent; afmail revalidates the latest file at push time and strips editor-only markers from the outgoing plain-text message.

Pushing is the moment your real mailbox changes. Bare afmail push is a pure preview containing the complete resolved queue and a workspace-bound plan_id; only --confirm PLAN_ID can apply that exact snapshot. So the agent stops and shows you the draft and all other effects before anything goes out:

Agent: Refund’s done. Here’s the reply before I send it:

Hi — sorry your order arrived late. I’ve refunded the full $79.90 to your original card; it should land in a few days. Thanks for your patience.

Send it and archive the thread?

You: Looks good — send it.

Only now, with the draft approved, does it push:

afmail case archive c20260606001 --reason "refunded order #4471, replied to customer"
afmail push                                              # review the full plan and copy plan_id
afmail push --confirm push_plan_<REVIEWED_PLAN_ID>       # apply only that exact plan

If the case still contains queued send drafts or a remote archive effect, case archive returns lifecycle archiving and queues the remote archive as its own push item. Its workspace is held under archived-cases/, but it does not become lifecycle archived or receive archived_rfc3339 until the send and archive pushes both succeed.

If a confirmed push exits with code 3 and outcome: partial, do not blindly repeat the whole push. Inspect failures, partially_applied_count, remaining_count, and audit_warnings, then compare afmail push list with afmail status before deciding whether another confirmed push is safe. A remote_outcome_unknown item must never be retried automatically. If a remote effect succeeded but its audit append did not complete, the queue retains an audit_pending item. The next preview marks it effect_scope: local_recovery; after review, confirming that new plan only completes the idempotent audit append and queue cleanup, and never repeats SMTP or IMAP effects.

Then it reports back:

Agent: Sent and archived. That was the only message that needed a reply. Here’s everything else in the inbox and what I’d do with each:

  • “Your account is suspended — verify now” — phishing. The link points to secure-paypa1.com (a look-alike, “1” in place of the “l”), not the real site, and it pressures you to log in. I’d mark it junk.
  • “RE: unpaid invoice” with a .zip attachment from a Gmail address, no prior thread — almost certainly malware. Junk.
  • “Order #4490 is out for delivery” — a routine shipping notification from the store, nothing to do. Archive.
  • “Payout of $2,140 sent to your bank” — your weekly Stripe payout receipt; worth keeping for records. Archive.

Want me to junk the first two and archive the rest?

That is the whole shape: you ask in plain language and the agent does the work in local files — investigating, proposing a plan, drafting — and nothing reaches your real mailbox until you’ve reviewed the resolved plan and it runs afmail push --confirm PLAN_ID with that exact ID.

The exact permission boundary for local reads, remote reads, local changes, remote effects, and permanent deletion is the authorization contract. Loading the afmail skill is automatic when an installed agent recognizes a mail task; loading it grants no additional permission.

Nothing touches your real mailbox until you confirm

Everything your agent does by default is local: replying writes a draft, archiving moves a local folder, marking spam/trash removes it from triage and adds it to generated spam/ or trash/ review views, and remotely deleted mail is retained under generated deleted/ views. afmail purge only previews the old local copies it would permanently remove; deletion requires rerunning it with the exact returned --confirm PLAN_ID. Local filing only queues a remote effect. afmail push is like git push — it previews everything pending, and --confirm PLAN_ID is the one explicit plan-bound moment that reviewed snapshot is sent, moved, or flagged. Any relevant change makes the old ID fail before remote access. Every confirmed effect lands in an append-only audit log, so a suggestion is never mistaken for a change after the fact.

Bring your own skill: afmail is the workspace, your agent is the brain

afmail deliberately does not classify your mail, decide what matters, or write your replies. It gives any agent skill a stable, file-first mailbox to operate on, safely behind the push boundary — so you compose the behavior you want on top of it:

afmail stays deliberately small — it’s the mailbox substrate, and the intelligence is whatever skill you point at it.

What your agent gets to work with

A pull leaves behind a workspace that keeps active attention, finished work, and machine evidence in separate places:

That is the shape. The docs cover the exact files, fields, and commands.

Review with your agent

Review commands are blocking, local human-in-the-loop sessions for work that benefits from dense reading or an explicit decision. An agent starts one in the background. Before triage review, it saves one complete typed proposal with afmail triage suggestion set; the proposal lives in the message’s canonical JSON metadata, never in Markdown or a separate review file. Review reads those persisted proposals and, on submit, writes a hash-bound human_suggestion before returning its finite result. It never files a message, changes a case, queues a push, or touches the remote mailbox.

Case and notification suggestions use the same lifecycle in their canonical JSON through afmail case suggestion set|clear and afmail notification suggestion set|clear. Notes and draft fields remain canonical user content and autosave through the Workspace API under an exclusive lock and base-hash check; each result records those writes in saved_files. Review never pushes or sends mail. A new Agent suggestion replaces the previous round and clears its human response. Cancel or window dismissal leaves suggestion state unchanged. Deletion proposals that set allow_notes: true require a separate destructive confirmation in Review.

All review surfaces expose one top-right Done button. Case, notification, and draft Done is enabled only after every edit is saved; dismissing the window never rolls back an autosave. Triage uses every persisted Agent suggestion by default and only shows a “Message to agent” text box when the user opens “Disagree or write to the agent”. Notes and drafts show no save badge on open; save progress begins only after an edit, and Saved locally appears only after the canonical file write succeeds. A save error keeps Done disabled.

afmail case review CASE_REF opens the case and its notes; afmail notification review NOTIFICATION_REF opens a notification and its notes; afmail case draft review CASE_REF DRAFT_NAME opens one draft. Submit, cancel, or dismiss each session returns one structured result with its outcome, base hashes, and any saved_files. The agent must re-check triage hashes before applying a decision. The persisted human suggestion is feedback, not proof that the formal action ran and not authorization for push or send; those still use the normal commands and preview/confirm flow.

If a suggestion set or clear may be retried after interrupted output, use a stable global --request-id for that exact command and inputs. Case and notification review hashes include their current notes; editing notes therefore makes an older proposal stale and requires a fresh suggestion.

Use --mode lan with any review command to use a phone or another browser on the same trusted IPv4 local network. afmail prints a clickable, one-time review_url, waits for that review to finish, and expires the URL after 15 minutes. Review mode defaults to local; headless is available for render and test inspection. LAN mode uses local HTTP and must not be used on public or untrusted networks.

Review HTML uses the same MiniJinja system and the same typed render documents as generated Markdown. Export editable multilingual templates with afmail render templates; language entries live under templates/<language>/ and shared layouts under templates/shared/. Workspace Review templates may provide HTML and CSS but never JavaScript. See the template contract.

Typed local HTTP API

Trusted Agent runtimes and applications can use afmail through the same domain core over HTTP. afmail api serve publishes OpenAPI 3.2 plus standalone JSON Schemas and reuses the CLI’s workspace locks, transaction guard, persistent idempotency records, suggestion hashes, audit behavior, and push plan confirmation. It does not expose a shell or generic command endpoint.

Set a random 32–512-character bearer-safe ASCII credential, then start the API from a mailbox workspace:

export AFMAIL_API_ACCESS_TOKEN_SECRET='replace-with-a-random-32-character-value'
afmail api serve --mode local

The ready event prints the API and OpenAPI URLs without printing the credential. Use --mode lan only on a trusted IPv4 local network. For arbitrary-network access, keep loopback mode and use an established encrypted tunnel or TLS reverse proxy instead of exposing afmail directly. Human review remains an afmail-owned interactive UI action; OpenAPI does not replace it with a generic HTTP command.

See the HTTP API contract.

Adopt it: hand afmail to your agent

The fastest way to get going is to let your agent read what afmail is and set it up for you. Paste this to your agent:

Read what Agent-First Mail is at https://agentfirstkit.com/agent-first-mail and tell me in plain terms what it would do for me. If I want it, install it — the prebuilt package for the quick path, or build from source after a quick security review of the repo if you’d rather read what you run — then run afmail skill install so you follow its behavior rules. Then set up my mailbox: afmail uses one workspace directory per account, so make a folder, run afmail init in it, and help me connect my mailbox.

If it’s a fit, install it — a prebuilt package, or from source if you want to read it first:

# prebuilt binary
brew install agentfirstkit/tap/afmail   # macOS / Linux
scoop bucket add agentfirstkit https://github.com/agentfirstkit/scoop-bucket && scoop install afmail   # Windows

# or build from source after reviewing the repo
git clone https://github.com/agentfirstkit/agent-first-mail
cd agent-first-mail
cargo install --path . --bin afmail --locked --force

Then install the embedded Agent Skill so the agent follows afmail’s behavior rules, and restart the agent afterward so it reloads them:

afmail skill install --agent codex --scope workspace
afmail skill status --agent codex --scope workspace

Then it’s one workspace directory per mailbox: run afmail init in a fresh folder or afmail init email-a from your agent root, connect the account, and your agent works that mailbox from its files. init creates only mailbox-workspace files; it never installs a Skill or edits the Agent root’s AGENTS.md. Its structured created_files, changed_files, and next_steps fields drive setup without guessing config keys. After applying those steps, use afmail status for local readiness and afmail remote test for IMAP plus configured SMTP handshake/authentication. The remote test sends no mail. See the Workspace Model.

Docs

Agent-First Mail v0.7: Run It Again

v0.7 makes every mutation safe to retry: a --request-id turns any local change into an idempotent operation, remote effects only fire from a plan you already reviewed, and pulls run inside a byte-and-count budget you set up front.

Agent-First Mail v0.6: Fewer Doors

v0.6 collapses the separate archive subcommand into case, replaces archive/reopen cycles for contacts and cases with explicit delete, and splits message restore into unspam and untrash — fewer places to look, more precise actions.

Agent-First Mail v0.5: Notifications Aren't Cases

v0.5 splits the two things afmail used to call 'archive': finished cases stay in the archive, but standalone notification and reference mail now files into its own notification collections — and every case has to name the queue it belongs to.

Agent-First Mail v0.4: The Workspace Speaks Your Language

v0.4 localizes the part of afmail you actually read: every generated Markdown view and workspace scaffold now ships in nine fully-translated language packs, chosen by one config line — while the CLI, the wire format, and the one push boundary stay exactly where they were.

Agent-First Mail v0.3: The Workspace Knows Who

v0.3 gives the mailbox workspace a sense of who: contact cards that auto-link to cases and materialise onto message views, and an identity registry of named send personas your drafts can write footers from — both still behind the one push boundary.

Agent-First Mail v0.2: One Push, All or Nothing

v0.2 tightens the push boundary into a single all-or-nothing operation, adds a draft preview, unifies message restore and lifecycle-agnostic case reads, lets you archive a message directly, and lets a workspace live in a subdirectory.

Agent-First Mail v0.1: Your Inbox as a Git Worktree

Agent-First Mail gives each mailbox a local, file-first workspace: pull mail into files, work cases and drafts offline, and push remote effects only when you explicitly confirm.