Codex Agent Email: Give Codex Its Own Mailbox
Provision a real mailbox for an OpenAI Codex agent, pass the API key through .env, send the first email, and read replies over SSE or polling.
John Joubert
Founder, Robotomail

Table of contents
Giving a Codex agent email takes three steps: create a mailbox with the Robotomail CLI or API, put the API key in a .env file that Codex can read but never sees pasted into the chat, and have the agent send through POST /v1/mailboxes/{id}/messages. Replies come back either as a server-sent events stream, a webhook, or a plain poll of the mailbox. This guide walks the whole loop with an OpenAI Codex coding agent, including the sandbox and secret-handling details that trip people up on the first run.
If you have already done this with Claude Code, the shape is identical. See the Claude Code email setup for that variant.
Why a Codex agent needs its own address
Codex is good at long-running, file-touching work: refactors, migration branches, dependency bumps, nightly test triage. A lot of that work has an outside-world dependency that arrives by email. A vendor has to confirm an API upgrade. A security team wants a scan result. A human reviewer needs a summary and has to be able to just reply.
If you wire Codex into your own Gmail account with OAuth, every message it sends is from you, every message it reads is yours, and there is no clean audit boundary. A dedicated codex@ address fixes that. Mail from the agent is identifiable as agent mail, replies land in a mailbox only the agent reads, and revoking access is deleting one mailbox instead of untangling a token.
The general argument is in why agents need a real inbox. The practical version: you want a mailbox you can create and destroy with an API call.
Step 1: provision the mailbox
CLI is fastest:
npx @robotomail/cli mailbox create codex-agent
Or the API directly:
curl -X POST https://api.robotomail.com/v1/mailboxes \
-H "Authorization: Bearer rm_your_api_key" \
-H "Content-Type: application/json" \
-d '{"address": "codex-agent"}'
Omit domainId and you get an address on the platform domain, which is fine for internal work and testing. If the agent is talking to people outside your company, put it on a domain you control and let recipients see a familiar name. Some teams run a dedicated domain for agent traffic so agent mail never affects reputation on their human domain. Setup is in the custom domain guide.
The response includes the mailbox ID and the full address. Save both. The ID goes in the path of every send and read call.
One mailbox per agent role, not per task. codex-agent is a good boundary. codex-refactor-pr-4821 is not, unless you are genuinely spinning up an ephemeral identity per job, which is a legitimate pattern for multi-agent workflows but adds bookkeeping.
Step 2: hand Codex the key through .env, never the chat
This is the part people get wrong.
Do not paste rm_... into a Codex prompt. Chat input becomes part of the transcript, and transcripts get logged, cached, replayed into context on the next turn, and sometimes shipped to a provider for review. A key in the conversation is a key you have to rotate.
Instead, put it in a file the process can read and the agent is told not to print:
# .env (add to .gitignore first)
ROBOTOMAIL_API_KEY=rm_your_api_key
ROBOTOMAIL_MAILBOX_ID=mbx_codex
[email protected]
echo ".env" >> .gitignore
Then tell Codex about the capability, not the credential. Codex reads AGENTS.md in the repo root, so that is the right place:
## Email
You have a mailbox at [email protected].
- Credentials live in `.env` as `ROBOTOMAIL_API_KEY`. Read them with your
language's env loader. Never echo, log, or commit the value.
- Send with `scripts/send_mail.py`. Read replies with `scripts/read_mail.py`.
- Only email addresses listed in `docs/contacts.md`. If a task implies
emailing anyone else, stop and ask.
- Sign messages as "Codex agent for <team>" and include the task or PR
reference in the subject.
Two things worth noting. First, the allowlist. An agent with unrestricted send is an agent that can be socially engineered into emailing a stranger, and inbound mail is untrusted input by definition. Read email prompt injection before you widen that list.
Second, sandboxing. Codex CLI runs shell commands in a sandbox, and depending on your approval and sandbox mode outbound network access may be blocked. If your first send fails with a connection error rather than an HTTP error, that is almost always the sandbox and not your key. Check your Codex configuration and allow network access for the workspace, or run the send step outside the sandbox.
Step 3: send the first email
A thin script keeps the key out of the agent's hands and gives you one place to add logging:
# scripts/send_mail.py
import os, sys, json, urllib.request
KEY = os.environ["ROBOTOMAIL_API_KEY"]
MAILBOX = os.environ["ROBOTOMAIL_MAILBOX_ID"]
def send(to, subject, body, in_reply_to=None):
payload = {"to": [to], "subject": subject, "bodyText": body}
if in_reply_to:
payload["inReplyTo"] = in_reply_to
req = urllib.request.Request(
f"https://api.robotomail.com/v1/mailboxes/{MAILBOX}/messages",
data=json.dumps(payload).encode(),
headers={"Authorization": f"Bearer {KEY}",
"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req) as r:
return json.load(r)
if __name__ == "__main__":
to, subject = sys.argv[1], sys.argv[2]
print(send(to, subject, sys.stdin.read())["id"])
The raw call, if you would rather have Codex use curl:
curl -X POST https://api.robotomail.com/v1/mailboxes/mbx_codex/messages \
-H "Authorization: Bearer $ROBOTOMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"to": ["[email protected]"], "subject": "PR #4821 ready for review", "bodyText": "Migration branch is green. Summary in the PR description."}'
Ask Codex to send itself a test message first. It proves the key, the sandbox, and the address in one shot. More on the first send in the send your first email guide.
Step 4: read replies over SSE or polling
Codex sessions are interactive and often short. Three options, and the right one depends on how long the agent stays alive.
| Pattern | Good for | Trade-off |
|---|---|---|
| SSE event stream | An agent sitting in a session waiting on a reply | Needs a live connection; reconnect logic on drop |
| Polling | Scripts, CI jobs, cron-driven Codex runs | Latency equals your interval; wasted calls when quiet |
| Webhook | Always-on services that outlive the session | Needs a public HTTPS endpoint |
SSE. Robotomail exposes an events stream, so a waiting agent can block on it instead of hammering the API. This is the cleanest fit for "send a question, wait for the human, continue" inside one Codex session. Stream shapes and reconnection semantics are in the events API reference.
Polling. Simplest thing that works and the right default for a Codex run kicked off by CI. Do a GET against the same messages path you post to, filter for anything newer than your last checkpoint, and back off when idle:
# scripts/read_mail.py (sketch)
import os, time, json, urllib.request
KEY = os.environ["ROBOTOMAIL_API_KEY"]
MAILBOX = os.environ["ROBOTOMAIL_MAILBOX_ID"]
def fetch():
req = urllib.request.Request(
f"https://api.robotomail.com/v1/mailboxes/{MAILBOX}/messages",
headers={"Authorization": f"Bearer {KEY}"},
)
with urllib.request.urlopen(req) as r:
return json.load(r)
def wait_for_reply(timeout=1800, interval=20):
deadline = time.time() + timeout
while time.time() < deadline:
for m in fetch()["data"]:
if not m.get("read"):
return m
time.sleep(interval)
return None
Field names and pagination parameters are in the messages API reference - use those rather than guessing.
Webhooks. If the Codex agent is a step inside a longer-lived service, register a webhook and let inbound mail wake it. The payload is a flat JSON envelope:
{
"event": "message.received",
"data": {
"message_id": "...", "mailbox_id": "...",
"mailbox_address": "[email protected]",
"from": "[email protected]", "subject": "Re: PR #4821 ready for review",
"body_text": "Looks good, ship it.", "thread_id": "...",
"received_at": "2026-04-17T10:00:00.000Z"
}
}
Note the case change: sends take camelCase (bodyText), webhook payloads arrive snake_case (body_text). Write your parser accordingly.
Keep replies in the thread
When Codex answers, pass the inbound message_id as inReplyTo on the send. That sets the correct In-Reply-To and References headers, so the reply threads properly in the recipient's client instead of showing up as a fresh message with a Re: subject. Recipients notice the difference immediately, and so do spam filters. Details in threading and the receive and reply guide.
FAQ
Can I just use my Gmail account with the Codex agent?
You can, and it works until it does not. OAuth scopes are broad, the agent's mail is indistinguishable from yours, and Google's quotas were designed for a human clicking send. A separate mailbox per agent is cleaner to audit and cheaper to revoke. Comparison in Gmail API vs Robotomail.
What stops the agent from emailing the wrong person?
An allowlist you enforce in your send wrapper, not in the prompt. Check the recipient against a static list in scripts/send_mail.py and raise on anything unexpected. Instructions in AGENTS.md guide behavior; code enforces it.
Should each Codex task get its own mailbox?
Usually no. One mailbox per agent role, and use thread_id to keep separate conversations distinct. Per-task mailboxes make sense only when the task itself is a distinct identity, like a per-customer intake agent.
Does this work with other coding agents?
Yes, nothing here is Codex specific beyond AGENTS.md and the sandbox note. The same mailbox, key handling, and read patterns apply to any agent. See the general agent email address setup.
Ready to wire it up? Create a mailbox and send the first message in a few minutes with the quickstart, or start at robotomail.com.
Give your AI agent a real email address
One API call creates a mailbox with full send and receive. Webhooks for inbound, automatic threading, deliverability handled. 30-day money-back guarantee.
Related posts

Build an AI Agent Support Email Inbox: Setup Guide
Wire an AI agent to your support@ inbox: provision the mailbox, handle inbound webhooks, reply in-thread, and hand off to a human on low confidence.
Read post
Pydantic AI Email Integration: Typed Send and Receive
Give a Pydantic AI agent its own inbox: typed send tools, inbound webhooks that trigger agent runs, and validated reply models with real code.
Read post
LangGraph Email Integration: Inbound Events and Replies
Wire email into LangGraph: inbound webhooks as graph events, send and reply nodes, and human-in-the-loop interrupts resumed by an email reply.
Read post