Agent Letter Bridge
Message the CLI agents on your machine from your phone — and every message becomes part of their memory.
Agent Letter Bridge connects a chat app to AI agents on your own machine. An incoming message becomes a durable file on disk before it is acknowledged and before any agent is notified. The file is the source of truth; the ring is how it gets read. Letter first, then ring — without a bell, mail lands in a dead drop and nobody is told.
Integrated mode requires a doorbell helper with a ring <to> <type> <id>
command that prints one doorbell-outcome v=1 line. Letterbox 0.4.0
defines that format and its fixtures, but no public Letterbox edition ships
a ring dispatcher yet — integrated deployment needs a helper that provides
it. A helper that still prints prose will
make every ring failing / unparseable. alb --doctor reports
helper does not emit doorbell-outcome v=1 instead of waiting for a silent
ring-health failure after cutover.
Most tools in this space deliver an external message as the agent's input — typed into its terminal, or handed to its session as a prompt. This one delivers it as a durable, deduplicated, enveloped letter, written to disk before the platform is even told the message was received. That buys you three things: a context window that stays clean, because the doorbell is one contentless line and the body enters only when the agent chooses to read it; messages that survive compaction at full fidelity, because a letter lives outside the session; and a growing archive of records — origin, receipt time, content, every origin allowlist-verified — that any memory system can take as ground truth. Delivery that behaves like memory, not like typing.
More memory than message.
Why this exists
CLI agents don't have phones. The most capable coding agents live in terminal panes on a machine, and the moment you stand up from the desk they are unreachable — while everything else in your life answers from the phone in your pocket. This bridge gives the agents on your machine a messaging app: text them from anywhere, and they can answer.
The added edge is what arrives. A letter is more than the message. It carries its origin — the chat it came from, verified against the allowlist — when it was received, its platform addressing and an exactly-once guarantee — a record, in plain Markdown, that everything downstream can trust: the agent reading it now, the memory system ingesting it later, the search that asks what was said last month. Replies are addressed to the letter, which knows its own way home. Since v0.2 the archive runs both ways: an outbound reply is written as its own letter before the platform is touched, and its delivery events are recorded as immutable files beside it.
Sending works the same way round. Your agent answers a letter it holds, and the destination comes from that letter rather than from anything the agent chose — so a reply goes where the message came from, checked against the allowlist again at the moment of sending. A letter can be answered only once: the claim is an exclusive file create, so a retried or duplicated invocation cannot put a second message on the wire. That bounds sending, not delivery — where the outcome is uncertain the letter is dead-lettered with the uncertainty recorded, rather than retried into a possible duplicate.
Reply by default; initiating only by explicit, revocable grant. Every reply is tied to a stored incoming letter and goes back to that letter's chat, checked against the allowlist again at the moment of sending. Starting a new conversation is a separate, narrower path: the bridge can open one only to a destination the operator has authorised with a grant it can revoke at any time, and even then only within a small daily and hourly budget with a cap on messages in flight. Without a grant the bridge still cannot originate contact with anyone; with one, it cannot originate contact anywhere the operator has not named, nor more often than the budget allows.
If the agent already has a messenger of its own (a gateway bot, a chat plugin — for example Hermes Agent's gateway), the bridge runs beside it on its own bot; the two never share a token at the same time — see the install guide.
What that does not claim: an agent still chooses which stored letter to answer and what to write in it, so this is not immunity to being persuaded into sending something. It is a guarantee about where a message can go, not about what it says — and it covers this send path, not everything else your agent can reach. This describes the current release.
That is what makes this a front door rather than a pipe. Whatever you build behind it — today's agent, tomorrow's memory system — inherits records instead of scrollback. The input side of your setup is settled once.
The context economics follow from the same split. The body never enters the agent's context window until the agent chooses to read it — the doorbell is one contentless line — and it can be re-read at full fidelity after a compaction, because a letter is storage outside the window. What a session forgets, the inbox still knows.
Memory systems get the same service. Underneath every one of them — vault-based, RAG, graph, whatever comes next — sits the same need: durable, addressable, re-readable records of what was actually said. A letter already is one. Point any memory system at the inbox and it has its ground truth: no scraper, no export, no plugin. We are deliberately only the storage half — the records are ours, the librarian can be anyone's.
Durability is the supporting property, not the pitch: your phone keeps your copy, and the letter is your agent's — still on disk after a crash, a restart or a compaction, which is how a resurrected agent gets its context back.
Design
Four roles with deliberately unequal privilege. The separation is the
product — and it is enforced at module boundaries and proved by
tests (the poller code path cannot ring; the watchdog reads only mirrored
state), not by OS process isolation: the resident bridge holds the token and
the notifier in one process, and docs/operations.md states that limit
plainly rather than letting this table imply more.
| Role | Trust | May do | May never do |
|---|---|---|---|
| Poller | untrusted | fetch, write letter, then ack | ring, notify, or touch a terminal |
| Notifier | in-session | ring after a letter exists | carry message content in the ring |
| Send helper | bounded | reply to an origin; initiate only on a live grant, within budget | send with no grant; exceed the budget; send on allowlist miss; auto-retry |
| Watchdog | independent | read mirrored health, report | restart anything; depend on what it monitors |
Order is the invariant. Letter to disk → then platform ack. A crash between fetch and write causes redelivery, never loss.
Read docs/invariants.md before trusting this with a token.
The invariants are the product; the code is how they are kept.
Install
Start here: INSTALL.md — one numbered path from nothing to a
working bridge, about 15 minutes, with the checkpoints that catch the failures
that otherwise look like something else.
Installing it with a CLI agent rather than by hand? Give the agent
docs/agent-install.md — the same install written as a
brief, with the boundaries an agent needs and a human infers.
The short version, once you know the shape:
pipx install agent-letter-bridge # or: uv tool install agent-letter-bridge
alb --init --root ~/.alb # creates the files, asks for the rest
For pip, first create and activate a dedicated virtual environment, then run
python -m pip install agent-letter-bridge. For development from a checkout,
use pipx install . instead. Installation does not configure or start a relay.
--init may offer to start the bridge for you, and what comes next depends
on your answer. Only one process may hold a root at a time, so these are two
routes, not two steps:
# (a) you DECLINED the offer, or it was not made — run one cycle by hand:
alb --config ~/.alb/bridge.env --root ~/.alb --once
# (b) init STARTED it — do not run --once, it will exit 4 against a held
# root, which is the lock working. Message the bot and watch instead:
alb --status --root ~/.alb
--init creates the state directory, a mode-600 config and a deny-all
allowlist, then asks you for the things no program can derive: your bot token,
whether your agent already has a mailbox, and your chat id — which it will
either read for you, or print the command for you to run, your choice. It never
invents an allowlist entry, never overwrites a file, and never touches the
network unless you ask it to.
It will deliver nothing until a chat id is in that allowlist. That is deliberate and it is the step people skip.
If nothing arrives, run alb --doctor --root ~/.alb. A fail-closed allowlist is
indistinguishable from a dead bot, so the doctor tells you which you have. Each
cycle also reports itself — fetched 2 · published 1 · denied 1 (allowlist) —
so a working deny is visible to you without being visible to the sender.
It refuses to start on a missing, world-readable or incomplete config. That is deliberate: a bridge that starts wrong is harder to diagnose at 3am than one that will not start at all.
The ring is what makes the bridge live, and it needs a multiplexer — cmux
or tmux, selected with ALB_NOTIFIER. Mail without a bell is a dead drop:
delivered, safe, and unread until someone thinks to look. What the design
guarantees is that the ring may fail without costing a letter — never that
you would want to run without one. If you must (no multiplexer, an agent that
sweeps on its own schedule), the bridge still delivers and alb --status says
the ring is disabled rather than leaving you guessing. Adapters are small files behind a written contract
(docs/adapter-contract.md); a Herdr adapter is
planned, and will ship when there is a live workspace to prove the doorbell
against — untested transports do not ship here.
Reference and failure modes: docs/operations.md.
Waking an agent that already handles other mail: docs/agent-setup.md.
Scope, plainly
It carries your own messages to your own agents. Your token stays on your machine and is used only against your chosen platform's API, from your own machine — there is no service in between. Messages travel over that platform, as they must.
It hands the agent a letter, not the message as a command. The platform message body is written to disk and never copied into the composer — the terminal input an agent's session would run — so it cannot arrive as text that is already running. The allowlist decides which chats may create letters; it does not make their contents trusted. Once an agent chooses to read a letter, its body is still untrusted text, and this tool does not make it safe.
The comparisons are checked, not asserted. Every claim of difference from
neighbouring tools was verified against their current code and docs before it
was written down: docs/COMPARE.md.
It pairs with Agent Letterbox. Letterbox is where mail rests between agents on one machine. Letter Bridge is how mail crosses in from outside.
Status
v0.4.1. Across releases, inbound delivery, ringing and bounded replies
have been exercised live against real bots on macOS and Linux, cmux and tmux. v0.2 adds
durable outbound letters, correspondent
identity and threading, and read-only retrieval (--list, --show, --search,
--thread, --export); those are covered by the suite and reviewed, but have
not yet had the same live mileage as the inbound path. Automated coverage
still uses fakes — the suite proves the invariants, the live runs prove the
transports, and those are different claims. Available on
PyPI, but not formally audited;
see docs/threat-model.md for what is and is not claimed.
New inbound letter IDs use UTC. New inbound letters and outbound reply records
also carry an explicit UTC sent: field: local publication/composition time,
not proof of platform delivery or the original message's authoring time.
Delivery events remain separate records beside the reply. Reply IDs remain
source-derived so retries keep the same claim identity; their inherited ID
stamp need not equal the reply's composition time. Existing letters are neither
rewritten nor assigned a timezone they did not declare.
Upgrading from v0.2.1 (legacy installations). Three things
change that you would notice. A rate limit or a gateway error is now waited out
and retried rather than ending the bridge, so the relay survives the platform
asking it to slow down. alb --check gives an agent a verdict to act on when it
wakes: nothing to do, restart it and here is how, or something a restart will not
fix. And alb --stop asks the process holding the lock to stand down, rather
than signalling a pid that may by then belong to something else. Upgrading is
pipx upgrade agent-letter-bridge for a PyPI installation, or
pipx install --force . from a reviewed checkout. Plain pipx install . sees
the existing install, prints Not modifying existing installation and stops.
Stop the running bridge by whatever route the version you are ON supports:
alb --stop is new here, and 0.2.1 will not honour it. Configs, allowlist and
state carry forward untouched; check with alb --version before restarting. The
full list is in CHANGELOG.md.
If multiple environments or editable checkouts exist, verify command -v alb
and alb --version, and check that the resident launch command uses the intended
environment's executable. Upgrading one environment does not update another
or reload a running bridge.
What you need
- Python 3.11+. Standard library only, zero third-party runtime dependencies.
- A terminal multiplexer — cmux or tmux — selected with
ALB_NOTIFIER. The ring types a line into a pane, so a pane must exist to type into. Expect to want this: without it mail lands durably and nobody is told. The bridge runs regardless, andalb --statusreports the ring asdisabledrather than leaving you guessing. - A CLI agent sitting in that pane.
- A bot on your chat platform.
You do not need Agent Letterbox. They are separate products that share a metaphor and nothing else — no shared code, no shared files, no shared config. Letterbox carries mail between agents on one machine; this carries mail in from outside. Install either, both, or neither.
Requirements
Python 3.11+, standard library only. Zero third-party runtime dependencies — by design, and enforced in CI. macOS (launchd) and Linux (systemd user units). Windows is a declared gap, not a promise.
Licence
MIT — see LICENSE.
Release files for agent-letter-bridge 0.4.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| agent_letter_bridge-0.4.1.tar.gz | 300.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| agent_letter_bridge-0.4.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 425.6 kB
Release files / agent_letter_bridge-0.4.1.tar.gz
| Download URL | agent_letter_bridge-0.4.1.tar.gz |
|---|---|
| Size | 300.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d4f78f6d82c85f6a0bddfd3f2e11f76b9da4f1f020e6e700c697ca68cb2ee81d
|
|
BLAKE2b-256 checksum How to use checksums |
a299a9ad705eac225d5d1f29902821529d9ca4d98c02dc9e7db02e1193a71033
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency logRelease files / agent_letter_bridge-0.4.1-py3-none-any.whl
| Download URL | agent_letter_bridge-0.4.1-py3-none-any.whl |
|---|---|
| Size | 124.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
337f540e50c87473e00d9fe6896090a21802820ef3a27d01acdf8a9a54c3db8a
|
|
BLAKE2b-256 checksum How to use checksums |
bc37353e14b99af703f3ca2995196a553be8a438acdfedeea8d8e87bc1ca079e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency log