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
| Invocation | Build | What it does |
|---|---|---|
mimux | any | Start the server. Opens the database, runs migrations, starts a sync worker per account, listens on MIMUX_HOST:MIMUX_PORT. |
mimux -version | any | Print the version and exit. |
mimux upgrade | any | Replace 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> | any | Print a shell completion script. See Shell completions below. |
mimux mail … | pro | The terminal client: read, search, triage, draft and send against a running mimux. |
mimux mcp | pro | Run the stdio MCP bridge against a running mimux. |
mimux licence status | pro | Report 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:
| Variable | Default |
|---|---|
MIMUX_DB | ./data/mimux.db |
MIMUX_HOST | 0.0.0.0 |
MIMUX_PORT | 8083 |
MIMUX_BASE_URL | http://localhost:<port> |
MIMUX_SECRET | generated once, persisted next to the database |
MIMUX_AI_BASE_URL | OpenRouter |
MIMUX_API_RATE_LIMIT pro | 120 requests per token per minute; 0 disables |
MIMUX_LICENCE_KEY pro | unset; 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.
| Flag | What it does |
|---|---|
--no-browser | Print 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. |
--scopes | What to pre-tick, space-separated. Default mail:read mail:modify accounts:read. |
--label | The token's name. Defaults to cli @ <hostname>. |
--insecure | Do 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.
| Variable | Flag | Default |
|---|---|---|
MIMUX_URL | --url | http://localhost:8083 |
MIMUX_TOKEN (or MIMUX_API_TOKEN) | --token | from 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.
| Command | Scope | What 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 whoami | — | The token's label and scopes. |
mimux mail accounts | accounts:read | Accounts with sync state, message and unread counts. |
mimux mail folders | mail:read | The folder tree per account, with folder ids. |
mimux mail list | mail:read | Messages newest-first; the unified inbox by default. |
mimux mail search <query> | mail:read | The client's own query language, local index or deep IMAP. |
mimux mail read <id> | mail:read | One message: headers, flags, attachments, body. |
mimux mail mark-read <id> | mail:modify | Mark read, or -unread. |
mimux mail star <id> | mail:modify | Star, or -off to unstar. |
mimux mail move <id> <dest> | mail:modify | A folder id, or archive/spam/trash. |
mimux mail draft | mail:send | Save a draft. Nothing is sent. |
mimux mail send | mail:send | Send now, or -dry-run to preview. |
mimux mail webhooks listen | webhooks:manage | Stream 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.
| Variable | Default | Description |
|---|---|---|
MIMUX_URL | what login stored, else http://localhost:8083 | Base URL of the running mimux. |
MIMUX_TOKEN (or MIMUX_API_TOKEN) | what login stored for that URL | An 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 code | Means |
|---|---|
0 | The API and MCP endpoints are answering. |
1 | They are paused, or the config/database could not be read. |
2 | Wrong 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