Output
Data goes to stdout. Progress bars, confirmations, footers, warnings, prompts and errors go to stderr. So a redirect gives you clean data and you still see what happened.
The five shapes
Section titled “The five shapes”Collections
Section titled “Collections”$ proton mail messages list --unread --page-size 3ID FROM SUBJECT DATE FLAGS──────── ──────────────── ───────────────────── ──────────────── ─────5bH2mQxK Fastmail Billing Invoice #2291 ready 2026-04-15 14:32 ●★29xL4pQrT Trailhead Weekly digest 2026-04-15 09:02 ●2mNp7RsV Jane Roe Re: Quarterly numbers 2026-04-14 17:48
3 of 47 messages. Next page: --page 1ID is always first. FLAGS reads ● unread, ★ starred, and a number for attachments.
An empty collection prints nothing on stdout, so a redirect yields an empty file rather than a stray header. It says No messages. on stderr, or No messages match. when a filter was applied.
Every list takes --page and --page-size. Everything except mail also takes --sort and --desc; each listing offers only the keys it has and says so when given another. Mail comes back newest first, ordered by Proton.
Records
Section titled “Records”$ proton drive items get /Documents/report.pdfName: report.pdfLocation: /DocumentsType: fileMIME Type: application/pdfUploaded: 2026-04-02 11:20Signature: verifiedSize: 2.4 MBShared: yesID: 7Kd91mQxDocuments
Section titled “Documents”Decrypted content meant to be read: a header block, a blank line, the body, and whatever trails it. --body-only gives you exactly the body.
$ proton mail messages get 5bH2mQxKSubject: Invoice #2291 is readyFrom: Fastmail Billing <billing@fastmail.com>Date: 2026-04-15 14:32Signature: verifiedID: 5bH2mQxK
Hi Roman, your invoice is attached.
AttachmentsID NAME SIZE──────── ──────────────── ───────kQ81mDx4 invoice-2291.pdf 84.2 KBMutations
Section titled “Mutations”$ proton mail messages trash --unread --older-than 30d✓ Moved 12 messages to trash.When something is created, its ID goes to stdout and the confirmation to stderr, so capturing it is a plain assignment:
LABEL=$(proton mail settings labels create --name Work)Streams
Section titled “Streams”mail messages watch and calendar reminders watch stay attached and print a line the moment something happens.
$ proton mail messages watch14:32 5bH2mQxK Fastmail Billing Invoice #2291 ready14:41 9xL4pQrT Trailhead Weekly digestIn JSON, each line is one object - what jq reads without --slurp. There is no envelope and no footer (why). Ctrl+C or SIGTERM ends a watch quietly with exit 0, so a service manager doesn’t log a failure every time you stop it.
Errors
Section titled “Errors”One line for the problem, an indented Try: block for the fix.
$ proton mail messages get 5bH2mQxK --render htmError: --render accepts: text, html, raw.Anything judgeable from your command line alone - a misspelled setting key, an impossible --render, a colour outside Proton’s palette - is caught before signing in or contacting Proton.
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
0 |
Success |
1 |
Something you passed was wrong |
2 |
Authentication failed |
3 |
Not found |
4 |
Ambiguous, or a conflict |
5 |
Network or server problem |
130 |
Cancelled with Ctrl+C |
JSON and YAML
Section titled “JSON and YAML”proton mail messages list --output jsonproton mail messages list --output yamlEvery collection is one envelope, keyed by its plural name:
{ "messages": [ { "id": "5bH2…", "subject": "Invoice #2291 is ready", "unread": true } ], "count": 3, "total": 47, "page": 0, "page_size": 3, "has_more": true}count is always there. total, page, page_size and has_more appear when the request involved them, so a consumer can tell “page 0” from “not paginated”.
Names, not numbers, matching the text output and set: { "type": "file", "state": "active", "unread": true }. Keys are snake_case, timestamps are <verb>_time in Unix seconds, sizes are size in bytes, and IDs are always complete.
Times come back in your own zone. An event’s start and end are RFC 3339 with your offset, and what it is anchored to is its own field:
{ "start": "2026-04-16T16:00:00+02:00", "end": "2026-04-16T17:00:00+02:00", "zone": "Europe/Vienna" }An all-day event has "all_day": true, begins at midnight on the date it names, and ends at the midnight after its last day - so end - start is how long it lasts.
Mutations emit JSON too:
$ proton mail settings labels create --name Work --output json{ "action": "created", "count": 1, "dry_run": false, "ids": ["kQ81mDx4T9…"], "kind": "label", "name": "Work" }The one exception is proton api, which passes Proton’s response through unchanged.
Colour, width and quiet
Section titled “Colour, width and quiet”Colour marks the parts that carry a verdict: ✓ green, ! yellow, Error: red, IDs magenta, ● unread, ★ starred, and the signature line green, yellow or red. A ■ beside a label, folder, calendar or group is the exact colour Proton stores for it. Everything else stays plain.
Shades come from your terminal’s theme, not from proton (why). Colour is off whenever output is piped or redirected, under --output json or yaml, and with --no-color or NO_COLOR. It never changes the layout and never carries meaning on its own - every verdict is spelled out.
On a terminal, a table too wide to fit gives up room from its widest flexible column, never from a date or an ID. Piped output is never truncated, and widths are measured in terminal cells, so a subject in Japanese or a filename with an emoji stays aligned.
--quiet silences confirmations, notes, footers and progress. It never silences the answer.
Prompts
Section titled “Prompts”Only account login and the commands that need your password again ever ask a question, and only when standard input is a terminal. Everything else fails with a message rather than waiting, so a scheduled job never hangs. Prompts go to stderr.
proton account login --no-input # fail instead of askingPROTON_NO_INPUT=1 proton account login