diff --git a/optional-skills/email/agentmail/SKILL.md b/optional-skills/email/agentmail/SKILL.md
index 770ca8d551..c75297a1f5 100644
--- a/optional-skills/email/agentmail/SKILL.md
+++ b/optional-skills/email/agentmail/SKILL.md
@@ -1,128 +1,95 @@
---
name: agentmail
-description: "Give the agent its own inbox: send and receive email."
+description: Use when an agent needs AgentMail CLI email inboxes.
version: 1.0.0
-author: teyrebaz33, Hermes Agent
+author: Haakam Aujla (Haakam21), AgentMail
license: MIT
platforms: [linux, macos, windows]
metadata:
hermes:
- tags: [email, communication, agentmail, mcp]
- category: email
+ tags: [Email, CLI, AgentMail, Communication]
+ homepage: https://agentmail.to
+prerequisites:
+ commands: [agentmail]
+required_environment_variables:
+ - name: AGENTMAIL_API_KEY
+ prompt: AgentMail API key (starts with am_)
+ help: "Create one at https://console.agentmail.to — or run the CLI self-signup flow in references/signup.md to obtain a key without one."
+ required_for: "authenticating the agentmail CLI; not needed before self-signup"
+ optional: true
---
-# AgentMail — Agent-Owned Email Inboxes
+# AgentMail Skill
-## Requirements
+AgentMail gives an agent its own email inbox for sending mail, receiving
+replies, completing email OTP flows, and running inbound email loops. Use it for
+agent-owned inboxes, not a user's existing IMAP/SMTP mailbox.
-- **AgentMail API key** (required) — sign up at https://console.agentmail.to (free tier: 3 inboxes, 3,000 emails/month; paid plans from $20/mo)
-- Node.js 18+ (for the MCP server)
+Use the `agentmail` CLI first. Use MCP only when the harness expects MCP tools;
+use REST only when the CLI is missing a required operation.
## When to Use
-Use this skill when you need to:
-- Give the agent its own dedicated email address
-- Send emails autonomously on behalf of the agent
-- Receive and read incoming emails
-- Manage email threads and conversations
-- Sign up for services or authenticate via email
-- Communicate with other agents or humans via email
-This is NOT for reading the user's personal email (use himalaya or Gmail for that).
-AgentMail gives the agent its own identity and inbox.
+- The agent needs an email address it owns.
+- The task involves email OTP flows, replies, threads, labels, or attachments.
+- The agent needs webhook or WebSocket delivery for inbound mail.
-## Setup
+## Prerequisites
-### 1. Get an API Key
-- Go to https://console.agentmail.to
-- Create an account and generate an API key (starts with `am_`)
+- Run commands through the `terminal` tool.
+- Install the CLI:
-### 2. Configure MCP Server
-Add to `~/.hermes/config.yaml` (paste your actual key — MCP env vars are not expanded from .env):
-```yaml
-mcp_servers:
- agentmail:
- command: "npx"
- args: ["-y", "agentmail-mcp"]
- env:
- AGENTMAIL_API_KEY: "am_your_key_here"
-```
-
-### 3. Restart Hermes
```bash
-hermes
+npm install -g agentmail-cli@latest
```
-All 11 AgentMail tools are now available automatically.
-## Available Tools (via MCP)
+- Export an API key:
-| Tool | Description |
-|------|-------------|
-| `list_inboxes` | List all agent inboxes |
-| `get_inbox` | Get details of a specific inbox |
-| `create_inbox` | Create a new inbox (gets a real email address) |
-| `delete_inbox` | Delete an inbox |
-| `list_threads` | List email threads in an inbox |
-| `get_thread` | Get a specific email thread |
-| `send_message` | Send a new email |
-| `reply_to_message` | Reply to an existing email |
-| `forward_message` | Forward an email |
-| `update_message` | Update message labels/status |
-| `get_attachment` | Download an email attachment |
+```bash
+export AGENTMAIL_API_KEY="am_..."
+```
+
+No API key yet? Use [signup.md](references/signup.md).
+
+## How to Run
+
+Use `--format json` whenever another command or script needs IDs.
+
+```bash
+agentmail inboxes list --format json
+```
+
+## Quick Reference
+
+- [AgentMail agent reference](https://agentmail.md): hosted copy.
+- [AgentMail](https://agentmail.to): product landing page.
+- [Console](https://console.agentmail.to): API keys and account management.
+- [Docs](https://docs.agentmail.to): full product documentation.
+- [signup.md](references/signup.md): self-signup and OTP verification.
+- [core.md](references/core.md): inboxes, messages, threads, labels, attachments.
+- [webhooks.md](references/webhooks.md): events to a public HTTPS server.
+- [websockets.md](references/websockets.md): events to a local agent process.
+- [mcp.md](references/mcp.md): MCP integration.
## Procedure
-### Create an inbox and send an email
-1. Create a dedicated inbox:
- - Use `create_inbox` with a username (e.g. `hermes-agent`)
- - The agent gets address: `hermes-agent@agentmail.to`
-2. Send an email:
- - Use `send_message` with `inbox_id`, `to`, `subject`, `text`
-3. Check for replies:
- - Use `list_threads` to see incoming conversations
- - Use `get_thread` to read a specific thread
-
-### Check incoming email
-1. Use `list_inboxes` to find your inbox ID
-2. Use `list_threads` with the inbox ID to see conversations
-3. Use `get_thread` to read a thread and its messages
-
-### Reply to an email
-1. Get the thread with `get_thread`
-2. Use `reply_to_message` with the message ID and your reply text
-
-## Example Workflows
-
-**Sign up for a service:**
-```
-1. create_inbox (username: "signup-bot")
-2. Use the inbox address to register on the service
-3. list_threads to check for verification email
-4. get_thread to read the verification code
-```
-
-**Agent-to-human outreach:**
-```
-1. create_inbox (username: "hermes-outreach")
-2. send_message (to: user@example.com, subject: "Hello", text: "...")
-3. list_threads to check for replies
-```
+1. Install `agentmail-cli@latest` and verify `agentmail inboxes list --format json`.
+2. If no API key is available, complete [signup.md](references/signup.md).
+3. Use [core.md](references/core.md) for inbox, send, read, reply, forward,
+ label, thread, and attachment flows.
+4. Add [webhooks.md](references/webhooks.md) or
+ [websockets.md](references/websockets.md) only when polling is not enough.
## Pitfalls
-- Free tier limited to 3 inboxes and 3,000 emails/month
-- Emails come from `@agentmail.to` domain on free tier (custom domains on paid plans)
-- Node.js (18+) is required for the MCP server (`npx -y agentmail-mcp`)
-- The `mcp` Python package must be installed: `pip install mcp`
-- Real-time inbound email (webhooks) requires a public server — use `list_threads` polling via cronjob instead for personal use
+
+- Prefer `AGENTMAIL_API_KEY` over `--api-key`.
+- Never expose `AGENTMAIL_API_KEY` in prompts, logs, URLs, or committed files.
+- Use stable `client_id` values for retried create operations.
+- Prefer `extracted_text` or `extracted_html` for LLM input when present.
+- React to `message.received`, not messages the agent sent.
## Verification
-After setup, test with:
-```
-hermes --toolsets mcp -q "Create an AgentMail inbox called test-agent and tell me its email address"
-```
-You should see the new inbox address returned.
-## References
-- AgentMail docs: https://docs.agentmail.to/
-- AgentMail console: https://console.agentmail.to
-- AgentMail MCP repo: https://github.com/agentmail-to/agentmail-mcp
-- Pricing: https://www.agentmail.to/pricing
+```bash
+agentmail inboxes list --format json
+```
diff --git a/optional-skills/email/agentmail/references/core.md b/optional-skills/email/agentmail/references/core.md
new file mode 100644
index 0000000000..6bc0ea5dee
--- /dev/null
+++ b/optional-skills/email/agentmail/references/core.md
@@ -0,0 +1,116 @@
+# AgentMail Core
+
+Common CLI path: create inboxes, send mail, read incoming mail, reply in
+threads, label work, and fetch attachments. Need a key first? Use
+[signup.md](signup.md). Need realtime delivery? Use [webhooks.md](webhooks.md)
+or [websockets.md](websockets.md).
+
+## Setup
+
+```bash
+npm install -g agentmail-cli@latest
+export AGENTMAIL_API_KEY="am_..."
+agentmail inboxes list --format json
+```
+
+## Inboxes
+
+```bash
+agentmail inboxes create \
+ --username support \
+ --display-name "Support Agent" \
+ --client-id support-agent-primary \
+ --format json
+
+agentmail inboxes get --inbox-id support@agentmail.to --format json
+```
+
+Omit `domain` to use `@agentmail.to`. Use stable `client_id` values for
+retried create commands.
+
+## Send
+
+```bash
+agentmail inboxes:messages send \
+ --inbox-id support@agentmail.to \
+ --to customer@example.com \
+ --subject "Hello" \
+ --text "Plain-text body." \
+ --html "
Plain-text body.
" \
+ --label outreach \
+ --format json
+```
+
+Send both `text` and `html` when possible. Recipient limit is 50 total across
+`to`, `cc`, and `bcc`.
+
+## Read and Reply
+
+```bash
+agentmail inboxes:messages list --inbox-id support@agentmail.to --label unread --format json
+agentmail inboxes:messages get --inbox-id support@agentmail.to --message-id --format json
+agentmail inboxes:threads get --inbox-id support@agentmail.to --thread-id --format json
+```
+
+For LLM input, prefer `extracted_text` or `extracted_html`. Some email has
+`html` but no `text`.
+
+```bash
+agentmail inboxes:messages reply \
+ --inbox-id support@agentmail.to \
+ --message-id \
+ --text "Thanks, I will take a look." \
+ --format json
+
+agentmail inboxes:messages reply-all \
+ --inbox-id support@agentmail.to \
+ --message-id \
+ --text "Thanks, everyone." \
+ --format json
+
+agentmail inboxes:messages forward \
+ --inbox-id support@agentmail.to \
+ --message-id \
+ --to teammate@example.com \
+ --format json
+```
+
+## Labels
+
+Use labels as lightweight state: `unread`, `handled`, `needs-review`.
+
+```bash
+agentmail inboxes:messages update \
+ --inbox-id support@agentmail.to \
+ --message-id \
+ --add-labels handled \
+ --remove-labels unread \
+ --format json
+```
+
+## Attachments
+
+```bash
+agentmail inboxes:messages get-attachment \
+ --inbox-id support@agentmail.to \
+ --message-id \
+ --attachment-id \
+ --format json
+```
+
+Fetch the returned download URL before it expires. Check
+`agentmail inboxes:messages send --help` for attachment-send flags.
+
+## REST Notes
+
+Use REST only when the CLI is unavailable or missing a required operation.
+
+```bash
+curl https://api.agentmail.to/v0/inboxes \
+ -H "Authorization: Bearer $AGENTMAIL_API_KEY"
+```
+
+Base URLs: `https://api.agentmail.to/v0`, `https://api.agentmail.eu/v0`.
+
+Error bodies include `name` and `message`; validation errors include `errors`.
+For `429`, honor `Retry-After` and back off.
diff --git a/optional-skills/email/agentmail/references/mcp.md b/optional-skills/email/agentmail/references/mcp.md
new file mode 100644
index 0000000000..ccdf6e86f2
--- /dev/null
+++ b/optional-skills/email/agentmail/references/mcp.md
@@ -0,0 +1,16 @@
+# AgentMail MCP
+
+Use MCP only when the user or harness already wants MCP tools. The CLI remains
+the default path for AgentMail workflows.
+
+Hosted server:
+
+```text
+https://mcp.agentmail.to/mcp
+```
+
+Authentication:
+
+- OAuth in compatible MCP clients.
+- API key in `?apiKey=...`.
+- API key in an `x-api-key` header.
diff --git a/optional-skills/email/agentmail/references/signup.md b/optional-skills/email/agentmail/references/signup.md
new file mode 100644
index 0000000000..3976f299b3
--- /dev/null
+++ b/optional-skills/email/agentmail/references/signup.md
@@ -0,0 +1,50 @@
+# AgentMail Self-Signup
+
+Use this when the agent does not have an AgentMail API key. A human must receive
+and provide the OTP.
+
+## Sign Up
+
+```bash
+npm install -g agentmail-cli@latest
+agentmail agent sign-up \
+ --human-email you@example.com \
+ --username my-agent \
+ --source agentmail-cli \
+ --referrer hermes-agent \
+ --format json
+```
+
+Export the returned `api_key`:
+
+```bash
+export AGENTMAIL_API_KEY="am_..."
+```
+
+Verify with the OTP:
+
+```bash
+agentmail agent verify --otp-code 123456
+```
+
+## Notes
+
+- Use a real human email address for `--human-email`.
+- `human_email` is the signup idempotency key, but signing up again with the
+ same email rotates the API key.
+- Before verification, the account has one inbox, 10 sends/day, and can only
+ send to the signup human email.
+
+## First Check
+
+```bash
+agentmail inboxes list --format json
+agentmail inboxes:messages send \
+ --inbox-id my-agent@agentmail.to \
+ --to you@example.com \
+ --subject "AgentMail verified" \
+ --text "My AgentMail inbox is verified." \
+ --format json
+```
+
+Continue with [core.md](core.md).
diff --git a/optional-skills/email/agentmail/references/webhooks.md b/optional-skills/email/agentmail/references/webhooks.md
new file mode 100644
index 0000000000..8bdbdba2e0
--- /dev/null
+++ b/optional-skills/email/agentmail/references/webhooks.md
@@ -0,0 +1,30 @@
+# AgentMail Webhooks
+
+Use webhooks when a public HTTPS server should receive AgentMail events. Use
+[websockets.md](websockets.md) for local processes without a public URL.
+
+## Create
+
+```bash
+agentmail webhooks create \
+ --url https://your-app.example.com/webhooks/agentmail \
+ --event-type message.received \
+ --inbox-id support@agentmail.to \
+ --client-id support-agentmail-webhook \
+ --format json
+```
+
+Store the returned `secret` immediately.
+
+## Handle
+
+Headers: `svix-id`, `svix-timestamp`, `svix-signature`.
+
+1. Verify the raw request body with the webhook secret.
+2. Dedupe by `svix-id` or `event_id`.
+3. Return `200` quickly.
+4. Process asynchronously.
+5. For `message.received`, load the thread with the CLI and reply if needed.
+
+Act on `message.received` only. Treating `message.sent` or delivery events as
+inbound work creates loops.
diff --git a/optional-skills/email/agentmail/references/websockets.md b/optional-skills/email/agentmail/references/websockets.md
new file mode 100644
index 0000000000..92dd25cede
--- /dev/null
+++ b/optional-skills/email/agentmail/references/websockets.md
@@ -0,0 +1,55 @@
+# AgentMail WebSockets
+
+Use WebSockets when a local agent process needs realtime inbound mail. After an
+event arrives, use the CLI for inbox, send, reply, label, and attachment work.
+
+## Python
+
+The Python example needs the separate AgentMail Python SDK, not the CLI:
+
+```bash
+pip install agentmail
+```
+
+`AgentMail()` reads `AGENTMAIL_API_KEY` from the environment. If the SDK is
+unavailable, use the `Raw` WebSocket below instead.
+
+```python
+from agentmail import AgentMail, MessageReceivedEvent, Subscribe
+
+client = AgentMail()
+
+with client.websockets.connect() as socket:
+ socket.send_subscribe(Subscribe(
+ inbox_ids=["agent@agentmail.to"],
+ event_types=["message.received"],
+ ))
+ for event in socket:
+ if isinstance(event, MessageReceivedEvent):
+ print(event.message.subject, event.message.from_)
+```
+
+## Raw
+
+```text
+wss://ws.agentmail.to/v0?api_key=$AGENTMAIL_API_KEY
+```
+
+EU region:
+
+```text
+wss://ws.agentmail.eu/v0?api_key=$AGENTMAIL_API_KEY
+```
+
+Subscribe frame:
+
+```json
+{ "type": "subscribe", "event_types": ["message.received"], "inbox_ids": ["agent@agentmail.to"] }
+```
+
+Omit `inbox_ids` and `pod_ids` for the API key scope.
+
+## Loop Rules
+
+- Dedupe every event by `event_id`.
+- Reconnect with backoff and resubscribe after reconnecting.
diff --git a/tests/tools/test_skill_env_passthrough.py b/tests/tools/test_skill_env_passthrough.py
index be08539866..1988efb992 100644
--- a/tests/tools/test_skill_env_passthrough.py
+++ b/tests/tools/test_skill_env_passthrough.py
@@ -1,6 +1,8 @@
"""Test that skill_view registers required env vars in the passthrough registry."""
import json
+import shutil
+from pathlib import Path
from unittest.mock import patch
import pytest
@@ -8,6 +10,12 @@ import pytest
import tools.env_passthrough as _ep_mod
from tools.env_passthrough import clear_env_passthrough, is_env_passthrough
+# Real optional AgentMail skill — its CLI-first workflow runs `agentmail` through
+# the sandboxed terminal, which strips secrets unless the skill declares them.
+_AGENTMAIL_SKILL_SRC = (
+ Path(__file__).resolve().parents[2] / "optional-skills" / "email" / "agentmail"
+)
+
@pytest.fixture(autouse=True)
def _clean_passthrough():
@@ -78,3 +86,54 @@ class TestSkillViewRegistersPassthrough:
assert result["success"] is True
from tools.env_passthrough import get_all_passthrough
assert len(get_all_passthrough()) == 0
+
+
+class TestAgentMailKeyPassthrough:
+ """The real AgentMail skill must declare AGENTMAIL_API_KEY so a profile-stored
+ key reaches the sandboxed terminal, while staying usable (self-signup) without
+ one."""
+
+ def _install_real_skill(self, tmp_path, monkeypatch):
+ dest = tmp_path / "email" / "agentmail"
+ shutil.copytree(_AGENTMAIL_SKILL_SRC, dest)
+ monkeypatch.setattr("tools.skills_tool.SKILLS_DIR", tmp_path)
+ return dest
+
+ def test_agentmail_api_key_registered_for_sandbox_when_set(self, tmp_path, monkeypatch):
+ """A profile-stored AGENTMAIL_API_KEY must pass through to the terminal
+ sandbox — otherwise the documented CLI workflow fails authentication."""
+ self._install_real_skill(tmp_path, monkeypatch)
+ monkeypatch.setenv("AGENTMAIL_API_KEY", "am_test_key_123")
+
+ with patch("tools.skills_tool._secret_capture_callback", None):
+ from tools.skills_tool import skill_view
+
+ result = json.loads(skill_view(name="agentmail"))
+
+ assert result["success"] is True
+ assert is_env_passthrough("AGENTMAIL_API_KEY"), (
+ "AGENTMAIL_API_KEY was set but not registered for sandbox passthrough — "
+ "the skill's frontmatter must declare it under required_environment_variables"
+ )
+
+ def test_agentmail_key_is_optional_so_self_signup_still_works(self, tmp_path, monkeypatch):
+ """With no key present the skill must NOT be blocked as setup_needed on the
+ key — the CLI self-signup flow obtains one without a pre-provisioned key."""
+ self._install_real_skill(tmp_path, monkeypatch)
+ monkeypatch.delenv("AGENTMAIL_API_KEY", raising=False)
+
+ with patch("tools.skills_tool._secret_capture_callback", None):
+ from tools.skills_tool import skill_view
+
+ result = json.loads(skill_view(name="agentmail"))
+
+ assert result["success"] is True
+ # `optional: true` means a missing key must NOT be reported as a blocking
+ # requirement — that is what keeps the CLI self-signup path viable.
+ assert "AGENTMAIL_API_KEY" not in result["missing_required_environment_variables"], (
+ "AGENTMAIL_API_KEY must be declared optional so a missing key does not "
+ "block the self-signup path"
+ )
+ assert result["setup_needed"] is False, (
+ "an absent optional key must not force the skill into setup_needed"
+ )