Install
$ agentstack add skill-gerodp-hermes-productivity-skills-emailops ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
EmailOps CLI
emailops-cli is the headless front-end for EmailOps, a privacy-first, AI-native email client. AI runs locally by default (embedded llama.cpp); email data lives in a local SQLite database. This skill lets the agent read, search, chat with, and compose / draft / send mail without opening the desktop app.
It complements the himalaya skill: Himalaya speaks raw IMAP/SMTP, while emailops-cli adds Gmail/Outlook OAuth accounts plus local AI — semantic chat over your mail, classification, and embeddings.
The agent contract: always pass --json
Every command supports --json, which prints exactly one stable envelope to stdout (logs always go to stderr). Parse this single shape regardless of success or failure — never scrape the human-readable output:
{ "ok": true, "data": { /* result */ }, "error": null } // success
{ "ok": false, "data": null, "error": { "code": "not_found", "params": {}, "message": "" } } // failure
Add --quiet to suppress the stderr app-log stream when you only want the envelope.
Exit codes (branch on these, not on text)
| Code | Meaning | Code | Meaning | |------|--------------------|------|--------------------| | 0 | ok | 5 | network / sync | | 2 | invalid input | 6 | AI | | 3 | not found | 130 | cancelled | | 4 | auth | 1 | otherwise |
Prerequisites
emailops-clionPATH(emailops-cli --versionto verify). It is built
from the EmailOps repo with the cli cargo feature; symlink the built binary into ~/.local/bin if needed.
- An EmailOps data directory with at least one configured account. Confirm
with doctor before anything else.
When to use
Use whenever the user asks about their own email: searching messages, asking "what did X say about Y?", listing accounts, reading a specific message, or checking mailbox health. Also use it to write mail — save or edit a draft, review existing drafts, or send a message. Prefer the cheap read paths (doctor, accounts, emails, show, search) over chat unless the question genuinely needs the local LLM.
Core commands
| Command | Purpose | Cost | |---------|---------|------| | emailops-cli doctor --json | Env readiness: DB, accounts, AI config. Loads no model. | cheap | | emailops-cli accounts --json | List configured accounts (id, email, provider). | cheap | | emailops-cli emails --json [--limit N] [--mailbox inbox\|sent\|spam\|trash] | List recent emails. | cheap | | emailops-cli show --json | Full single email (headers + body). | cheap | | emailops-cli search '' --json [--limit N] [--trace] | Full-text search across an account's mail. | cheap | | emailops-cli chat '' --json [--trace] | Ask the local AI a question against your mail. | loads LLM | | emailops-cli stats --json | Per-account dashboard totals + coverage. | cheap | | emailops-cli compose --to … --subject … (--body …\|--body-file …) --json | Save a draft (default) or --send to deliver. | cheap (write) | | emailops-cli drafts --json | List saved drafts, newest-first. | cheap | | emailops-cli draft --json | Show a single draft (recipients, subject, body, attachments). | cheap |
Scope to an account with --account (defaults to the single enabled account). When multiple accounts exist, list them first and pass --account explicitly.
Procedure
- Health check first (read-only, no model load):
``bash emailops-cli doctor --json ` Confirm data.ok == true and data.accountsEnabled > 0. If aiEnabled is false, avoid chat` and stick to search/read commands.
- Resolve the account when there's more than one:
``bash emailops-cli accounts --json ` Use the returned id or email with --account`.
- Read paths for lookups (fast, safe even while the desktop app is open):
``bash emailops-cli search 'invoice from acme' --json --limit 5 emailops-cli show --json emailops-cli emails --json --mailbox inbox --limit 25 ``
- AI path for natural-language questions (loads the local model):
``bash emailops-cli chat 'what did the landlord say about the lease renewal?' --json ` Add --trace to inspect routing, retrieval, tool calls, and the sources behind the answer under data.trace`.
- Multi-turn: carry context across one-shot invocations by passing the
conversationId from a previous chat --json result: ``bash emailops-cli chat 'and what date was that?' --json --conversation ``
- Parse the envelope, branch on
ok/ exit code, and answer the user
from data. Surface error.message on failure.
Writing mail: compose, drafts, send
compose is the single write path. By default it saves a draft (and, on Gmail/Outlook accounts, also pushes it to the provider's Drafts folder — no extra flag). Add --send to deliver immediately instead. Both paths return the resulting record under data.
> 🚫 NEVER send without explicit user confirmation. Do not run compose … > --send until you have shown the user the full email about to be sent — > the sending account, every To/Cc recipient, the subject, the complete > body, and any attachments — and the user has explicitly approved that exact > message. No exceptions: not for "obvious" replies, not because the user said > "send it" earlier about a different draft, not to save a round-trip. A vague > or prior "go ahead" is not sufficient — re-confirm after showing the final > content. When unsure whether you have approval, save a draft and ask — you > never need permission to save a draft, and you always need it to send.
# Save a draft (default). --to / --cc / --attach are repeatable.
emailops-cli --account compose \
--to a@x.com --to b@y.com --cc c@z.com \
--subject "Q3 numbers" \
--body "Hi — draft here." \
--attach /abs/path/report.pdf --json
# Long/rich body: read it from a file instead of --body (mutually exclusive).
emailops-cli --account compose \
--to a@x.com --subject "Notes" --body-file /abs/path/body.txt --json
# Update an existing draft in place rather than creating a new one.
emailops-cli --account compose --draft \
--subject "Q3 numbers (rev)" --body "Updated." --to a@x.com --json
# Send now — ONLY after showing the full email and getting explicit approval
# (see "Mandatory send procedure" below). Delivers immediately; irreversible.
emailops-cli --account compose \
--to a@x.com --subject "Q3 numbers" --body "Final." --send --json
Rules and shapes:
--to,--cc,--attachare repeatable; pass the flag once per value.- Body is
--bodyXOR--body-file. Supplying both is invalid input
(exit 2, no JSON envelope — it's rejected at the argument layer). --body-file reads the body verbatim from the path.
- Use real line breaks — write bodies like a normal email. The CLI stores
the body verbatim and does not interpret escape sequences, so a plain --body "Hi\n\nText" is stored as the literal characters \n and the recipient sees \n in the message. Format the body the way a person would: greeting on its own line, a blank line between paragraphs, and a blank line before the sign-off. Two reliable ways to pass actual newlines:
--body-file(preferred for anything multi-line): write the body to a
file with real newlines, then reference it — printf 'Hi there,\n\nFirst paragraph.\n\nThanks,\nGero\n' > /tmp/body.txt then compose … --body-file /tmp/body.txt.
- ANSI-C quoting for short inline bodies:
--body $'Hi there,\n\nText'
(the $'…' form makes the shell expand \n into real newlines; ordinary "…" double quotes do not).
- Attachments are path references, resolved at compose/send time. Use
absolute paths readable now; filename and mimeType are inferred. Each returned attachment is {id, draftId, filePath, filename, mimeType}.
compose(draft) returns the draft object:id,accountId,status
("draft"), subject, toAddresses[], ccAddresses[], body, attachments[], providerDraftId (null on IMAP/local; set once pushed to Gmail/Outlook), createdAt/updatedAt. drafts returns an array of these (newest-first); draft returns one.
- Drafts don't require recipients (a bare draft with empty
toAddressesis
saved fine) — but confirm the recipient list before --send.
Review drafts:
emailops-cli --account drafts --json # newest-first list
emailops-cli --account draft --json # one draft, full
Mandatory send procedure (never skip a step)
--send is irreversible and there is no undo. Every send MUST follow this order:
- Save it as a draft first (
compose …, no--send) — never build the
send command straight from user text.
- Show the user the full outgoing email by reading it back with
draft --json: the sending account, all To/Cc recipients, the subject, the entire body, and the filename of each attachment. Present it plainly, not just a summary.
- Ask for explicit approval of that exact message and wait for a clear
"yes, send this". If the user asks for any change, edit the draft (compose --draft …) and repeat from step 2 — approval of an earlier version does not carry over.
- Only then run
compose --draft --send --json(or an equivalent
--send with the identical, already-approved fields).
If you cannot show the full message and get a fresh, explicit yes, do not send — leave the draft saved and tell the user it's ready to review.
Pitfalls
- Heavy writes need the app closed.
sync,classify,embed, and the
heavy compose write paths — --send, and provider draft push on Gmail/Outlook — mutate the SQLite DB (and hit the network); run them only when the EmailOps desktop app is not running (WAL contention). A plain save-as-draft on an IMAP/local account is safe even while the app is open (the draft stays local — providerDraftId is null). Read commands are always safe.
- Don't scrape pretty output. Human output is styled (color/tables) only on
an interactive TTY and is not a stable contract. Always --json.
- Logs are on stderr. Don't merge stderr into stdout when parsing JSON;
use --quiet to silence the app-log stream.
- No model for
doctor. It's intentionally cheap — use it as the gate
before any AI command rather than calling chat blindly.
- Never send without explicit user confirmation.
--sendis irreversible;
always follow the Mandatory send procedure above — save a draft, show the full email (account, all recipients, subject, body, attachments), get a fresh explicit yes, then send. When in doubt, leave it as a draft.
- Same commands in the REPL. Running
emailops-cliwith no subcommand opens
an interactive REPL that exposes these as /compose, /drafts, and /draft . For the agent, prefer the one-shot subcommands with --json.
- Double-encoded saved files. When emailops-cli output is written to a file
and ends up double-escaped (the outer JSON has an "output" key whose value is a string containing the inner JSON), unwrap it first:
``bash cat saved_result.json | jq -r '.output' | jq ... ``
Without -r .output, jq will try to parse the escaped string as literal JSON tokens and silently fail or return wrong results.
- Outlook/Hotmail control characters break
jq. Emails from Outlook/Hotmail
providers can inject raw \r\n sequences that cause jq: parse error: Invalid string: control characters … errors. Pre-filter before piping to jq:
``bash cat output.json | sed 's/\\r//g' | jq ... ``
Verification
emailops-cli doctor --json # expect ok:true, accountsEnabled > 0, exit 0
emailops-cli accounts --json # expect a non-empty data array
If doctor returns ok:true with at least one enabled account, the skill is correctly wired and downstream commands can be trusted.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: gerodp
- Source: gerodp/hermes-productivity-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.