Technology June 21, 2026 7 min read

Higgs: an agent-first CLI for Proton Mail

#AI#Agents#CLI#Proton Mail#Privacy#Go#Local LLM#Ollama

A while back I wrote about the agent-first CLI as a design pattern — the argument that for most cases where one engineer wants their agent to drive a tool, a well-designed CLI beats an MCP server. The proof-of-concept I kept pointing at was a little Proton Mail CLI I’d been calling protoncli. It outgrew that name. It’s now Higgs, it lives in its own org at github.com/higgscli/higgs, and it shipped 1.0. This post is the deep dive: what it actually does, and how every decision in it points at the agent instead of the human.

The problem I refused to compromise on

I get somewhere between 300 and 700 emails a week across personal and work accounts. Triaging that by hand is a part-time job. Triaging it with an LLM is the obvious move — except I’m a Proton user precisely because of the privacy properties, and the whole point evaporates the moment my mail flows through somebody else’s server to get classified.

So the constraint was non-negotiable from line one: the email never leaves my hardware. No hosted API, no cloud model, no telemetry. That single constraint shapes the entire architecture.

Higgs connects to Proton Mail Bridge — Proton’s local IMAP gateway — over 127.0.0.1, and runs classification through a local Ollama model (Gemma by default, picked for native function-calling and a 128K context even on the small variants). The mail goes from Bridge, to the CLI, to a model running on my own machine, and back. Nothing sensitive ever touches a network it shouldn’t.

Every decision points at the agent

A normal mail CLI is built for a human at a terminal. Higgs is built for a language model in a loop. Here’s the contract, made concrete.

A schema manifest instead of --help archaeology. higgs schema emits a JSON description of every subcommand: flags, arguments, stdout format, exit codes. The agent loads it once and knows the entire surface. No prompt-engineered syntax, no copy-pasted help text, no screen-scraping. The CLI is the source of truth, and tool definitions get generated from it.

NDJSON on stdout, prose on stderr. Every successful output is structured JSON. Streaming commands emit line-delimited JSON and terminate with {"type":"summary"} so the caller knows the stream finished without heuristics. There is no English on stdout to parse.

Typed error envelopes. Failures come back as {"error": {"kind", "code", "reason", "message", "hint"}}. The agent branches on .error.kind, never on parsed prose. The hint field is written for the agent, not the human.

Exit codes as an enum. Nine distinct codes map 1:1 to error kinds. That makes retry logic deterministic enough to live in a shell script without an LLM in the loop: retry on code 5 (transient IMAP), prompt the user on code 2 (auth), surface code 4 (config) to the caller.

Sanitized stderr. Stderr gets fed back into the model’s context, which makes it a prompt-injection vector — so Higgs strips ANSI escapes, bidirectional controls, and zero-width characters before anything reaches it. Unsanitized stderr from a mailbox full of attacker-controlled content is exactly the kind of thing that ends up in a model’s context window and ruins your day.

Secrets out-of-band. Credentials live in the OS keyring — macOS Keychain, Windows Credential Manager, or Linux Secret Service — with an AES-256-GCM encrypted-file fallback when no keyring is available. The agent calls the CLI; the CLI reads from the keychain; the secret never flows through a prompt.

Checkpointed state. A classification run over thousands of messages will get interrupted. Higgs keeps state in SQLite with backfill (replay prior NDJSON logs into the state DB after a crash) and state clear. Agents crash. The tool survives it and picks up where it left off.

The flagship workload: classification

The headline command is classify. It walks your mailboxes, streams each message through the local model, and emits an NDJSON prediction per message — optionally writing the labels back to IMAP with --apply.

The classifier is constrained to an 11-label taxonomy: Orders, Finance, Newsletters, Promotions, Jobs, Social, Services, Health, Travel, Security, and Signups. Constraining the label space is what makes a small local model reliable — you are not asking it to be creative, you are asking it to bucket. And because models (and legacy folders) drift, there are 612 alias mappings that normalize off-taxonomy names back to the canonical set, with cleanup-labels to consolidate the mess that years of manual foldering leaves behind.

The full command set is small and composable:

  • scan-folders — enumerate IMAP mailboxes, return the canonical All Mail / Labels roots
  • classify — stream messages through Ollama, emit NDJSON predictions, optionally --apply
  • apply-labels — write pending labels from the state DB to IMAP
  • cleanup-labels — consolidate legacy labels into the canonical taxonomy
  • fetch-and-parse — pull and parse messages without classifying
  • backfill — replay prior NDJSON logs into the state DB after a crash
  • state — inspect or reset the SQLite state (stats, clear)
  • schema — the machine-readable manifest

Why this matters: the agent builds on top of it

Here’s the part that sold me. Because Higgs exposes primitives an agent can compose, the agent starts building infrastructure I never asked it to.

My kids’ Japanese school sends mail from five different addresses — general announcements, my kid’s class, events, billing, the parent association. I asked my agent to help me keep track of it. On its own, it wrote a small Python script on top of Higgs that uses the IMAP wrapper and keyring credentials, walks the relevant mailboxes, tracks the last-processed UID per mailbox so it’s idempotent, and copies matches into a dedicated label. Then it wrote a LaunchAgent to run that every ten minutes and at login, seeded the state at the current high-water UIDs so it wouldn’t re-touch already-labeled mail, and even handed me the teardown command.

I didn’t write any of that. The agent did, because the CLI gave it building blocks to compose. An MCP server is an endpoint. A CLI is a building block. That difference shows up the first time you ask an agent to do something the original tool author never anticipated — and higgs fetch-and-parse INBOX | jq 'select(.from | contains("school"))' | higgs apply-labels is a real pipeline, not a hypothetical.

Where it stands

Higgs is Go (95%+ of the codebase), Apache 2.0, built on emersion/go-imap and spf13/cobra, and installs via Homebrew or go install. It’s unofficial and unaffiliated with Proton — community-built, local-only, no API keys, no cloud, no telemetry.

It’s at github.com/higgscli/higgs. If you live in Proton and you’ve been wanting to point an agent at your inbox without giving up the reason you chose Proton in the first place, this is the tool I wished existed, so I built it.

Get the next post in your inbox

Short, practical notes on agent infrastructure, DevSecOps, and AI-native product work. No spam.

Powered by Buttondown.

Enjoyed this article? Check out more of my thoughts on startups and technology.

View All Posts