A terminal coding agent for Proton Lumo, written in Rust. It works the way Claude Code does: it holds a conversation with Lumo, reads and edits the files of the current project, and runs commands, with the same end-to-end encryption as the official web client.
Unofficial. Proton does not publish a Lumo API. lumo-cli speaks the protocol of their open-source web client (ProtonMail/WebClients,
packages/lumo-api-client), which may change without notice. Use it with your own account, at your own risk.
- Features · Installation · Usage
- Working on a project · Project configuration
- Server mode · Sign-in and the CAPTCHA
- Sandbox (Landlock) · Lumo's PGP key
- Architecture · On-disk files · Security
- Agent mode (the default): Lumo reads and edits files and runs commands through the native
function calling of
ai/v1/chat/completions. The read-only tools (list_dir,read_file,search) run directly;write_file,edit_fileandbashrequire confirmation, with the diff or the command shown beforehand. - End-to-end encryption, as in the web client: a fresh AES-256-GCM key per request, encrypted to
Lumo's PGP key, with associated data bound to the
request_id. Responses are verified. - Streamed responses, reasoning included (
/think). - Proton sign-in over SRP (the password never leaves the machine), TOTP, token refresh. Guest mode works without an account, under stricter limits.
- Models
lumo-lite(default),lumo-maxandapertus-15; server-side toolsweb_search,weather,stock,cryptocurrency.lumo-maxhas a daily quota and agent mode spends one request per tool step, hence the default. - A Landlock sandbox around the agent's commands, a dry-run mode, retries with back-off, and a configurable PGP-key source with rotation detection.
Prebuilt, statically linked binaries (x86_64 and aarch64, musl) are published by the release
workflow. The installer downloads the latest one, checks its SHA-256 against the release's
SHA256SUMS, and places it in ~/.local/bin:
curl -fsSL https://raw.githubusercontent.com/r3dlight/lumo-cli/main/install.sh | shLUMO_INSTALL_DIR changes the destination, LUMO_VERSION selects a tag and LUMO_BASE_URL points
at a mirror. Without the script, the same archive can be unpacked directly:
curl -fsSL https://github.com/r3dlight/lumo-cli/releases/latest/download/lumo-cli-x86_64-unknown-linux-musl.tar.gz | tar -xz -C ~/.local/binFrom source, with stable Rust 1.88 or newer (rust-toolchain.toml selects it) and a C compiler
for the TLS backend (aws-lc-sys):
cargo install --git https://github.com/r3dlight/lumo-cli --locked # or: cargo build --releaseAt runtime the binary needs nothing beyond the kernel: git for /commit, and rg (falling back
to grep) for search, are optional. The sandbox needs Landlock (Linux 5.13 or newer; 6.12 for
the whole policy) and is optional as well.
lumo-cli login # Proton sign-in (SRP + 2FA); optional but recommended
lumo-cli login --import-rclone # or: reuse an already authenticated Proton session (see CAPTCHA)
lumo-cli login --paste # or: paste UID + tokens by hand
lumo-cli # agent REPL in the current directory
lumo-cli --chat # plain chat, without local tools
lumo-cli -p "fix the bug in src/main.rs" # one-shot
lumo-cli --model lumo-lite --think # model choice + reasoning
lumo-cli --tools web_search # enable server-side web search
lumo-cli --yolo # auto-approve writes and commands (use with care)
lumo-cli --dry-run # show proposed writes and commands without running them
lumo-cli check-sandbox # prove the sandbox holds (see below)
lumo-cli logoutBefore each file write or shell command, the diff or the command is shown for approval
([y]es / [a]lways / [n]o), unless --yolo is set.
| Command | Effect |
|---|---|
/help |
list the commands |
/clear |
reset the conversation history |
/chat |
toggle between agent mode and plain chat |
/model [name] |
show or change the model (lumo-lite, lumo-max, apertus-15) |
/think |
toggle reasoning mode |
/tools [t] |
enable or disable a server-side tool (web_search, weather, …) |
/yolo |
toggle auto-approval of writes and commands |
/dry |
toggle dry-run (show without executing) |
/commit [msg] |
stage and commit the working tree in the project repository |
/attach <path> |
attach a text file as context for the next message |
/audit |
show where this session's action log is written |
/limits |
show the remaining quota on your plan |
/status |
session, mode, model, and cumulative token usage |
/quit |
exit |
| Option | Effect |
|---|---|
-p, --prompt <TXT> |
one-shot instead of the REPL |
--chat |
plain chat (no local tools) |
--model <NAME> |
lumo-lite (default), lumo-max, apertus-15 |
--think |
reasoning mode |
--yolo |
auto-approve writes and commands (use with care) |
--dry-run |
show actions without running them |
--tools a,b |
Lumo server-side tools (web_search, …) |
--sandbox / --no-sandbox |
require or disable the Landlock sandbox |
--ro |
read-only project (the sandbox and file tools refuse to modify it) |
--noexec |
deny execve of project files inside the sandbox |
--commit |
commit the working tree after each turn that changed files |
--attach <path> |
attach a text/document file as context (repeatable) |
--max-turns <N> |
maximum turns sent per request (default 40) |
Transient failures (connection, timeout, 502/503/504) are retried with exponential back-off; a quota 429 is not, and a 401 is refreshed once. A warning appears at startup when the deployed Lumo web app has a different major version from the one this client was tested against.
- Audit trail. In agent mode, every file write, edit and shell command is appended to a
per-session log in the state directory, outside the project so it is never committed and stays
out of the sandbox's reach. It records the action (path and diff, or command and exit status),
not command output, which could carry secrets.
/auditprints its path. - Git.
/commit [message]stages and commits the working tree;--commitdoes so after each turn that changed files, deriving a message from your instruction when you give none. It never pushes or forces, and calls git through an argument vector, not a shell. - Token usage. Each request prints its token count, and
/statusthe session total. - Attachments.
--attach <path>(repeatable) or/attach <path>adds a text or document file as context; files are size-capped and rejected if binary. Images are not supported yet. - Read-only review.
--romakes the project read-only for the sandbox and the file tools alike, which suits reviewing an untrusted repository.--noexecalso denies executing project files.
A repository may carry a .lumo-config/ directory:
instructions.md: guidance added to the system prompt (build and test commands, conventions), in the manner of aCLAUDE.md.config.toml: defaults formodel,tools,think,max_turns,ro,noexec, and the security-relevantyolo,no_sandbox,commit. Unknown keys are rejected.
A cloned repository is not trusted, so this config is never applied on its own. On first sight, and after any edit, lumo-cli shows what it would apply, flags the dangerous settings in red, and asks for approval, remembered by a SHA-256 of the content. Outside a terminal it fails closed. Command-line flags always win over the config.
lumo-cli config show # what it would apply, and whether it is approved
lumo-cli config allow # approve the current content non-interactively
lumo-cli config reset # forget the approval for this projectlumo-cli serve runs a local, OpenAI-compatible HTTP server backed by Lumo, with the encryption
done in-process, so editors and other OpenAI clients can use Lumo through this one binary.
lumo-cli serve # http://127.0.0.1:8787/v1
lumo-cli serve --port 9000 --api-key K # require `Authorization: Bearer K`It exposes GET /v1/models, POST /v1/chat/completions (streaming or not) and GET /health. It
binds to loopback by default; any other address requires --api-key, since whatever reaches the
port can spend your quota. The request model selects the tier and reasoning_effort: "high"
turns on reasoning. Function calling and image inputs are not translated yet.
lumo-cli signs in the way the web client does (anonymous session, matching x-pm-appversion,
browser User-Agent, cookie jar), which avoids the CAPTCHA in most cases. If Proton still demands
verification, typically over a VPN, Tor or a datacenter address:
- Run
lumo-cli loginagain; the challenge is often transient. Signing in once at https://account.proton.me from the same network also helps. - Avoid VPN, Tor and datacenter addresses for the sign-in.
- Reuse a session that is already authenticated:
lumo-cli login --import-rclonereads~/.config/rclone/rclone.conf(options--remote,--rclone-config),lumo-cli login --pastetakes a UID and tokens by hand, and--captcha-token <token>accepts thex-pm-human-verification-tokenof a CAPTCHA solved in a browser.
An imported session is validated against Lumo before it is stored. lumo-cli then shares it; its token refreshes may later invalidate the source (rclone, for instance), which then signs in again.
In agent mode, every shell command and search runs under the kernel's Landlock LSM, with no
external binary. lumo-cli builds a policy for the current project and launches each subprocess as
lumo-cli sandbox-exec -- …; that child restricts itself through the
landlock crate (landlock_restrict_self, no_new_privs),
then executes the program. The policy reaches the child through an environment variable, never a
file the sandbox could write, and the child is started from a file descriptor of the executable
opened at startup, so replacing the binary from inside the sandbox changes nothing. Each run gets a
private TMPDIR, and each project private XDG directories, following
claude-island.
| Allowed | Denied | |
|---|---|---|
| Files | the current project (rw + exec), detected toolchain caches, a private TMPDIR, /usr /bin … (read/exec), /etc /proc /sys (read) |
everything else: ~/.ssh, ~/.aws, dotfiles, other projects, /tmp, $HOME itself |
| Network | outbound TCP on 443/80/53 | listening (bind), other ports |
| Processes | signals to processes outside the sandbox, abstract UNIX sockets created outside it | |
| Secrets | SSH_AUTH_SOCK, AWS_*, GH_TOKEN, and any variable whose name ends in _TOKEN, _SECRET, _KEY, _PASSWORD, _PASSWD, _CREDENTIAL(S), _AUTH, _APIKEY, _BEARER, _PRIVATE or _CERT, removed before launch |
Toolchains are detected from marker files (Cargo.toml, package.json, pyproject.toml,
go.mod, Makefile/CMakeLists.txt) and their caches opened accordingly. lumo-cli's own file
tools, which are not subprocesses, are kept within the project root as well.
The sandbox is on whenever the kernel supports Landlock; otherwise the tool warns and runs
unconfined. --sandbox makes it mandatory, --no-sandbox turns it off; --ro and --noexec
tighten the project access. At startup lumo-cli pins the policy to the highest Landlock ABI the
kernel supports, the child requires exactly that ABI, and anything an older kernel cannot enforce
is printed as a warning:
| Kernel | Landlock ABI | Not enforced before it |
|---|---|---|
| 5.13 | 1 | (file access rights: the minimum) |
| 5.19 | 2 | renames and links across directories are always denied |
| 6.2 | 3 | truncate(2) of files outside the sandbox |
| 6.7 | 4 | outbound TCP restriction |
| 6.10 | 5 | device ioctls |
| 6.12 | 6 | scoping of signals and abstract UNIX sockets |
| 7.1 | 9 | connections to pathname UNIX sockets outside the allowed trees |
To verify rather than trust:
$ lumo-cli check-sandbox
[PASS] deny: read ~/.ssh
[PASS] deny: list $HOME
[PASS] deny: read ~/.aws/credentials
[PASS] allow: read /etc/hostname
[PASS] allow: write inside the project
[PASS] allow: execute /usr/bin/true
result: OK, every probe behaved as required
Each request's AES key is encrypted to Lumo's public PGP key, so a substituted key would expose
your prompts. lumo-cli therefore never adopts a key from the network on its own: the embedded key
is pinned, and a new one is taken only from $LUMO_PUBKEY_PATH, from the override file
~/.config/lumo-cli/lumo-pubkey.asc, or after explicit confirmation. A daily, offline-tolerant
check compares the active key with the one in Proton's WebClients repository and warns on a
rotation.
lumo-cli key show # source, fingerprint, user id, creation date
lumo-cli key check # compare against Proton's canonical key
lumo-cli key update # fetch it, show the fingerprint, confirm, store it
lumo-cli key update --file k.asc # or from a local file (or --url <URL>)
lumo-cli key reset # revert to the embedded keyVerify the fingerprint against a trusted Proton source before confirming an update.
| Module | Responsibility |
|---|---|
main.rs |
CLI (clap), subcommands, startup wiring |
protocol.rs |
Wire types of the Lumo v2 chat-completions protocol |
crypto.rs |
Per-request AES-256-GCM and PGP encapsulation of the key |
keys.rs |
Lumo's PGP key: sources, rotation detection, key update |
auth.rs |
SRP sign-in (proton-srp), 2FA, refresh, session import |
client.rs |
Encrypted requests, SSE streaming, retries, tool-call accumulation |
agent.rs |
The agent loop: tool definitions, approvals, tool turns |
tools.rs |
Local tools (files, search, shell) and project confinement |
sandbox.rs |
Landlock policy and self re-execution of confined subprocesses |
audit.rs |
Append-only audit trail |
git.rs |
Committing the agent's changes |
attach.rs |
Text and document attachments |
projconfig.rs |
.lumo-config with approval gating |
serve.rs |
The OpenAI-compatible server |
ui.rs |
REPL, streaming rendering, diffs, prompts |
Requests go to POST https://lumo.proton.me/api/ai/v1/chat/completions (SSE) with each message
carrying "encrypted": true and a lumo object holding the PGP-encrypted request key and the
request_id. Tool results are sent back as a lumo_tool_call turn followed by a tool turn,
both encrypted.
| Path | Contents |
|---|---|
~/.config/lumo-cli/session.json |
Proton session tokens; mode 600 |
~/.config/lumo-cli/lumo-pubkey.asc |
PGP key override, when present |
~/.config/lumo-cli/history.txt |
REPL input history |
~/.config/lumo-cli/approved-configs.json |
approved .lumo-config hashes |
~/.cache/lumo-cli/last-key-check |
timestamp of the last key-rotation check |
~/.local/state/lumo-cli/audit/*.jsonl |
per-session audit logs |
~/.cache/lumo-cli/sandbox/lumo-* |
private XDG directories of the sandboxed tools |
The conversation history lives only in memory and is discarded when the REPL exits.
- Conversations are encrypted between the client and the model, and response integrity is verified (GCM with associated data).
- The password serves only the local SRP handshake and is wiped afterwards; only session tokens are stored, in a mode-600 file. Each request's AES key is wiped when the request ends.
- Nothing is written or executed without approval, except under
--yolo. The audit log is owner-only and holds no command output. servebinds to loopback unless given an--api-key, compares it in constant time, bounds request bodies and generation time, and sets no CORS headers.- The code follows the ANSSI Rust guide, with the
mechanical rules enforced by the build:
unsafe_codeforbidden; the Clippy lintsunwrap_used,expect_used,panic,unreachable,indexing_slicing,string_slice,arithmetic_side_effects,cast_possible_truncationandmem_forgetset todenyinCargo.toml(tests exempt);overflow-checkskept in release; the stable toolchain pinned;cargo fmt,clippy -D warningsandcargo auditclean. The one acknowledged advisory (RUSTSEC-2023-0071, RSA decryption, reached transitively) is documented in.cargo/audit.tomlas unreachable, since lumo-cli performs no RSA decryption. - Every direct dependency is validated and recorded in
DEPENDENCIES.md.