Skip to content
proton-cli

Output and exit codes

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.

Terminal window
$ proton mail messages list --unread --limit 3
ID FROM SUBJECT DATE FLAGS
──────── ──────────────── ───────────────────── ──────────────── ─────
5bH2mQxK Fastmail Billing Invoice #2291 ready 2026-04-15 14:32 ●★2
9xL4pQrT 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 1

ID is always the first column. FLAGS reads ● for unread, ★ for starred, ✗ for a message Proton flagged as phishing or suspicious, ⧗ for mail that deletes itself, and a number for attachments.

An empty collection prints nothing on stdout, so a redirect yields an empty file rather than a stray header. On stderr it says No messages., or No messages match. when a filter was applied.

Every list takes --page and --limit; --limit 0 returns the whole collection. Most also take --sort and --desc, and each names the sort keys it accepts. A listing whose order is the answer takes neither: mail comes back newest first, your addresses in the order the first one is the default, and your filters in the order they run.

Terminal window
$ proton drive items get /Documents/report.pdf
Name: report.pdf
Location: /Documents
Type: file
MIME Type: application/pdf
Uploaded: 2026-04-02 11:20
Signature: verified
Size: 2.4 MB
Shared: yes
ID: 7Kd91mQx

Decrypted content meant to be read: a header block, a blank line, the body, and whatever follows it. --body-only gives you the body alone.

Terminal window
$ proton mail messages get 5bH2mQxK
Subject: Invoice #2291 is ready
From: Fastmail Billing <billing@fastmail.com>
Date: 2026-04-15 14:32
Signature: verified
ID: 5bH2mQxK
Hi Roman, your invoice is attached.
Attachments
ID NAME SIZE
──────── ──────────────── ───────
kQ81mDx4 invoice-2291.pdf 84.2 KB
Terminal window
$ 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:

Terminal window
LABEL=$(proton mail settings labels create --name Work)

mail messages watch and calendar reminders watch stay attached and print a line the moment something happens.

Terminal window
$ proton mail messages watch
14:32 5bH2mQxK Fastmail Billing Invoice #2291 ready
14:41 9xL4pQrT Trailhead Weekly digest

In JSON, each line is one object, which jq reads without --slurp. There is no envelope and no footer.

Ctrl+C or SIGTERM ends a watch quietly with exit 0, so a service manager does not log a failure every time you stop it.

One line for the problem, then an indented Try: block for the fix.

Terminal window
$ proton mail messages get 5bH2mQxK --render htm
Error: --render accepts: text, html, raw.

Anything judgeable from your command line alone is caught before signing in or contacting Proton: a misspelled setting key, an impossible --render, a colour outside Proton’s palette.

The message says what happened and what to try; the code is what a script reads.

Code Meaning What to do
0 Success
1 Something you passed was wrong Fix the command. Nothing was sent
2 Authentication failed Sign in again, or fix the password
3 Not found Run the matching list
4 Ambiguous, or a conflict Narrow the term, or use the ID it printed
5 Network or server problem, including a rate limit Wait and retry
6 Refused by your confirmation policy Change the policy; nothing about the command was wrong
7 A bug in proton Report it. Nothing you typed caused it
8 Not supported yet See What it can’t do
130 Cancelled with Ctrl+C

For a scheduled job the difference between 2 and 5 is the one that matters: 2 means fix the credential, 5 means come back later. Retrying never helps with 6 or 8.

Terminal window
proton mail messages list --output json
proton mail messages list --output yaml

Every 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 present. total, page, page_size and has_more appear when the request involved them, so a consumer can tell “page 0” from “not paginated”.

A list is always a list. An empty collection is [] and an empty map is {}, never a missing key. So jq '.attendees[]' iterates nothing rather than failing on null.

A scalar that does not apply is left out entirely. For example, occurrence_count appears only when the number is known. An event that does not recur has no count, and neither does a series with no end date. Read rrule to tell those two apart.

Names, not numbers, matching the text output and set:

{ "type": "file", "state": "active", "unread": true }

Keys are snake_case, the collection’s own key included: a listing of watched addresses is watched_addresses. Timestamps are <verb>_time in Unix seconds. Sizes are size in bytes. IDs are always complete.

Times come back in the zone you are working in - --zone, or TZ, or zone: in your config, or your system’s. An event’s start and end are RFC 3339 with that offset, and the zone the event 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. It 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:

Terminal window
$ 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 marks the parts that carry a verdict: ✓ green, ! yellow, Error: red, ✗ red, IDs magenta, ● unread, ★ starred, and the signature, DMARC and Flagged lines 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.

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 it never carries meaning on its own: every verdict is spelled out in words.

--force-color or FORCE_COLOR paints anyway, for the cases proton cannot see: a pager, a multiplexer, a CI log that renders the escapes itself.

Terminal window
proton mail messages list --force-color | less -R
FORCE_COLOR=1 proton mail messages list | tee run.log

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.

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.

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.

Terminal window
proton account login --no-input # fail instead of asking
PROTON_NO_INPUT=1 proton account login