{"slug": "show-hn-higgs-a-local-ai-cli-for-proton-mail-no-cloud-no-telemetry", "title": "Show HN: Higgs, a local AI CLI for Proton Mail (no cloud, no telemetry)", "summary": "Higgs, an unofficial, agent-first CLI for Proton Mail designed to be driven by language models, has been released as a local-only tool with no cloud dependencies or telemetry. The project, built by the community and not affiliated with Proton AG, features a schema manifest, NDJSON streaming, typed error envelopes, and checkpointed state to simplify integration with AI agents. Its first workload is a Proton Mail inbox classifier using a local LLM via Ollama and Proton Mail Bridge.", "body_md": "An agent-first CLI for Proton Mail. Schema manifest for tool use, NDJSON on stdout, typed error envelopes, and a stable exit-code enum — designed to be driven by a language model, not a human.\n\nUnofficial project.`higgs`\n\nis an independent, community-built CLI for Proton Mail. It is not affiliated with, endorsed by, or sponsored by Proton AG. \"Proton\", \"Proton Mail\", and related marks are trademarks of Proton AG; this project uses them only to describe interoperability.\n\nWiring a normal CLI into an agent loop is painful. Stdout mixes prose and data, errors are English sentences, exit codes are 0-or-1, and the only tool specification is `--help`\n\n. `higgs`\n\ninverts that. Every design decision assumes the primary caller is a model:\n\n**Schema manifest.**`higgs schema`\n\nemits a JSON description of every subcommand — flags, args, stdout format, exit codes. An agent loads it once and can drive the tool without prompt-engineered command syntax.**NDJSON streaming with a terminator.** Every streaming command emits one JSON object per line and ends with`{\"type\":\"summary\", ...}`\n\n. Callers know when a stream is done without heuristics.**Typed error envelopes.** Every failure emits`{\"error\": {\"kind\", \"code\", \"reason\", \"message\", \"hint\"}}`\n\n. Agents branch on`.error.kind`\n\n, not on parsed English.**Exit codes as an enum.** Exit codes map 1:1 to error kinds, so retry and escalation are deterministic: retry on`5 imap`\n\n, prompt the user on`2 auth`\n\n, surface to the caller on`4 config`\n\n.**Sanitized stderr.** Human-readable progress on stderr, stripped of ANSI escapes, bidi controls, and zero-width characters — safe to feed back into a model's context.**Checkpointed state.** SQLite state DB with`backfill`\n\nand`state clear`\n\nso runs are resumable across crashes and restarts.**Secrets out-of-band.** Credentials go to the OS keyring (macOS Keychain, Windows Credential Manager, libsecret on Linux) with an AES-256-GCM file fallback, so nothing sensitive flows through an agent's context. The first workload riding this contract is a local-only Proton Mail inbox classifier via Proton Mail Bridge and Ollama. The classifier is useful on its own, but the contract is the point.\n\n`higgs classify`\n\nconnects to a running [Proton Mail Bridge](https://proton.me/mail/bridge) over IMAP, streams each message through a local LLM ([Ollama](https://ollama.com/) by default, or any self-hosted OpenAI-compatible server such as llama.cpp `llama-server`\n\n— see `PM_LLM_BACKEND`\n\n), and applies one or more labels from an 11-category taxonomy. Every step runs on `localhost`\n\n: no API keys, no cloud inference, no telemetry.\n\nThe default model is [Gemma 4](https://ollama.com/library/gemma4), chosen because it has native function-calling support, a 128K context window on the small variants, and fits comfortably on a laptop.\n\n-\nInstall and sign into\n\n[Proton Mail Bridge](https://proton.me/mail/bridge). Note the IMAP username and bridge password it assigns. -\nInstall\n\n[Ollama](https://ollama.com/download)and pull a model:\n\n```\nollama pull gemma4\n```\n\n-\nInstall\n\n`higgs`\n\n:\n\n```\n# Homebrew (macOS & Linux)\nbrew tap higgscli/higgs\nbrew install higgs\n\n# go install\ngo install github.com/higgscli/higgs/cmd/higgs@latest\n```\n\nOr build from source:\n\n```\ngit clone https://github.com/higgscli/higgs.git\ncd higgs\nmake build\n```\n\n-\nExport Bridge and Ollama settings. A\n\n`.env`\n\nat the repo root works:\n\n```\nexport PM_IMAP_USERNAME=\"alice@proton.me\"\nexport PM_IMAP_PASSWORD=\"bridge-generated-password\"\nexport PM_IMAP_HOST=\"127.0.0.1\"\nexport PM_IMAP_PORT=\"1143\"\nexport PM_OLLAMA_MODEL=\"gemma4\"\n```\n\n-\nDry-run against your inbox:\n\n```\nhiggs classify --dry-run --limit 20 INBOX\n```\n\nReview the NDJSON. When the suggestions look right, rerun with\n\n`--apply`\n\nto write labels back to Proton.\n\n`higgs schema`\n\nreturns a manifest of every subcommand. Load it once, drive the CLI from it.\n\n```\nhiggs schema classify\n{\n  \"name\": \"classify\",\n  \"summary\": \"Classify messages with Ollama and optionally apply labels\",\n  \"args\": [{\"name\": \"mailbox\", \"required\": false, \"default\": \"INBOX\"}],\n  \"flags\": [\n    {\"name\": \"dry-run\", \"type\": \"bool\", \"description\": \"Preview suggestions without writing labels\"},\n    {\"name\": \"apply\", \"type\": \"bool\", \"description\": \"Apply suggested labels to IMAP\"},\n    {\"name\": \"limit\", \"type\": \"int\", \"default\": 100},\n    {\"name\": \"workers\", \"type\": \"int\", \"default\": 4},\n    {\"name\": \"no-state\", \"type\": \"bool\"},\n    {\"name\": \"reprocess\", \"type\": \"bool\"}\n  ],\n  \"stdout\": \"ndjson\",\n  \"exit_codes\": [0, 2, 3, 4, 5, 6, 7, 9]\n}\n```\n\n**stdout**: structured JSON. Single-object commands pretty-print; streaming commands emit NDJSON.** stderr**: human-readable progress, sanitized of ANSI escapes, bidi controls, and zero-width characters.** NDJSON terminator**: every streaming command ends with one`{\"type\":\"summary\", ...}`\n\nline. Read until you see it.**Error envelope**: failures emit a typed envelope with`kind`\n\n,`code`\n\n,`reason`\n\n,`message`\n\n, and`hint`\n\n.\n\n```\n{\n  \"error\": {\n    \"kind\": \"config\",\n    \"code\": 400,\n    \"reason\": \"configError\",\n    \"message\": \"PM_IMAP_USERNAME is required\",\n    \"hint\": \"export PM_IMAP_USERNAME=<bridge-username>\"\n  }\n}\n```\n\n| Code | Kind | Description |\n|---|---|---|\n| 0 | success | Command completed without error |\n| 1 | api | Upstream API error (generic) |\n| 2 | auth | Authentication or credential failure |\n| 3 | validation | Invalid flags, arguments, or input |\n| 4 | config | Missing or malformed configuration |\n| 5 | imap | IMAP protocol or connection error |\n| 6 | classify | Classification error (Ollama, prompt, parsing) |\n| 7 | state | State DB error (SQLite) |\n| 8 | discovery | Mailbox discovery failure |\n| 9 | internal | Unexpected internal error |\n\nThe intended flow is: discover mailboxes, classify them, apply labels. Everything else (`backfill`\n\n, `cleanup-labels`\n\n, `state`\n\n, `verify`\n\n) exists to repair or inspect state along the way.\n\nCommands compose over pipes: every command that accepts an explicit `--uid`\n\nlist also accepts `--uid -`\n\n, which reads the UID set from stdin. Stdin may be plain UIDs (comma- or whitespace-separated) or NDJSON, from which each row's numeric `uid`\n\nfield is taken (rows without one, like `summary`\n\nlines, are skipped). So any command's NDJSON output is directly usable as another's input:\n\n```\nhiggs search INBOX --before 2025-07-01 | higgs archive INBOX --uid -\nhiggs state query INBOX --is-mailing-list false | higgs move INBOX Folders/Personal --uid -\n```\n\nEnumerate IMAP mailboxes and return the canonical `All Mail`\n\nand `Labels`\n\nroots.\n\n```\nhiggs scan-folders\n{\n  \"mailboxes\": [\n    {\"name\": \"INBOX\", \"delimiter\": \"/\", \"messages\": 1204, \"unseen\": 18, \"attributes\": []},\n    {\"name\": \"Labels/Orders\", \"delimiter\": \"/\", \"attributes\": [\"\\\\HasNoChildren\"]}\n  ],\n  \"all_mail\": \"All Mail\",\n  \"labels_root\": \"Labels\"\n}\n```\n\nStream messages through Ollama and emit one NDJSON object per message, followed by a `summary`\n\nterminator. Add `--apply`\n\nto write labels back to IMAP in the same pass.\n\n```\nhiggs classify --dry-run --limit 20 INBOX\nhiggs classify --apply --workers 4 \"Folders/Accounts\"\nhiggs classify --reprocess --no-state INBOX\n```\n\nFlags: `--dry-run`\n\n, `--apply`\n\n, `--limit N`\n\n, `--no-state`\n\n, `--reprocess`\n\n, `--workers N`\n\n.\n\n```\n{\"mailbox\":\"INBOX\",\"uid\":1842,\"uid_validity\":1,\"subject\":\"Your order has shipped\",\"from\":\"ship-confirm@amazon.com\",\"date\":\"2026-04-09T14:22:10Z\",\"suggested_labels\":[\"Orders\"],\"confidence\":0.94,\"rationale\":\"Shipping notification with tracking number\",\"is_mailing_list\":false}\n{\"type\":\"summary\",\"mailbox\":\"INBOX\",\"classified\":20,\"errors\":0,\"skipped\":0}\n```\n\nApply pending labels recorded in the state DB. Use when `classify`\n\nran without `--apply`\n\n.\n\n```\nhiggs apply-labels --limit 100 \"Folders/Accounts\"\nhiggs apply-labels --dry-run \"Folders/Accounts\"\n```\n\nConsolidate legacy or user-created labels into the canonical 11-label taxonomy. Useful after migrating from Proton's built-in filters.\n\n```\nhiggs cleanup-labels --dry-run\nhiggs cleanup-labels\n```\n\nFetch and parse messages without classifying. Useful for piping into other tools.\n\n```\nhiggs fetch-and-parse INBOX | jq 'select(.from | contains(\"github\"))'\n```\n\nReplay a prior `classify`\n\nNDJSON log into the state DB. Recovers state after a crash or migration.\n\n```\nhiggs backfill classify.log\n```\n\nInspect or reset the SQLite state DB.\n\n```\nhiggs state stats\nhiggs state stats \"Folders/Accounts\"\nhiggs state clear \"Folders/Accounts\"\n```\n\n`state query`\n\nexposes the per-message classification records `classify`\n\npersists (labels, confidence, rationale, mailing-list flag), so results stay queryable after the fact — no re-parsing saved NDJSON. It is purely local (no IMAP connection) and streams NDJSON rows whose `uid`\n\nfields pipe straight into `--uid -`\n\nconsumers.\n\n```\nhiggs state query INBOX --is-mailing-list false\nhiggs state query INBOX --label Personal --min-confidence 0.8\nhiggs state query --failed\n```\n\nFlags: `--is-mailing-list true|false`\n\n, `--applied true|false`\n\n, `--min-confidence F`\n\n, `--max-confidence F`\n\n, `--label X`\n\n(exact element match), `--failed`\n\n, `--limit N`\n\n.\n\nAudit a mailbox against an expected UID set without mutating anything. `--expect present`\n\n(default) requires every given UID to exist, `--expect absent`\n\nrequires none to, and `--expect exact`\n\nrequires the mailbox's UID set to equal the given set. Violations stream as NDJSON rows and make the command exit non-zero.\n\n```\nhiggs verify Archive --uid 1842,1843\nhiggs search INBOX --before 2025-07-01 | higgs verify INBOX --uid - --expect absent\n```\n\nEmit a machine-readable manifest of every subcommand. See [The agent contract](#the-agent-contract).\n\n```\nhiggs schema\nhiggs schema classify\n```\n\nAll configuration is read from environment variables. Defaults target a standard Proton Mail Bridge + Ollama setup on the same host.\n\nCredentials are stored in the OS keyring (macOS Keychain, Windows Credential Manager, Linux Secret Service via libsecret) so they never live in shell history or a `.env`\n\nfile. When the keyring is unreachable, an encrypted-file fallback (`~/.higgs/credentials.enc`\n\n, AES-256-GCM with Argon2id-derived keys) is available.\n\n```\n# Prompt interactively and store via the OS keyring (default).\nhiggs auth login\n \n# Pipe the password in from a secret manager:\npass show proton/bridge | higgs auth login --username alice@proton.me --password-stdin\n \n# Check where credentials live and which backends are available.\nhiggs auth status\n \n# Remove stored credentials from every backend.\nhiggs auth logout\n```\n\nIf `PM_IMAP_USERNAME`\n\nand/or `PM_IMAP_PASSWORD`\n\nare set in the environment they always win — useful for one-off overrides in CI or shells. To use the encrypted-file backend, export `PM_KEYSTORE_PASSPHRASE`\n\n(required to read or write the file) and optionally `PM_KEYSTORE_PATH`\n\nto relocate it.\n\n| Variable | Default | Description |\n|---|---|---|\n`PM_IMAP_HOST` |\n`127.0.0.1` |\nBridge host |\n`PM_IMAP_PORT` |\n`1143` |\nBridge IMAP port |\n`PM_IMAP_USERNAME` |\n(required) |\nBridge IMAP username |\n`PM_IMAP_PASSWORD` |\n(required) |\nBridge-generated password |\n`PM_IMAP_SECURITY` |\n`starttls` |\nOne of `starttls` , `tls` , `insecure` |\n`PM_IMAP_TLS_SKIP_VERIFY` |\nauto | Skip TLS verification (auto-enabled for loopback) |\n`PM_IMAP_APPLY_TIMEOUT` |\n`180` |\nPer-command timeout (seconds) for `--apply` |\n\n| Variable | Default | Description |\n|---|---|---|\n`PM_LLM_BACKEND` |\n`ollama` |\nChat backend: `ollama` or `openai` |\n\nEverything stays local either way: `openai`\n\nmeans the OpenAI-compatible Chat\nCompletions API served by self-hosted engines like llama.cpp `llama-server`\n\n—\nnot the OpenAI cloud.\n\n| Variable | Default | Description |\n|---|---|---|\n`PM_OLLAMA_BASE_URL` |\n`http://localhost:11434` |\nOllama API base URL |\n`PM_OLLAMA_MODEL` |\n`gemma4` |\nModel name passed to Ollama |\n\n| Variable | Default | Description |\n|---|---|---|\n`PM_OPENAI_BASE_URL` |\n(required) |\nServer base URL, e.g. `http://localhost:8080` (llama.cpp `llama-server` ) |\n`PM_OPENAI_MODEL` |\n(required) |\nModel name/alias the server expects |\n`PM_OPENAI_API_KEY` |\n(none) |\nOptional bearer token; sent only when set |\n\nReasoning models (e.g. Qwen3) are handled automatically: thinking is disabled\nfor structured calls (`classify`\n\n, `extract`\n\n, `digest`\n\n), enabled for `ask`\n\nand\n`summarize`\n\n, and any `<think>`\n\npreamble is stripped before parsing. Structured\noutput uses `response_format: json_schema`\n\n. Example:\n\n```\nPM_LLM_BACKEND=openai \\\nPM_OPENAI_BASE_URL=http://localhost:8080 \\\nPM_OPENAI_MODEL=qwen3.6-35b-a3b \\\nhiggs classify INBOX --limit 20 --dry-run\n```\n\n| Variable | Default | Description |\n|---|---|---|\n`PM_CLASSIFY_LIMIT` |\n`100` |\nMax messages per `classify` run |\n`PM_CLASSIFY_BATCH_SIZE` |\n`25` |\nIMAP fetch batch size |\n`PM_CLASSIFY_WORKERS` |\n`4` |\nParallel LLM requests (llama-server serializes unless started with `--parallel N` ) |\n\n| Variable | Default | Description |\n|---|---|---|\n`PM_STATE_DB` |\n`~/.higgs/state.db` |\nSQLite state DB path |\n\nThe classifier is constrained to 11 canonical labels. 612 aliases in `internal/labels/data/labels.toml`\n\nnormalize legacy or model-generated names back to this set.\n\n| Label | Covers |\n|---|---|\n| Orders | Purchase confirmations, shipping, returns |\n| Finance | Banks, cards, taxes, invoices |\n| Newsletters | Editorial digests, blog mailings |\n| Promotions | Marketing, discounts, sales |\n| Jobs | Recruiters, job boards, offers |\n| Social | Social network notifications, friend activity |\n| Services | SaaS account activity, product updates |\n| Health | Providers, pharmacy, insurance |\n| Travel | Flights, hotels, itineraries |\n| Security | 2FA, password resets, security alerts |\n| Signups | Account creation, email verification |\n\nSee [ internal/labels/data/labels.toml](https://github.com/higgscli/higgs/blob/main/internal/labels/data/labels.toml) for the full alias map.\n\nEvery common task is wrapped in the repo `Makefile`\n\n.\n\n| Target | Description |\n|---|---|\n`make build` |\nBuild the `./bin/higgs` binary |\n`make test` |\nRun `go test ./...` |\n`make test-race` |\nRun tests with the race detector |\n`make cover` |\nCoverage profile plus `go tool cover -func` summary |\n`make cover-html` |\nHTML coverage report at `coverage.html` |\n`make vet` |\nRun `go vet ./...` |\n`make vuln` |\nRun `govulncheck` (installs into `./bin` if missing) |\n`make check` |\n`vet` + `test-race` + `vuln` |\n`make clean` |\nRemove the built binary and coverage outputs |\n`make tidy` |\nRun `go mod tidy` |\n\nRun `make check`\n\nbefore opening a PR.\n\nBug reports and pull requests are welcome — see [CONTRIBUTING.md](https://github.com/higgscli/higgs/blob/main/CONTRIBUTING.md) for the workflow and code-review expectations.\n\nPlease report vulnerabilities privately via the process in [SECURITY.md](https://github.com/higgscli/higgs/blob/main/SECURITY.md). Do not open public issues for security reports.\n\nApache License 2.0 — see [LICENSE](https://github.com/higgscli/higgs/blob/main/LICENSE).\n\n[Proton Mail Bridge](https://github.com/ProtonMail/proton-bridge)— local IMAP gateway to Proton Mail[Ollama](https://github.com/ollama/ollama)— local LLM runtime[emersion/go-imap](https://github.com/emersion/go-imap)— IMAP client library[spf13/cobra](https://github.com/spf13/cobra)— CLI framework[BurntSushi/toml](https://github.com/BurntSushi/toml)— TOML decoder for the label taxonomy", "url": "https://wpnews.pro/news/show-hn-higgs-a-local-ai-cli-for-proton-mail-no-cloud-no-telemetry", "canonical_source": "https://github.com/higgscli/higgs", "published_at": "2026-07-28 15:15:07+00:00", "updated_at": "2026-07-28 15:22:13.968252+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "artificial-intelligence", "large-language-models"], "entities": ["Higgs", "Proton Mail", "Proton AG", "Ollama", "Gemma 4", "Proton Mail Bridge"], "alternates": {"html": "https://wpnews.pro/news/show-hn-higgs-a-local-ai-cli-for-proton-mail-no-cloud-no-telemetry", "markdown": "https://wpnews.pro/news/show-hn-higgs-a-local-ai-cli-for-proton-mail-no-cloud-no-telemetry.md", "text": "https://wpnews.pro/news/show-hn-higgs-a-local-ai-cli-for-proton-mail-no-cloud-no-telemetry.txt", "jsonld": "https://wpnews.pro/news/show-hn-higgs-a-local-ai-cli-for-proton-mail-no-cloud-no-telemetry.jsonld"}}