Command line

The mimux binary has a deliberately small command line. Running it with no arguments starts the server; there are five subcommands, three of which come from the pro layer.

The whole surface

InvocationBuildWhat it does
mimuxanyStart the server. Opens the database, runs migrations, starts a sync worker per account, listens on MIMUX_HOST:MIMUX_PORT.
mimux -versionanyPrint the version and exit.
mimux upgradeanyReplace this binary with the newest release of the same build. --check reports without installing; --version vX.Y.Z pins. See Upgrading.
mimux completion <bash|zsh>anyPrint a shell completion script. See Shell completions below.
mimux mail …proThe terminal client: read, search, triage, draft and send against a running mimux.
mimux mcpproRun the stdio MCP bridge against a running mimux.
mimux licence statusproReport this install's licence and whether the API is answering.

That is the complete list. upgrade and completion are the only two a free binary has — the other three are registered from files carrying the pro build tag, so a free binary neither has them nor mentions them, and its completion script does not offer them either.

An unrecognised verb exits 2 with mimux: unknown command "…" rather than falling through and booting the server. Booting touches the database, including one-time migrations, which is a hostile response to a typo.

mimux — the server

mimux

No arguments, no config file. Everything it needs to find its way comes from the environment, each with a working default, so a fresh install with zero environment variables boots and runs:

VariableDefault
MIMUX_DB./data/mimux.db
MIMUX_HOST0.0.0.0
MIMUX_PORT8083
MIMUX_BASE_URLhttp://localhost:<port>
MIMUX_SECRETgenerated once, persisted next to the database
MIMUX_AI_BASE_URLOpenRouter
MIMUX_API_RATE_LIMIT pro120 requests per token per minute; 0 disables
MIMUX_LICENCE_KEY prounset; takes precedence over the key saved in Settings → Licence

Full descriptions are on the Installation page. The pre-rename SM_* names still work throughout the v0.20 series, logging one deprecation warning per variable, and are removed in the v0.21 series.

On startup the process also renames a pre-rename sm.db (and its -wal/-shm sidecars) to the mimux.db the config points at, when the new file is absent and the old one is present. A failed rename is fatal rather than ignored — silently booting onto an empty database next to a populated one presents as total data loss.

SIGINT and SIGTERM trigger a clean shutdown: sync workers stop and the HTTP server drains, with a 10-second timeout.

Shell completions

mimux completion bash and mimux completion zsh print a completion script to stdout. It is generated from the same subcommand registry the dispatcher itself uses, so it always matches what the running binary implements — a free binary's script offers completion and upgrade only; a pro binary's also offers mail, mcp, licence, and completes mimux mail <TAB> with the full verb list.

# bash, system-wide (root)
mimux completion bash | sudo tee /etc/bash_completion.d/mimux >/dev/null

# bash, this user only
mkdir -p ~/.local/share/bash-completion/completions
mimux completion bash > ~/.local/share/bash-completion/completions/mimux
# picked up automatically by bash-completion; otherwise add to ~/.bashrc:
#   source ~/.local/share/bash-completion/completions/mimux

# zsh, any directory on $fpath
mkdir -p ~/.zsh/completions && fpath+=(~/.zsh/completions)  # in ~/.zshrc, before compinit
mimux completion zsh > ~/.zsh/completions/_mimux
autoload -Uz compinit && compinit

Running mimux completion with no argument, or one it does not recognise, prints the same usage and install instructions to stderr and exits 2.

mimux mail — the terminal client pro

A mailbox from the shell: list, search, read, triage, draft and send. It is the third way into the same automation layer — for a shell, a cron job, a jq pipeline, or an agent that has a bash harness but no MCP client.

Like the MCP bridge below, it is a thin HTTP client of a running mimux. It never opens the database: the server process owns that file and is actively writing to it, so a second writer would be a corruption and lock-contention problem. It authenticates with the same scoped API tokens everything else uses, which is also what keeps the pro layer behind its licence — there is no second authorisation surface, and nothing here that a free binary plus a file path could reach.

An agent skill teaches a coding agent to drive this CLI — auth, the query language, scopes, failure modes, and the never-send-without-approval rule. Install it with:

npx skills add mattmezza/mimux --skill mimux-cli

Signing in

$ mimux mail login https://mail.example.com
Opened your browser to approve this login. If nothing happened, go to:
https://mail.example.com/cli/auth?challenge=…&name=cli+%40+laptop&port=49711&scopes=…&state=…
Waiting for the browser…
Signed in to https://mail.example.com as "cli @ laptop" (mail:read mail:modify accounts:read).
Saved to /home/you/.config/mimux/credentials.json

The browser opens on an approval page inside your existing mimux session. You name the token, tick what it may do — pre-ticked from what the command asked for, but what you leave ticked is what it gets — pick an expiry, and approve. The token travels back over a loopback port the CLI is listening on and is written to $XDG_CONFIG_HOME/mimux/credentials.json ( ~/Library/Application Support on macOS), mode 0600 in a 0700 directory. It is never printed, and never has to be pasted anywhere.

Under the hood it is an authorization code with PKCE: the CLI generates a verifier, sends only its SHA-256 in the URL, and presents the verifier when it redeems the code. Another local process that raced for the loopback port would hold a code it cannot spend. The code is good for one use and expires in two minutes. The token it buys is an ordinary token from Settings → API — listed there, revocable there.

FlagWhat it does
--no-browserPrint the URL instead of opening it; the page shows a code to paste back at the Code: prompt. Also what happens automatically when no opener is found — the SSH case.
--scopesWhat to pre-tick, space-separated. Default mail:read mail:modify accounts:read.
--labelThe token's name. Defaults to cli @ <hostname>.
--insecureDo not verify TLS certificates for this instance — for a self-signed cert on the LAN. Remembered, so it is not re-typed into a script, and every later command that uses that instance says so on stderr.

Pointed at a free build, it stops before opening anything: a free binary has no /api for the CLI to talk to, and says so rather than walking you through a login that could not have worked.

More than one mimux

$ mimux mail login https://mail.example.com
$ mimux mail login http://nas.lan:8083 --insecure
$ mimux mail use https://mail.example.com    # commands now default here
$ mimux mail use                             # list what is signed in
$ mimux mail logout http://nas.lan:8083      # forget it locally

logout removes the stored entry and nothing else — the token still exists until you revoke it in Settings → API, which is the honest thing for a local command to say. With exactly one instance signed in it needs no argument; with more than one it asks which rather than guessing.

Configuration

Where a command talks, first match wins: --url, MIMUX_URL, the instance use pinned, the only instance signed in, http://localhost:8083. Which token it presents: --token, MIMUX_TOKEN (or MIMUX_API_TOKEN), the stored entry for whichever instance that turned out to be.

VariableFlagDefault
MIMUX_URL--urlhttp://localhost:8083
MIMUX_TOKEN (or MIMUX_API_TOKEN)--tokenfrom mimux mail login

The environment is still there for CI and containers, where no browser is going to open and no config directory survives the run. mimux mcp reads the same store and the same two variables, so a login serves both. With nothing stored and nothing exported, commands point at login rather than dumping a 401:

$ mimux mail accounts
mimux mail accounts: not signed in — run `mimux mail login http://localhost:8083`, or set MIMUX_TOKEN to a token from Settings → API
export MIMUX_URL=https://mail.example.com
export MIMUX_TOKEN=mimux_pat_...

The commands

They mirror the MCP tools one for one — same nouns, same scopes, same semantics — because they are the same product wearing a different skin. The scope column is what the token must carry; a token that is missing one is told which one.

CommandScopeWhat it does
mimux mail login [<url>]Approve a token in the browser and store it.
mimux mail logout [<url>]Forget a stored token. Revoking it is a separate, deliberate act in Settings → API.
mimux mail use [<url>]Make an instance the default, or list what is signed in.
mimux mail whoamiThe token's label and scopes.
mimux mail accountsaccounts:readAccounts with sync state, message and unread counts.
mimux mail foldersmail:readThe folder tree per account, with folder ids.
mimux mail listmail:readMessages newest-first; the unified inbox by default.
mimux mail search <query>mail:readThe client's own query language, local index or deep IMAP.
mimux mail read <id>mail:readOne message: headers, flags, attachments, body.
mimux mail mark-read <id>mail:modifyMark read, or -unread.
mimux mail star <id>mail:modifyStar, or -off to unstar.
mimux mail move <id> <dest>mail:modifyA folder id, or archive/spam/trash.
mimux mail draftmail:sendSave a draft. Nothing is sent.
mimux mail sendmail:sendSend now, or -dry-run to preview.
mimux mail webhooks listenwebhooks:manageStream live webhook events to a local URL (-forward-to, signed like production) or run a shell command per event (-execute, payload on stdin) — see Webhooks.

Every command takes --url, --token and --json; -h on any of them lists its own flags. Exit codes are 0 fine, 1 the operation failed, 2 wrong usage.

Reading

$ mimux mail whoami
url:    http://localhost:8083
token:  laptop
scopes: mail:read accounts:read

$ mimux mail accounts
ACCOUNT     EMAIL                   STATE  MESSAGES  UNREAD  LAST SYNC
GMail       me@gmail.com            ok     3520      151     2026-08-18 15:02
merola.co   matteo@merola.co        ok     2157      38      2026-08-18 15:01

$ mimux mail folders -account merola.co
merola.co
  Inbox          86  inbox    4 unread
  Sent           93  sent
  Archive        98  archive  1 unread
  Spam           90  spam     32 unread
  Trash          91  trash
  Invoices       96

list is the unified inbox unless you narrow it with -account or -folder; -unread and -starred filter, and --cursor takes the value printed at the bottom of a page. The FLAGS column is three characters — u unread, * starred, @ has an attachment:

$ mimux mail list -unread -limit 5
ID     DATE              FLAGS  FROM                        SUBJECT
12792  2026-08-17 10:19  u..    Jacob Riff                  Re: prodcontent + enterprise security reviews?
12689  2026-08-13 23:47  u..    ZeptoMail Notifications     Credits expiry
12622  2026-08-13 00:03  u..    Google AI Studio            [Action Required] Update your Gemini API billing
12479  2026-08-03 15:55  u.@    "Cal.eu"                    FlowRent Demo between Matteo Merola and Ashley Nell

more: --cursor 2026-08-03T13:55:02Z_12479

-limit counts conversations, not rows: a page carries every message of the conversations it contains, so a thread is never cut in half across a page boundary. That is why the output above can be longer than the limit you asked for.

search speaks the same query language as the search box — from:, to:, subject:, body:, is:unread, is:starred, has:attachment, before:/after: with YYYY-MM-DD, quoted phrases, and - to negate:

$ mimux mail search receipt -limit 3
ID     DATE              FLAGS  FROM               SUBJECT
12819  2026-08-17 23:29  ..@    OpenRouter, Inc    Your receipt from OpenRouter, Inc #2536-8232
12697  2026-08-13 19:05  ...    "Purelymail, LLC"  Your Purelymail, LLC receipt [#1447-0849]

Flags may follow the query, which is how people actually type. A query that starts with the negation - needs a -- first, so the shell's argument parser does not read it as a flag:

mimux mail search -- -from:noreply is:unread

-deep asks the IMAP servers themselves instead of the local index — for very old mail, or folders that rarely sync. It waits up to -wait (30s by default) and otherwise hands back the job id; the result is cached, so re-running the identical query answers instantly.

$ mimux mail read 12819
id:       12819
account:  GMail
from:     OpenRouter, Inc <invoice+statements@openrouter.ai>
to:       me@gmail.com
subject:  Your receipt from OpenRouter, Inc #2536-8232
date:     Mon, 17 Aug 2026 23:29:11 CEST
flags:    ..@
attach:   [0] receipt.pdf (application/pdf, 41231 bytes)

Thanks for your business...

-html returns the sanitised HTML body instead of the plain-text one. -headers raw prints the message's own header block as it arrived, -headers parsed prints it one field per line with repeated fields kept, and -headers both prints both.

mimux mail header 123
mimux mail download 123 --output message.eml
mimux mail forward-eml 123 recipient@example.com

forward-eml saves a draft and never sends it. Review the draft in mimux, then send it explicitly.

Triage

$ mimux mail mark-read 12792
12792 marked read

$ mimux mail star 12816
12816 starred

$ mimux mail move 12689 archive
12689 moved to archive

$ mimux mail move 12689 96          # or a folder id from `mimux mail folders`
12689 moved to 96

trash is the one destination that needs a second, explicit statement of intent, exactly as the move_message MCP tool requires confirm=true — from the human's point of view it is how mail disappears:

$ mimux mail move 12689 trash
mimux mail move: moving to trash needs -yes — it deletes mail from the human's point of view
usage: mimux mail move <id> <folder-id|archive|spam|trash>

Drafting and sending

The body comes from -body, or from standard input when that flag is omitted — so the text can come out of a file, a heredoc, or another program. draft saves and sends nothing; the draft appears in Drafts in the web UI like any other.

$ echo "Thanks — next week works." | mimux mail draft -in-reply-to 12792
account:  merola.co
to:       jacob@example.com
subject:  Re: prodcontent + enterprise security reviews?

Thanks — next week works.

Draft 4 saved, NOT sent. It is in Drafts in the web UI; send it with
`mimux mail send` once you are happy with the text above.

-in-reply-to takes a mimux message id and derives the recipient, the subject and the threading headers from the original, the way draft_reply does for an agent. -reply-all keeps every original recipient, minus you.

send takes the same flags and sends immediately. -dry-run prints exactly what would go out and sends nothing, which is the CLI's version of the draft-then-approve step: show the preview to the human, then re-run without the flag.

$ mimux mail send -to ada@example.com -subject "Re: invoice" -body "Paid, thanks." -dry-run
account:  merola.co
to:       ada@example.com
subject:  Re: invoice

Paid, thanks.

Dry run — nothing was sent. Re-run without -dry-run to send it.

$ mimux mail send -to ada@example.com -subject "Re: invoice" -body "Paid, thanks."
sent <3f9c1e2a@merola.co>

-at takes an RFC3339 time and queues the message in the outbox instead of sending it now.

Sending a saved draft is not a CLI operation. The REST API has no send-this-draft endpoint — send_draft is MCP-only — so draft and send are two separate acts here. Compose with -dry-run when what you want is a preview before sending.

Piping to jq

--json prints the API's response verbatim — not a re-encoding — so what jq sees is exactly what the API reference documents. This is the mode to script against; the tables above are for humans and their column widths are not a contract.

$ mimux mail search 'is:unread from:google' -limit 3 --json | jq -r '.data[] | "\(.id)\t\(.from.address)\t\(.subject)"'
12622	googleaistudio-noreply@google.com	[Action Required] Update your Gemini API billing in Google AI Studio
12135	sc-noreply@google.com	New reasons prevent pages from being indexed on site humux.dev

Archive every unread newsletter older than a month, in one line:

mimux mail search 'is:unread newsletter before:2026-07-18' --json \
  | jq -r '.data[].id' \
  | while read -r id; do mimux mail move "$id" archive; done

Which sender is filling the inbox:

mimux mail list -limit 200 --json \
  | jq -r '.data[].from.address' \
  | sort | uniq -c | sort -rn | head

Errors go to standard error and never into the JSON stream, so a pipeline fails cleanly. A missing scope names itself, because an agent told forbidden cannot fix itself and one told which scope can tell its operator:

$ mimux mail star 12816
mimux mail star: this token is missing the mail:modify scope — tick it in Settings → API and mint a new token

mimux mcp — the stdio MCP bridge pro

Speaks MCP on stdin/stdout and forwards every tool call to a running mimux's /api/mcp endpoint. It exists for MCP clients — editors, desktop apps — that cannot speak streamable HTTP themselves.

It never opens the database: one server process owns that. So it is safe, and in fact required, to run it while the server is up.

VariableDefaultDescription
MIMUX_URLwhat login stored, else http://localhost:8083Base URL of the running mimux.
MIMUX_TOKEN (or MIMUX_API_TOKEN)what login stored for that URLAn API token from Settings → API.

If you have run mimux mail login, the bridge already has everything it needs — the config block is just the command:

mimux mcp
# or, for CI and containers where nothing was stored:
MIMUX_URL=http://localhost:8083 MIMUX_TOKEN=mimux_pat_... mimux mcp

With nothing stored and nothing exported it prints mimux mcp: not signed in — run `mimux mail login …` and exits 1. A command plus an optional env map is exactly the shape of an MCP client's config block — see MCP server for one.

mimux licence status pro

Prints what this install makes of its licence. Unlike the two commands above it talks to no server: it reads the same config and the same database, but opens the store read-only, so it runs happily alongside a live mimux and writes nothing.

$ mimux licence status
status:    Annual licence, expires 2027-03-14 (m…o@example.com, from env).
key:       MIMUX_LICENCE_KEY
build:     v0.21.0-pro
released:  2026-03-10
plan:      annual
email:     m…o@example.com
issued:    2026-03-14
expires:   2027-03-14
api:       answering

key: is one of MIMUX_LICENCE_KEY, saved in Settings → Licence, or none configured. With no licence at all it prints a trial: line instead of the payload fields — either when the trial started, or a note that the server stamps that on its first pro boot. An expired annual licence inside its grace period adds a grace: line.

released: is the date this build went out, linked into the binary. A perpetual licence adds a covered: line giving the date its coverage runs to and whether this build falls inside it — that comparison is the whole perpetual rule. A binary you compiled yourself has no release date, prints unknown, and is never gated on one. Keys issued before coverage dates existed carry a watermark: line instead, which for them is what is enforced.

Exit codeMeans
0The API and MCP endpoints are answering.
1They are paused, or the config/database could not be read.
2Wrong usage — the only accepted form is mimux licence status.

That makes it one line in a monitoring script:

mimux licence status >/dev/null || alert "mimux API is paused"

Whatever it reports, mail keeps working. The licence gate covers /api/v1/* and /api/mcp and nothing else.

Development targets

Not part of the CLI, but worth knowing: the repository's Makefile carries the developer entry points. make help lists them all.

make setup      # air, golangci-lint, npm deps
make css        # Tailwind watcher (separate terminal)
make dev        # hot-reloading server
make build      # the free binary
make build-pro  # the commercial binary
make check      # lint, tests, and the three licence-split verifications
make diagnose   # sanitised environment dump to paste into a bug report