Sending email via AgentMail
Use when sending email via AgentMail API.
AgentMail gives agents their own inboxes. Base URL https://api.agentmail.to, API version path /v0. Auth is Authorization: Bearer <key>. Official docs: https://docs.agentmail.to (append .md to any page for clean markdown; llms-full.txt has the whole reference).
Credential location
The key lives as AGENTMAIL_API_KEY in /root/.hermes/.env and in each profile's .env under /root/.hermes/profiles/<name>/.env. There is no CLI installed and no MCP server — call the REST API directly (stdlib urllib works; the agentmail PyPI SDK also works).
Read the key defensively
A .env value has been stored with the variable name duplicated into the value (AGENTMAIL_API_KEY=AGENTMAIL_API_KEY=<key>). Always strip a repeated prefix before use:
line = [l for l in open('/root/.hermes/.env', errors='ignore')
if l.strip().startswith('AGENTMAIL_API_KEY=')][0].rstrip('\n')
key = line.split('=', 1)[1].strip().strip('"').strip("'")
if key.startswith('AGENTMAIL_API_KEY='):
key = key.split('=', 1)[1].strip().strip('"')
A healthy key is ~70 chars. ~94 chars means the duplicated prefix is still attached.
Diagnose 403s before blaming scope
- Bare
{"message":"Forbidden"}with nocodefield = the token is malformed/unparseable (CloudFront rejects it at the edge). A bogus key and an empty key produce the identical response, so use that as a control to confirm. This is the duplicated-prefix symptom. {"code":"missing_permission", ...}naming a specific permission = the key authenticated but is scoped too narrowly. Only the owner can mint a broader key at https://console.agentmail.to. A key cannot create a key with permissions it lacks.
Probe scope cheaply: GET /v0/inboxes (inbox_read), /v0/domains (domain_read), /v0/threads (thread_read), /v0/api-keys (api_key_read).
To test message_send without an inbox id, POST a send to a nonexistent inbox: 404 Inbox not found means send is permitted; 403 missing_permission means it is not.
Send
GET /v0/inboxes -> {count, inboxes:[{inbox_id, email, ...}]}
POST /v0/inboxes/{inbox_id}/messages/send -> {message_id, thread_id}
GET /v0/inboxes/{inbox_id}/messages?limit=3 -> read back
inbox_id IS the email address (e.g. am-jkdev001@agentmail.to), not a UUID. Send body: {"to": ["addr"], "subject": "...", "text": "..."} (html, cc, bcc also supported).
Never hardcode an inbox — list inboxes and pick, since the set changes.
Verification gate
A 200 from the send endpoint only means AgentMail accepted it for SES handoff. Always read the inbox back and confirm the message appears with label sent, the expected recipient, and a fresh timestamp. Report message_id and thread_id. Never claim delivery to the recipient's mailbox — that is SES's job and only the recipient can confirm it.
Deliverability caveat
If GET /v0/domains returns count: 0, there is no verified sending domain and mail goes out from the shared agentmail.to domain via Amazon SES. That is fine for testing but lands in spam for corporate Exchange/Outlook tenants at higher volume. Flag this; do not silently rely on it for anything production.
Key rotation
When the owner replaces the key, propagate it to every profile .env (they hold independent copies and will otherwise keep failing), back up each file first, chmod 600, then verify each copy authenticates with GET /v0/inboxes. Never print the key.
Environment note
The terminal tool has returned empty output for every command in at least one session on this host, making curl/grep useless. Fall back to execute_code with stdlib urllib and os.walk — it works reliably. Also keep filesystem scans bounded: a full os.walk of /root/Working exceeded the 300s cell timeout.
Source: jknash/hermes-shared-skills · branch hermes-jkdev001 @ 1d0d545c3970 · skills/operations/agentmail-email-sending/ · view source · Imported 2026-10-04. Supporting files (references, scripts) remain in the source repository.
version 1.0.0.
Published by Muse · 2026-10-04.