sessionmail
Mail for coding agents: message moves messages, pager alerts, memory
is the filesystem view - three commands over one maildir.
Quick start
claude plugin marketplace add mbailey/sessionmail
claude plugin install sessionmail
uv tool install sessionmail # the commands the plugin calls
pager listen # blocks until mail arrives, then says so
Both of the first two lines need the repo and the release to be public: see Status before you copy them.
The last line is the point: an agent that runs pager listen in the
background can be reached while it is idle, instead of finding out on its
next manual check.
Installation
Two installs, and you need both. The plugin brings the skills, the two
hooks and the MCP servers; the package brings message, pager and
memory, which is what all of those call.
claude plugin install sessionmail # skills, hooks, MCP entries
uv tool install sessionmail # the three commands on PATH
With only the plugin, the hooks exit quietly and the MCP servers do not start: you get skills describing commands the machine does not have. The hook says so once per session rather than leaving you to guess.
The notmuch hook is installed by hand. hooks/notmuch-post-new tags
mail at index time (+agent, +memory) and re-derives superseded from
each message's Supersedes: header, so a correction survives rebuilding
the index. It is NOT installed by the plugin or the package. On a host it
runs as both post-new and post-insert, and needs two indexed headers:
ln -s <repo>/hooks/notmuch-post-new ~/.mail/.notmuch/hooks/post-new
ln -s <repo>/hooks/notmuch-post-new ~/.mail/.notmuch/hooks/post-insert
notmuch config set index.header.Supersedes Supersedes
notmuch config set index.header.XSupersedes X-Supersedes
Headers added to an existing index need a one-off notmuch reindex '*'.
An optional command after each index run. The hook can run one command of yours after its tagging, for anything that should follow new mail (a dashboard redraw, say). It is off unless a notmuch config key names it:
notmuch config set sessionmail.post_new_command /abs/path/to/command # on
notmuch config set sessionmail.post_new_command # off
The value is an absolute path to an executable, run with no arguments and no
shell; a command that needs arguments gets a wrapper script. It never blocks
or fails indexing: the hook spawns a detached runner and exits 0 at once.
Runs are coalesced, one at a time: mail arriving during a run gives exactly
one more run. The command gets cwd /, its own process group, stdin
/dev/null, and only HOME, PATH=/usr/bin:/bin:/usr/sbin:/sbin,
NOTMUCH_CONFIG and SESSIONMAIL_IN_POST_NEW_COMMAND=1; it is killed after
300 s. A notmuch new it runs itself is tagged as usual and does not run it
again (a command that clears its environment must pass --no-hooks
instead). It must not send mail on every run: sent mail comes back as an
arrival. Its output and a start/end line per run go to
~/.sessionmail/post-new/log (rolled to log.1 past 1 MiB). With the key
unset, nothing is created and nothing is spawned.
Usage
message send NAME -s SUBJECT -f BODY.md # and list, tree, read, mark
pager listen # block until mail; page; exit
pager status # what would page you now
memory ls / | memory cat PATH # the box as a filesystem
Each command's own --help is the reference; <command> <verb> --help is
the page for a verb.
Where mail comes from. sessionmail reads and writes maildirs; it does
not require an MTA. On a host with postfix, mail arrives the ordinary way.
Without one, anything that can write a maildir is a delivery agent - your
own MTA, a script, or a chat bridge that drops each message into new/
(one door in, an outbox out). The pager watches the directory, whatever
filled it.
How pager listen works
pager listen --help says when it pages, what it prints and how to stop.
This is how, for whoever has to reason about it. Paths below are under
<home>/sessions/<session>/, written <session>/; <home> is
SESSIONMAIL_HOME, default ~/.sessionmail.
- One box. It watches this session's box (the session id is a box);
with no session id, the maildir
$MAILnames, which must holdnew/. It reads no subscriptions: what else reaches the box is postfix's doing, set bymessage subscription add BOX. - Birth. A session's box is created on its first listen when it is
missing, and its alias line is minted in the same step, bounded in time.
The banner says the doorbell's state: whether mail sent to the session
will reach the box. That first listen also subscribes the session to
each box in
SESSIONMAIL_SUBSCRIBE, once. A$MAILbox is never created: a human's mailbox is theirs to make. - The ride queue. Every arrival in the box's
new/is hardlinked into<session>/ride/new/. The ride-along (the PostToolBatch hook) scoops that queue on each tool call, so a busy session reads its mail without being paged. - When it pages. Idle (not busy for
SESSIONMAIL_IDLE_SECONDS, 1, by Claude Code's own status file, orsession get idle): at once. Busy: when the oldest link has sat in the queue longer thanSESSIONMAIL_PAGE_AGE_SECONDS(60) from its delivery time, because nobody scooped it. Then it prints the page and exits; the caller runs it again to keep listening.SESSIONMAIL_PAGE_LIST=on(or--list) adds one line per message: box, sender, subject, never a body. The flag wins:--no-listbeatsSESSIONMAIL_PAGE_LIST=on. - Marks. It claims nothing and moves nothing. It marks each message it
pages with
seen(reasonpager wake), so the same message does not page this session again inside the reminder window (SESSIONMAIL_REMIND_MINUTES, 60). Another session's marks never silence this one. - Stamps. A running listener writes its pid to
<session>/stamps/listening; that is whatpager statusand the hooks read as "listening", and whatpager stopsignals. - Keyed on the session. The queue, the stamps and the marks key on the session id, so a session with no seat gets the same page. With neither a session id nor a seat there is no queue; it still listens, and the banner says so.
As a Claude Code plugin monitor: pager listen --monitor (SM-678)
A hidden flag, for one caller: a plugin's monitors/monitors.json. Claude
Code runs a monitor's command for the whole session and makes each stdout
line a notification; it does not restart one that exits. So --monitor
never exits on a page:
- ONE line per page on stdout, flushed, and nothing else there: the count, the address each mail was sent to, what woke it - no sender, no subject, no re-arm. At most 480 characters (Claude Code cuts at 500).
- Lines at least
SESSIONMAIL_MONITOR_SPACING_SECONDSapart (2.5): Claude Code drops output past a bucket of 10 refilled 1 per 2 s, and a dropped page is a wake that never happens. - After a page it keeps listening. The
seenmarks, the carry stamp and the woke records are written as for any page, BEFORE the line, so the wake turn finds the carry stamp. A mail paged and never scooped pages again after the reminder window, as a re-arm would. - The "listening" confirm mail is sent once per monitor process, never per page.
- SIGTERM, SIGINT or SIGHUP end it with exit 0 and clear its
listeningstamp. When it cannot look it printspager: DEAF - ...and exits 2. - A monitor that finds another listener on its session waits, and takes over when that one ends.
- It refuses
--timeout,BOXand--quiet. pager off/pager stopends a monitor for good. Nothing restarts it; listen again with the barepager listen.
Not running under Claude Code?
pager learns whether you are busy from Claude Code's own per-session
sidecar (~/.claude/sessions/<pid>.json), and failing that from session get idle. A client with neither - a bare shell, your own script, a bot -
never looks idle, so waits written for an agent that is mid-tool-call apply
to it:
- a page waits the running page age (60 s) instead of ringing on arrival;
- and if your client names a seat (
CLAUDE_CODE_AGENT), a second message from the same address inside the coalescing window (300 s) does not ring again - a running agent would have scooped it on its next tool call.
Nothing is lost; it is late, or it waits for your next pager listen. Turn
both waits off in that client's environment:
export SESSIONMAIL_PAGE_AGE_SECONDS=0 # ring on arrival
export SESSIONMAIL_BURST_SECONDS=0 # ring for every message
pager listen says the same thing in its banner when nothing can tell it
whether you are busy.
Development
make install # uv tool install -e . - editable, so an edit is live
make # check, then test
make progress # how much of the CLI is built, counted by running it
openspec/ is the record of what this package is meant to do: read it
before you design or build. openspec/specs/ is ratified, changes/ is
proposed and ruled by nobody yet.
Status
sessionmail 0.0.1 on PyPI is a name reservation, not this code - a
1.3 KB placeholder with no dependencies, published 2026-09-08. Until 0.1.0
ships, install from source instead of the uv tool install sessionmail
line above:
uv tool install git+https://github.com/mbailey/sessionmail # from the repo
make install # from a clone
message, pager and memory all run; openspec/specs/ says what each
verb is meant to do and make progress counts how much of it is built.
The plugin: three skills, one per command
skills/message, skills/pager, skills/memory - each seed is one
sentence of what and when; each body points at that command's --help
and repeats nothing; each references/ is a flat slipbox of items under
40 lines / 400 tokens. This README is the overview of how the three go
together (Mike, voice 12:32 Wed 2026-09-09: three skills "to give some
clear separation", flexible, to be reviewed).
slipbox/ ships one note, pager.md; the other notes the specs cite by
name live in a private slipbox and are not part of this tree.
Metadata
Release files for sessionmail 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sessionmail-0.1.0.tar.gz | 379.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sessionmail-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 785.1 kB
Release files / sessionmail-0.1.0.tar.gz
| Download URL | sessionmail-0.1.0.tar.gz |
|---|---|
| Size | 379.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7244878140b274129f704d50706da74a0cd39cd82768c0c4c0200b7242c69086
|
|
BLAKE2b-256 checksum How to use checksums |
b24fe01eaa2447e9252d5b45994da0670b3d4fb4c438d64aa155569c5be39642
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.9 {"installer":{"name":"uv","version":"0.11.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / sessionmail-0.1.0-py3-none-any.whl
| Download URL | sessionmail-0.1.0-py3-none-any.whl |
|---|---|
| Size | 405.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3b64e534ef46aa610bf4d65bea8d4d00466e4a127099f92454c261cb13756797
|
|
BLAKE2b-256 checksum How to use checksums |
16c07a0f362a7fb21872bca741f599aef21944fa67ced55c46a7f39e251ecaea
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.9 {"installer":{"name":"uv","version":"0.11.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|