Skip to content

Recipes

mxr is a Unix citizen. Most list/search commands support --format json|jsonl|ids|csv|table; the core mail mutations support --dry-run and accept either explicit IDs, --search QUERY, or piped IDs on stdin. The exact per-command capabilities live in the automation contract; the JSON shapes are documented in JSON output schemas. This page is the cookbook.

Each recipe shows three things:

  1. Situation — the actual problem you’re solving.
  2. Pipeline — copy-pasteable, no shell prompt prefix.
  3. What you get — the shape of the output.

If you’d rather hand the situation to an LLM, every section ends with a Tell an agent block — a natural-language prompt that maps cleanly onto the same pipeline.

Situation: too many unread messages to scroll, but you know it’s “from someone at acme”.

Terminal window
mxr search 'is:unread from:acme' --format jsonl \
| jq -r '"\(.message_id)\t\(.from)\t\(.subject)"' \
| fzf --delimiter='\t' --with-nth=2,3 \
| cut -f1 \
| xargs -I{} mxr cat {} --view reader

What you get: an interactive list keyed by sender + subject; pressing Enter prints the chosen message body (rendered with reader mode) to your terminal. Use --view raw for the unrendered body, --view html to dump the original HTML.

Terminal window
mxr drafts --format jsonl recover \
| jq -r '"\(.draft_id // .id)\t\(.subject // "(no subject)")"' \
| fzf --delimiter='\t' --with-nth=2 \
| cut -f1 \
| xargs mxr drafts resume
Terminal window
mxr storage --by sender --format jsonl \
| jq -r '"\(.bytes)\t\(.key)"' \
| sort -rn \
| fzf --header='bytes | sender' \
| awk '{print $2}' \
| xargs mxr sender

What you get: pick the heaviest senders interactively; Enter drills into their full profile (volume, cadence, open commitments).

Tell an agent
"Help me find the sender I email most often that I haven't replied to in 30 days. List the candidates first; I'll pick one."
Terminal window
mxr search 'is:unread newer_than:1d' --format json \
| jq -r 'group_by(.from)
| map({sender: .[0].from, count: length, latest: max_by(.date).subject})
| sort_by(-.count)
| .[]
| "\(.count)\t\(.sender)\t\(.latest)"'

What you get: a per-sender digest, descending by message count, with the most recent subject for context.

Find threads that need follow-up (no reply in 7 days)

Section titled “Find threads that need follow-up (no reply in 7 days)”
Terminal window
mxr stale --theirs --older-than-days 7 --format json \
| jq -r '.[] | "\(.thread_id)\t\(.latest_subject)\t\(.latest_date)"'

--theirs means they owe me a reply (latest message is outbound). --older-than-days 7 excludes threads with activity in the last week.

Terminal window
mxr owed --format json \
| jq -r 'sort_by(-.overdue_score) | .[]
| "\(.overdue_score | tostring | .[0:4])\t\(.waiting_days|round)d\t\(.counterparty_email)\t\(.subject)"' \
| head -20

What you get: top 20 threads where you are the bottleneck, ranked by waiting_days / expected_days (using the recipient’s typical cadence; default 7 days when no history). Same set as mxr search 'is:owed-reply' — pick whichever surface fits your script.

Extract all attachment filenames from a query

Section titled “Extract all attachment filenames from a query”
Terminal window
mxr search 'has:attachment from:billing' --format ids \
| while IFS= read -r id; do
mxr attachments list "$id"
done
Tell an agent
"Summarize my unread mail from the last 24 hours grouped by sender. Use `mxr search 'is:unread newer_than:1d' --format json` and group with jq."
Terminal window
mxr search 'from:no-reply@*.example.com older_than:30d' --format ids \
| mxr archive --yes

--yes skips the confirmation prompt; needed when stdin is not a terminal.

Terminal window
mxr search 'from:spam@example.com' --format ids \
| mxr trash --yes
Terminal window
mxr search 'from:billing@*.example.com' --format ids \
| mxr label billing --yes

For mxr-on-mxr bulk actions, prefer piping IDs directly into the mutation. Use GNU parallel only when you fan out non-mxr work.

Always preview when piping into mutations:

Terminal window
mxr search 'from:no-reply' --format ids \
| mxr archive --dry-run

Every recipe here uses the pre-send safety pipeline. The --check flag runs every safety check WITHOUT sending, exits 2 on any Blocker, and prints a JSON report you can parse.

Block sends with leaked secrets (pre-commit hook)

Section titled “Block sends with leaked secrets (pre-commit hook)”

.git/hooks/pre-commit for a repo where you stash outgoing-mail templates:

#!/usr/bin/env bash
set -euo pipefail
for draft in mail/*.md; do
to=$(yq '.to' "$draft")
body=$(awk '/^---$/{n++;next} n==2' "$draft")
printf '%s' "$body" | mxr compose --to "$to" --body-stdin --check --format json | \
jq -e '
([.issues[] | select(.severity == "blocker") | .code]) as $blockers
| if $blockers | length == 0 then true
else error("blocked: \($blockers | join(\", \")) in \(input_filename)")
end' || exit 1
done

What you get: any commit that introduces a draft with a PEM private key, AWS/OpenAI/GitHub token, or other blocker-grade secret fails the hook before push.

Send only if safety is clean, otherwise mint and pause

Section titled “Send only if safety is clean, otherwise mint and pause”
Terminal window
report=$(mxr send "$DRAFT_ID" --check --format json)
verdict=$(echo "$report" | jq -r '.verdict')
case "$verdict" in
safe|warn) mxr send "$DRAFT_ID" ;;
blocked)
token=$(echo "$report" | jq -r '.issues[] | select(.severity == "blocker") | .override_token | select(. != null)' | head -1)
echo "BLOCKED. To override: mxr send $DRAFT_ID --override-safety $token"
exit 2
;;
esac

Audit every scheduled send before it fires

Section titled “Audit every scheduled send before it fires”
Terminal window
mxr drafts --format json | jq -r '.[] | select(.send_at != null) | .id' \
| while read draft_id; do
verdict=$(mxr send "$draft_id" --check --format json | jq -r '.verdict')
printf '%s\t%s\n' "$draft_id" "$verdict"
done

What you get: one line per scheduled draft with its current verdict. Catch drafts that would silently fail when the scheduler fires them (the scheduler clears the schedule on Blocker, so an unaddressed warning isn’t enough — only Blockers stop a scheduled send).

crontab -e:

0 8 * * 1-5 /usr/local/bin/mxr owed --format json | jq -r 'sort_by(-.overdue_score) | .[0:10] | .[] | " • \(.counterparty_email): \(.waiting_days|round)d — \(.subject)"' | { echo "Threads you owe (top 10):"; cat; } | mail -s "mxr: owed replies" you@example.com
Tell an agent
"Walk my `mxr search 'is:owed-reply'` set. For each thread, show me a
two-line summary, then ask if I want to reply (`r`), snooze a week
(`s`), or skip. Use `mxr summarize` for context. Never send without
showing me the body."

The core mail mutations (archive, trash, spam, snooze, label, etc.) support --dry-run and print what would happen without touching the provider.

Tell an agent
"Archive every unread newsletter older than 30 days. Show me the dry-run first; I'll approve."
Terminal window
watch -n 30 'mxr count is:unread'
Terminal window
watch -n 60 'mxr search "newer_than:5m" --format json \
| jq -r ".[] | .from" | sort | uniq -c | sort -rn | head'

What you get: a refreshing top-10 of senders pinging you in the last 5 minutes — useful during incidents.

Terminal window
watch -n 5 'mxr sync --status --format table'
Tell an agent
"Tell me when @ceo emails me. Poll every 30 seconds with `mxr search 'from:ceo@example.com is:unread' --format ids` and ping me on stdout when the result is non-empty."

crontab -e:

0 9 * * 1-5 /usr/local/bin/mxr search 'is:unread newer_than:1d' \
--format json | mail -s "Morning digest" you@example.com

Auto-snooze low-priority newsletters until weekend

Section titled “Auto-snooze low-priority newsletters until weekend”
0 17 * * 5 /usr/local/bin/mxr search 'label:newsletters is:unread' \
--format ids | /usr/local/bin/mxr snooze --until 'monday 9am' --yes

~/.config/systemd/user/mxr-cleanup.service:

[Service]
Type=oneshot
ExecStart=/bin/sh -c 'mxr search "from:no-reply older_than:90d" --format ids | mxr archive --yes'

~/.config/systemd/user/mxr-cleanup.timer:

[Timer]
OnCalendar=Sun *-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
Terminal window
systemctl --user enable --now mxr-cleanup.timer
Tell an agent
"Set up a weekly cron that archives no-reply mail older than 90 days. Show me the cron line; don't install it."
Terminal window
mxr search 'from:alice' --format ids \
| xargs -I{} mxr cat {} --view reader \
| $EDITOR - # paste content into editor as scratch

Open a draft directly in your editor (no daemon)

Section titled “Open a draft directly in your editor (no daemon)”

mxr compose already opens $EDITOR with the markdown + frontmatter shell. To prepare a one-off reply from a script:

Terminal window
mxr reply MESSAGE_ID --body-stdin <<'EOF'
Hey — quick yes from me. Will follow up tomorrow with the deck.
EOF
Terminal window
mxr replies --format ids | while read id; do
mxr cat "$id" --view reader
echo "Reply? [y/N/q]"
read answer < /dev/tty
case "$answer" in
y) mxr reply "$id" ;;
q) break ;;
esac
done

With grep / ripgrep — content search across exports

Section titled “With grep / ripgrep — content search across exports”
Terminal window
# Export everything from a sender, search the corpus locally
mxr search 'from:legal@example.com' --format ids \
| while IFS= read -r id; do mxr export "$id" --format markdown; done \
| rg -i 'NDA|confidential|terms'
Terminal window
# Fast full-text grep over message bodies via Tantivy
mxr search 'NDA OR "non-disclosure"' --format jsonl \
| jq -r '.subject + " — " + .from'

The second form is ~100× faster because it stays inside the search index. Use the first only when you need regex or ripgrep features the search grammar doesn’t support.

Fetch bodies for many threads concurrently

Section titled “Fetch bodies for many threads concurrently”
Terminal window
mxr search 'from:billing' --format ids \
| parallel -j8 mxr cat {} --view reader \
| rg -i 'amount due|invoice' \
| head
Terminal window
mxr accounts --format ids \
| parallel -j4 mxr sync --account {}

mxr is designed so agents can run it directly. Three rules keep the interaction safe:

  1. Read-only first. Have the agent run mxr search, mxr cat, mxr stale, mxr storage --by sender, mxr sender, mxr summarize before any mutation.
  2. Always --dry-run before bulk mutations. Every mutation supports it; the agent should preview the affected IDs and report them back to you.
  3. Use --format json or --format jsonl when piping into the agent’s reasoning loop, never table (that’s for humans).
  • “Show me the 10 senders I owe replies to. Use mxr stale and mxr sender to verify cadence.”
  • “Find every newsletter from this month, group by sender, and propose a label rule for the noisiest three.”
  • “Draft a polite decline to the latest message from acme.com, but show me the draft before sending.”
  • “Summarize unread mail from the last 24h grouped by importance. Use mxr summarize only on threads with 4+ messages.”
Terminal window
mxr search '<query>' --format json # full search
mxr cat MESSAGE_ID --view reader # rendered, distraction-free
mxr summarize THREAD_ID # LLM Markdown summary + next steps
mxr sender alice@example.com # per-sender aggregates
mxr stale --mine --older-than-days 7 --format json
mxr count <query> # count without payload
mxr drafts --format json # what's waiting
mxr doctor --format json # daemon health
Terminal window
mxr archive ID --dry-run # preview
mxr archive --search 'from:noreply older_than:30d' --dry-run
mxr archive --search 'from:noreply older_than:30d' --yes
mxr label <name> ID --dry-run
mxr snooze ID --until '...' --dry-run
mxr send DRAFT_ID --dry-run # preview send
mxr screener allow|deny|feed|paper-trail <addr>

The agent doesn’t need a special API. The same flags humans use are the same ones it composes.

With AI features — synthesis with citations

Section titled “With AI features — synthesis with citations”

”What did we decide about X last quarter?”

Section titled “”What did we decide about X last quarter?””

Situation: you need a grounded answer plus the messages that prove it.

Terminal window
mxr ask "what did Alice and I decide about pricing in Q2?" \
--from alice@example.com \
--after 2026-04-01 --before 2026-06-30 \
--format json \
| tee /tmp/answer.json \
| jq -r '.text, "\nCitations:", (.citations[] | "- \(.message_id)\t\(.subject)")'

What you get: the synthesized answer to stdout, JSON to /tmp/answer.json. jq prints the answer text followed by citation rows you can pipe back into mxr cat. If retrieval can’t support an answer the text is literally “not enough evidence” — no synthesized confidence.

Resolve overdue commitments before standup

Section titled “Resolve overdue commitments before standup”

Situation: it’s Friday and you want to know what you promised to send this week.

Terminal window
mxr commitments --status open --format json \
| jq -r '.[] | select(.direction == "yours" and .by_when != null)
| "\(.by_when)\t\(.contact_email)\t\(.what)\t\(.evidence_msg_id)"' \
| sort \
| column -t -s $'\t'

What you get: a sortable table of every open promise with the source message id — pipe --format ids and xargs -I{} mxr cat {} --view reader if you want to read the original draft text.

Situation: inbound question you’d otherwise forward — find who’s answered something similar.

Terminal window
mxr expert MESSAGE_ID --format json \
| jq -r '.[0:3]
| .[]
| "\(.score | tostring | .[0:4])\t\(.email)\t\(.reason)"'

What you get: top 3 candidate experts with score and the cited reason. Their citations[] point at the answer messages, not at the matching questions — verify before forwarding.

Situation: you’re about to reply on a 3-month-old thread.

Terminal window
mxr briefing thread THREAD_ID --format json \
| jq -r '.body_markdown,
"\nCitations:",
(.citations[]? | " - \(.message_id // .thread_id): \(.quote)")'

What you get: a Markdown recap plus citations when the model grounded claims in specific messages. Cached, so the second run is instant.

Pick a send slot that matches the recipient

Section titled “Pick a send slot that matches the recipient”

Situation: scheduling a sensitive ask, want to land it in their fastest reply bucket.

Terminal window
mxr send-time alice@example.com --at "$PROPOSED_AT" --format json \
| jq -r 'if .confidence == "low" then "low confidence — no recommendation"
else "best window: \(.recipient_rows[0].best_windows[0] | "\(.weekday) \(.hour_start):00-\(.hour_end):00")"
end'

What you get: a one-liner with the best window if mxr has enough data, or an honest “no recommendation” line when sample count is low. Pair with mxr send DRAFT_ID --at "$WHEN" to actually schedule.

Tell an agent
"Before scheduling DRAFT_ID with `mxr send DRAFT_ID --at <when>`, run
`mxr send-time <to_address> --at <when> --format json`. If the proposed
slot is at least 2x slower than the best window AND confidence is medium
or high, propose the better slot. Don't auto-reschedule."