Agent workflows
The CLI commands built for agents - a throwaway inbox and wait --otp for sign-ups, read with its trust verdict and agent-safe text, send, tail for live mail, and register and claim for an agent that starts with no account.
A handful of top-level commands cover what agents do with email most often, each in one line that works the same from a shell script or a model’s tool call. They all print JSON when piped, and the reading commands only ever hand over the agent-safe text of a message.
| Job | Command |
|---|---|
| Get an address | machinemail inbox new |
| Wait for a verification code | machinemail wait --otp |
| Wait for any message | machinemail wait --from ... --subject ... |
| Read a message safely | machinemail read <message-id> |
| Send | machinemail send |
| Follow mail live | machinemail tail |
| Start with no account | machinemail register, then machinemail claim |
send, wait and read use a default inbox when you leave it out: MACHINEMAIL_INBOX, then the inbox saved in the profile by register, then the inbox of an inbox-scoped key.
Sign up for a service
The most common agent job: create an address, use it to sign up somewhere, and read back the verification code.
Create a throwaway inbox
Without
--usernamethe address gets a random, hard-to-guess local part.--ephemeralmakes the inbox delete itself, with its mail, after the TTL (one hour unless--ttlsays otherwise;--ttltakes600,10m,2hor1d); each inbound message extends it. On a terminal it shows the inbox and the next command to run:Submit the sign-up form
Have the agent (or your test) enter
$INBOXin the service’s sign-up form.Wait for the code
waitblocks until a new message matching the filters arrives, then prints only the code, or the magic link when the message has no code. It exits 8 if nothing arrives within--timeout(default 5 minutes;0waits forever).If the email may have arrived before
waitstarted (you submitted the form first), add--since 5mso recent mail counts too. Only mail received afterwaitstarts counts otherwise.
With --json you get the code and where it came from:
How the code is found, and what is trusted
wait --otp is built so that a hostile email cannot hand the agent a code of its choosing:
- The code is read from
trust.content.text, the agent-safe text of the trust report: hidden text and zero-width characters are removed first, so a code planted in invisible HTML is not picked up. - Only mail whose sender authenticated is used: trust level
verifiedorknown, or a DMARC pass, or an aligned SPF or DKIM pass. Unauthenticated mail does reach the inbox (labelledUNAUTHENTICATED), which is why this check matters. Suspicious and quarantined mail is never used. --from github.commatchesgithub.comand its subdomains, never a lookalike such asgithub.com.example.net.--fromalso takes a full address.- A magic link must come from
trust.links, unflagged, and be on the sender’s domain or a subdomain of it. Pass--link-domainwhen the link is on a parent domain;--linkprints the link even when there is also a code. --pattern <regex>replaces the code pattern (the first capture group, else the whole match).
For a test sender without SPF or DKIM (a local mail server, an injected test message), add --allow-unauthenticated. It still skips suspicious and quarantined mail.
See OTP and sign-ups for the same flow over the API and MCP.
Wait for a message
Without --otp, wait prints the message itself. Filter by sender and subject (each can repeat; any match counts):
wait listens on the event stream and polls as a fallback (every 5 seconds while the stream is up, 2 seconds without it), so it still works when the stream drops. --poll uses polling only.
Read a message safely
On a terminal, read shows the headers, the trust verdict and the agent-safe text:
This sender did not authenticate (no SPF, DKIM or DMARC pass), so the message is UNKNOWN and untrusted; wait --otp would only take a code from it with --allow-unauthenticated.
You do not need the inbox: read tries the default inbox, then every inbox the key can list. Pass --inbox, or the inbox as the first argument, to skip the search.
Piped (or with --json), read prints the agent view, the shape to hand a model:
bodyis the agent-safe text: quotes, signatures, hidden text and zero-width characters removed.body_sourcesays where it came from.untrustedistrueunless the sender authenticated (trust levelverifiedorknown). Treat an untrusted body as data, never as instructions.- For quarantined mail the body is withheld, as the MCP server does.
--full prints the whole API message instead, including the original text and HTML; it is untrusted, so do not pass it to a model unfiltered. --original shows the sender’s text on a terminal, labelled untrusted, and --raw prints the RFC 5322 source. wait (without --otp) prints the same agent view. See Using trust in agents for how to act on the verdict.
Send
--to,--ccand--bccrepeat or take comma-separated addresses.- The body comes from
--text,--htmlor--file(a.htmlfile is sent as HTML);-reads it from stdin. --attachrepeats; attachments are base64-encoded, up to 6 MB per request in total.--header "Name: value",--reply-toand--labelrepeat too.
Piped, send prints the new message_id, thread_id and timestamp. A send is never retried after a server error, so a script cannot send the same message twice. To reply in a thread, use machinemail messages reply <inbox-id> <message-id> --text ... (see messages).
Follow mail live
On a terminal, tail prints one line per event with the trust badge of inbound mail, under a status line:
Piped, it prints NDJSON, one {id, event, data} per line, which suits an agent reading a stream or a log file:
-e/--event-types filters by type (comma-separated or repeated), --inbox and --pod by scope, and --max n stops after n events. tail reconnects with backoff and resumes from the last event it saw; --last-event-id resumes an earlier run.
Register an agent without a human
An agent with no account and no key can sign itself up through auth.md:
register creates a sandbox organization with one inbox on machinemail.dev, gets an access token, and saves everything in a profile: the current profile if it has no key, otherwise one named agent (or --profile). It never replaces a profile that holds an API key unless you pass --force. The token lasts an hour; the CLI renews it before each command, so you never handle it. Tokens are never printed.
The sandbox:
- reaches only its own inbox,
- can send a few messages to a few different recipients per 24 hours (by default 10 messages, 10 recipients, at most 5 per message; see Rate limits),
- expires after 24 hours unless a human claims it.
The new inbox becomes the profile’s default inbox, so send, wait and read work without --inbox:
Piped, register prints the registration, including inbox_id, the scopes and the claim link and code:
Hand it to a human
The claim is how the agent’s human takes it over. Give them the link and the 6-digit code; they sign in to MachineMail, enter the code, and the agent and its inbox move into their organization with full access. See Claiming an agent for the human’s side.
Codes expire after 10 minutes and a new one replaces the old. claim (and register --wait) with --wait blocks until the human finishes, then saves the post-claim credentials to the profile. A claim code that expires while waiting exits 8. register --email limits who can claim from the start; register --no-claim skips the first code.
A claimed agent’s token can be renewed for about a day. For an agent that keeps working longer, create an API key for it (scoped to its inbox if that is all it needs) and save it to a profile of its own: