mailo
Tells you when your AI agent gets stuck, dies, or finishes.
Agents and long jobs run while nobody is watching. Things happen that you need to know about and no way to find out — and the one case that matters most, an agent frozen waiting for approval, cannot announce itself. mailo hooks into Claude Code, catches that moment, and pushes it to your phone.
- No daemon, no server. One executable and one config file
- Zero dependencies (standard library only), Python ≥ 3.11
- Two channels: Discord webhook (immediacy) and SMTP mail (a record)
Status
v0.1.0 — it works. Not published to PyPI yet. The hook input fields follow the reference documentation and have not been measured against a live session yet (docs/mailo/07-open-questions.md Q5).
uv tool install mailo-cli # or: pipx install mailo-cli
mailo init
The distribution is called mailo-cli because PyPI rejects mailo as too
similar to an existing project. Everything else — the command, the module, the
config paths — is mailo.
| Design | docs/mailo/ — event model, hook wiring, config, onboarding, channels |
| Reference | docs/channels/, docs/pypi/ — from the official docs |
| Open questions | docs/mailo/07-open-questions.md — delayed blocked, SessionEnd, hook measurement |
| Tests | python -m unittest discover -s tests (73 tests, no dependencies) |
CLI
mailo init create the config (~/.config/mailo/config.toml)
mailo config set smtp.username ... set one key, the way git config does
mailo config list / get / unset / edit
mailo config export move the config to another machine in one line
mailo test [-c CHANNEL] verify the config and send a test
mailo send -e EVENT -s SUBJECT [-b BODY]
mailo run [--name NAME] -- CMD... wrap a command — exit code, duration, log tail
mailo install-hooks / uninstall-hooks
There are three ways in. The first time, init takes everything you collected in
one paste, in any order. After that config set changes one key at a time. When
you add a machine, config export moves the whole thing in one line. Secrets are
never accepted as command-line arguments. (docs/mailo/05-onboarding.md)
Four events
| Event | When | Cost of missing it |
|---|---|---|
blocked |
Stopped, waiting for approval or input | The highest. Thirty seconds of attention would clear it; thirty minutes are lost instead |
failed |
Died (OOM, rate limit, non-zero exit) | High. You find out in the morning that a three-hour run died |
done |
Finished normally | Low. You just cannot queue the next thing |
custom |
Called directly from a script | — |
Routing is per event. blocked to Discord, done to mail, failed to both —
two independent paths, so one can die and the notification still arrives.
Design principles
- The only failure mode of a notifier is being noisy. That is why the
Stophook, which fires every turn, is not used, and why there is a cooldown and a daily cap. (docs/mailo/01-events.md §3.1) - The default leaks nothing. Bodies carry metadata only — no conversation
text, no full paths.
include_message_textdefaults tofalse. (docs/mailo/04-configuration.md §6.2) - The hook path never disturbs the session. Always exit 0, always
async: true. (docs/mailo/03-hooks.md §5.1) - No plaintext SMTP, and no way to disable TLS verification. Not even as an option. (docs/mailo/06-channels.md §7.1)
Known weakness
The immediacy of blocked rests on Discord alone. Mail cannot deliver
immediacy, so it is not an alternative. Discord is a chat app, which means push
delivery depends on the user's per-channel notification settings — something we
cannot enforce. The only mitigation is guidance: make a dedicated channel for
mailo, set it to "All Messages", and keep mobile push on. (docs/mailo/04-configuration.md §6.4)
Layout
src/mailo/
cli.py command parsing and implementations
schema.py the config key schema — one table shared by init, config set, and load
config.py reading and writing, mode 600 enforcement, machine-to-machine transfer
tomlwrite.py in-place TOML editing (stdlib has no writer, and comments must survive)
onboard.py discovery, order-free paste parsing, domain presets
hooks.py hook entry point (always exit 0) and settings.json merging
notify.py routing, cooldown, daily cap, retry classification
runner.py mailo run — command wrapping
channels/ discord (urllib) and smtp (smtplib)
Licence
MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mailo_cli-0.1.0.tar.gz.
File metadata
- Download URL: mailo_cli-0.1.0.tar.gz
- Upload date:
- Size: 75.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
640362514a93c0c8d080907d4285b2098d884b1ec1115220dbacf026ffe1d055
|
|
| MD5 |
edb24c869d03fb8001478be72d17573e
|
|
| BLAKE2b-256 |
ad4d46663619c0c971df67a9513bc503b3fc33f315ce47a927154e1542e31aac
|
Provenance
The following attestation bundles were made for mailo_cli-0.1.0.tar.gz:
Publisher:
release.yml on 1935138/mailo
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mailo_cli-0.1.0.tar.gz -
Subject digest:
640362514a93c0c8d080907d4285b2098d884b1ec1115220dbacf026ffe1d055 - Sigstore transparency entry: 2464263163
- Sigstore integration time:
-
Permalink:
1935138/mailo@c6d8a9c9075e8c0f30de29fa5347c76bc8e96263 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/1935138
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c6d8a9c9075e8c0f30de29fa5347c76bc8e96263 -
Trigger Event:
push
-
Statement type:
File details
Details for the file mailo_cli-0.1.0-py3-none-any.whl.
File metadata
- Download URL: mailo_cli-0.1.0-py3-none-any.whl
- Upload date:
- Size: 46.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1f9f4d58656e22b10c5b6feb27a5fb2a8f412f8c5d8685dc7af62d428ee98433
|
|
| MD5 |
41b4089c7010b2f60fda3bdb01feb7a2
|
|
| BLAKE2b-256 |
84b52b39dd7962b542ce1b484d4ddb379047f9b91553183b3a0b9971be57b019
|
Provenance
The following attestation bundles were made for mailo_cli-0.1.0-py3-none-any.whl:
Publisher:
release.yml on 1935138/mailo
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mailo_cli-0.1.0-py3-none-any.whl -
Subject digest:
1f9f4d58656e22b10c5b6feb27a5fb2a8f412f8c5d8685dc7af62d428ee98433 - Sigstore transparency entry: 2464263381
- Sigstore integration time:
-
Permalink:
1935138/mailo@c6d8a9c9075e8c0f30de29fa5347c76bc8e96263 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/1935138
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c6d8a9c9075e8c0f30de29fa5347c76bc8e96263 -
Trigger Event:
push
-
Statement type: