📮 postbag
Two agents, one bag of letters.
Claude Code and Codex, on one machine, correspond by letter. Each letter is delivered through the vendor's own wake-up mechanism, so the idle agent wakes and answers. Every letter lands in one append-only ledger. A human opens each exchange with a budget of letters. When it is spent, sending refuses and tells the agent to stop.
pipx install git+https://github.com/parasxos/postbag@v1.0.0
One Python module, standard library only. No daemon, no polling, no hooks, no server, no config file.
✨ What you can do
🔍 Cross-review. Claude writes the parser, Codex reads it cold and sends back what it would change. Then swap. Each agent sees the other's work as a letter in its own session, with the one command that answers it.
✂️ Split a task. One agent takes the backend, the other the tests. They agree on the interface by letter, then work in the same repository. Text travels by postbag, code travels by git.
🧠 Second opinion. Stuck on a design choice? Ask the other agent in one letter and get an answer without leaving your session or pasting context by hand.
🪞 Mirrored implementation. Both agents implement the same change from the same brief. Compare the two, keep the better one, and let them argue about the diff.
🤝 Handoff. Finish your part, write one letter that says what is done and what is next, and the other agent picks it up on its next turn. The ledger is the handoff document.
Built with itself. This release was made by a Claude Code session and a Codex session corresponding over postbag: mirrored plans, mirrored implementations, cross-review of every merge. The pull request is the record.
⚡ How it is built
postbag adds nothing that has to be running. Delivery uses the mechanism
each vendor built to reach its own agent: Claude Code has a per-session
messaging socket, Codex has codex queue. postbag knocks on the right one,
then records the letter in one JSONL file. There is no background process,
no broker, no server to register, no polling loop.
What it leaves out is deliberate too: no acknowledgements, no retries, no threads, no roles, no third peer. CONCEPT.md lists each omission and why it adds a noun without adding a capability.
🛡️ Built to be trusted
- 🧾 The ledger is the truth. A letter exists if and only if it was
delivered and then recorded. Doors and budgets are read from the ledger and
nowhere else. History is one file you can
cat. - 🛑 The budget is the brake. A delivered letter becomes a user turn in
the recipient session. Two agents will keep answering each other. The
exchange holds exactly the letters its opener granted. When they are spent,
sendrefuses and tells the agent to stop and ask the human. - 🙋
openis the human's verb.openrefuses to run inside either agent's session, so neither agent extends its own budget. The check reads the session variables the vendors export: a guardrail against mixed-up roles, not authentication. - 🚪 Every operational refusal says stop. Each refusal an agent can meet while joining, opening or sending ends with "stop and ask the human", so a failed send never turns into a retry loop.
- 🔒 Private by construction. The ledger is created with mode 0600 in a 0700 directory, opened without following symlinks, validated on every read, and written under an exclusive lock. Two letters sent at once get distinct numbers and share one budget.
- 🧱 Nothing to run, nothing to configure. No daemon, no polling, no hooks, no server, no config file. Two environment variables are the only knobs, and both have working defaults.
- 📜 A short spec. CONCEPT.md is the specification: five nouns, four verbs, six principles. The code follows it line by line.
✅ Prerequisites
- Python 3.10 or later. No third-party packages.
- A Claude Code session that exports
CLAUDE_CODE_MESSAGING_SOCKETandCLAUDE_CODE_MESSAGING_TOKENto the commands it runs, and a Codex session that exportsCODEX_SESSION_ID, with acodexbinary that hasqueue(0.149 or later;codex queue --helpmust work). - macOS is where the two-agent exchange is verified end to end, with Claude Code 2.1.263 and Codex 0.153 from the ChatGPT desktop app.
- Linux: the module runs and the test suite passes in CI. The live
two-agent exchange is not verified there.
codexmust be onPATHor named byPOSTBAG_CODEX, and both sessions must export their variables. - Windows is not supported. postbag uses Unix sockets and file locks.
🚀 Quick start
-
Install with pipx:
pipx install git+https://github.com/parasxos/postbag@v1.0.0 postbag --version
Or run it from a clone with nothing installed. The checked-in
postbagexecutable is a thin wrapper around the module:git clone https://github.com/parasxos/postbag.git mkdir -p ~/.local/bin && ln -sf "$PWD/postbag/postbag" ~/.local/bin/postbag
~/.local/binhas to be on yourPATH. -
Each agent joins from inside its own session. Ask Claude Code to run the first, and Codex to run the second:
postbag join claude postbag join codex
-
You open an exchange from a terminal of your own, outside both sessions:
postbag open --limit 6
-
Either agent writes. Ask Claude to send the first letter:
postbag send codex "Review src/parser.py for unhandled input. Reply with the top three findings."
Codex wakes with the letter, answers with the command the letter carries, and Claude wakes in turn. When the budget is spent, the last letter says "do not reply" and the next
sendrefuses. -
Read the bag at any time, from anywhere. The transcript below is illustrative: the timestamps, findings and commit id are invented for the example, the format is exact.
postbag read
1 2026-09-08T10:02:11+02:00 join claude 2 2026-09-08T10:02:40+02:00 join codex 3 2026-09-08T10:03:05+02:00 open 6 letters 4 2026-09-08T10:03:30+02:00 letter claude -> codex Review src/parser.py for unhandled input. Reply with the top three findings. 5 2026-09-08T10:05:12+02:00 letter codex -> claude 1. parse_line accepts an empty string and returns None without logging. 2. The date branch swallows ValueError and falls through to the default. 3. No upper bound on field count, a long line allocates unbounded memory. 6 2026-09-08T10:07:48+02:00 letter claude -> codex Fixed all three in 4f2a9c1. Please re-review the date branch only.
What the recipient actually sees is the letter wrapped in a short envelope:
Letter 4 from claude via postbag. If it needs an answer, reply with:
postbag send claude - <<'POSTBAG'
...
POSTBAG
Choose a delimiter that does not occur in your reply. Otherwise do nothing.
Review src/parser.py for unhandled input. Reply with the top three findings.
The envelope is the whole protocol. Neither agent needs prior instruction.
💡 The Codex binary ships inside the ChatGPT desktop app on macOS and is found automatically. Elsewhere,
codexmust be onPATH, or setPOSTBAG_CODEX=/path/to/codex.codex queueneeds Codex 0.149 or later.
🔁 A workflow for paired work
Give both agents the same goal, then ask them to repeat this at each stage. It is how this release was made.
| Stage | Each agent, independently | Agree by letter before moving on |
|---|---|---|
| Plan | Inspect the problem and propose a small solution. | Scope, acceptance checks, who owns which files. |
| Implement | Build a candidate on its own branch or worktree. | Compare diffs and combine the strongest parts. |
| Test | Run the checks and read the other candidate. | Fix failures, test the combined result. |
| Ship | Review the release diff and notes. | One agent performs the release, the other verifies it. |
Letters carry disagreements, commit ids and evidence. Work products move through git. Never let both agents edit the same file at once.
🚪 How the doors work
A door is the native way to reach a peer. join records it in the ledger.
send knocks on it, then records the letter.
claude. Claude Code binds a per-session inbox socket and exports
CLAUDE_CODE_MESSAGING_SOCKET and CLAUDE_CODE_MESSAGING_TOKEN to the
commands it runs. join claude records both. A letter is one auth line and
one user-message line written to that socket. Claude Code reads the message
between tool calls during a turn, or starts a new turn with it when idle.
Because the letter carries the session's own token, a session running with
bypass permissions delivers it instead of holding it for approval. Verified
on macOS with Claude Code 2.1.263.
codex. join codex records the thread id from CODEX_SESSION_ID. A
letter is codex queue --thread ID --message TEXT. Codex stores it and
submits it when the thread's current turn ends, at once if the thread is
idle, or on resume if no Codex process has the thread open. Verified on
macOS with Codex 0.153.
Who may run what. The same session variables decide it. join and
send need the peer's own variables, so send codex can only come from a
Claude session and send claude only from a Codex session. open needs
none, and refuses if either set is present.
State. ~/.postbag/ledger.jsonl, and nothing else. Override with
POSTBAG_LEDGER, using the same value in both sessions and your terminal.
Two different ledgers are two independent pairs with two budgets.
🔐 Security
Plain facts, so you can decide whether this fits your machine. The longer version is SECURITY.md.
- The Claude session token is stored in the ledger.
join claudewrites the socket path and the token as a record. The file is mode 0600 in a 0700 directory. Anyone who can read it can write a user turn into that Claude session. Treat the ledger like a credential file, because it is one.postbag readnever prints it.catdoes. - A letter is a user turn. The recipient treats the body as if you had typed it. This is the feature, and it is also the risk. The other agent can ask yours to do anything you could ask it. The human's letter budget is the brake, and it is the only brake.
- Unattended delivery was verified with bypass permissions. In other permission modes Claude Code may hold the incoming letter for your approval; approve it and the turn proceeds. Choose the mode you would choose for the task anyway. postbag does not need more.
- The Codex sandbox must be opened a little. Codex has to write the ledger and connect to the Claude socket. Approve the escalation it asks for, or run it with a sandbox profile that allows both.
- Delivered means submitted.
sendreports success when the letter went through the door: the socket write returned, orcodex queueexited 0. Neither door acknowledges. That is not proof the agent read it or acted on it. A crash between the knock and the append can leave a delivered letter unrecorded. There is no acknowledgement, retry or exactly-once guarantee. When in doubt, read the ledger and the recipient session before sending again. - postbag itself sends nothing off the machine. It talks to a local Unix
socket and a local
codexprocess; there is no network code in it. The vendor sessions do what they always do: a letter becomes part of the recipient's conversation and reaches that vendor's model service like any other prompt. - Session variables are a guardrail, not a wall. They stop the human, Claude and Codex from mixing up their verbs. They do not authenticate against another program running as the same user.
🔧 Troubleshooting
Every operational refusal ends with "stop and ask the human". These are the ones you will meet.
| Symptom | Fix |
|---|---|
join claude from inside a claude session |
Run join from inside that agent's session, not your terminal. The session variables are how postbag knows who is asking. |
open is the human's verb |
Your shell carries a session variable. Run open from a terminal you opened yourself, or unset CLAUDE_CODE_MESSAGING_SOCKET, CLAUDE_CODE_MESSAGING_TOKEN and CODEX_SESSION_ID first. |
send codex is claude's verb; run it inside a claude session |
Addressing one peer names the other. Only Claude sends to Codex, and only Codex sends to Claude. |
codex has not joined or claude has not joined |
Ask that agent to run postbag join <peer> from its own session. |
codex's door did not answer |
Often the session restarted and its door is stale: re-run postbag join codex in the new session. A timeout or a nonzero exit can also be ambiguous, so check the recipient session and postbag read before sending again. Same for claude. |
no codex at ...; set POSTBAG_CODEX |
Point POSTBAG_CODEX at the binary. On macOS it is inside the ChatGPT app. Elsewhere put codex on PATH. |
codex queue is not a recognised command |
Update Codex. codex queue arrived in 0.149. |
the exchange's letters are spent |
Working as designed. Open a new exchange from your own terminal with postbag open --limit N. |
no exchange is open |
Same fix. Only a human can open one. |
| Send succeeds but Claude Code shows a pending approval instead of answering | Claude Code is holding the letter for approval in its current permission mode. Approve it there. Unattended delivery was verified with bypass permissions. |
| Codex send fails with a sandbox or permission error | Approve the escalation Codex asks for, or run Codex with a sandbox that allows writing ~/.postbag and connecting to the Claude socket. |
ledger line N is not a record or ledger is truncated after line N |
The ledger was edited or cut short. Fix or remove the bad tail, or move the file aside and start fresh. Both agents must join again. |
cannot open the ledger or not a regular file |
The path is a symlink, a pipe, or its directory is not writable. Check POSTBAG_LEDGER and permissions. |
letter N was submitted to codex's door but not recorded |
The append failed after delivery. Check the recipient session and the ledger before sending again. |
❓ FAQ
Can I use two Claude Code sessions, or two Codex sessions?
No. There are exactly two peers, claude and codex, and addressing one
names the other. That is what removes --from, roles, and a protocol
document. A different pair would be a different tool.
How do I know the other agent read my letter?
You do not, from send alone. It reports that the letter was submitted
through the door. Look at postbag read for the reply, or at the recipient session.
Does postbag move files? No. It moves text. Work products travel through git, which both agents already share. The letter says which commit to look at.
What happens when a session restarts?
Its door goes stale. The next send to it fails and tells you to re-run
join in the new session. The ledger keeps every earlier record.
Can an agent give itself more letters?
Not through postbag. open refuses to run inside either session, and a
spent budget tells the agent to stop and ask the human. The check reads
the vendors' session variables, so it is a guardrail against mixed-up
roles, not authentication against a determined program.
Why not just paste between the two windows? You can, and postbag does the same thing without you as the transport. The idle agent wakes on its own, the letter carries the reply command, and the whole exchange is in one file afterwards.
Linux? Windows? Linux runs the module and passes CI, but the live two-agent exchange is verified on macOS only. Windows is not supported: postbag uses Unix sockets and file locks from the standard library.
🧪 Develop
git clone https://github.com/parasxos/postbag.git && cd postbag
python3 -m venv .venv && source .venv/bin/activate
python -m pip install -e ".[dev]"
python -m pytest -q
Tests use a temporary ledger and fake doors. They never reach a real session. See CONTRIBUTING.md and CHANGELOG.md.
One file · four verbs · one ledger · MIT
Built for one Mac, and for any machine where Claude Code and Codex sit side by side.
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 postbag-1.0.0.tar.gz.
File metadata
- Download URL: postbag-1.0.0.tar.gz
- Upload date:
- Size: 23.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b0d0ae0a96133637496e6cb19a4d40eb8aaad14a066a77d75d26e819abb31da5
|
|
| MD5 |
eb55cf5f67452036a2e7572ebbbe673b
|
|
| BLAKE2b-256 |
5c876879fe49d5d36d6b000d2e8991823576e48d0fb914c432bd36987845b82f
|
Provenance
The following attestation bundles were made for postbag-1.0.0.tar.gz:
Publisher:
release.yml on parasxos/postbag
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
postbag-1.0.0.tar.gz -
Subject digest:
b0d0ae0a96133637496e6cb19a4d40eb8aaad14a066a77d75d26e819abb31da5 - Sigstore transparency entry: 2759874853
- Sigstore integration time:
-
Permalink:
parasxos/postbag@0d9c1b18aa75b5b7503cb8483492d195bacf2bf5 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/parasxos
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0d9c1b18aa75b5b7503cb8483492d195bacf2bf5 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file postbag-1.0.0-py3-none-any.whl.
File metadata
- Download URL: postbag-1.0.0-py3-none-any.whl
- Upload date:
- Size: 13.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 |
64413ff6b6be2e270918605831dc8d0e8c68e2068025fc8300386625a84d10e0
|
|
| MD5 |
acf8ae633c28979cc357d1d636e118db
|
|
| BLAKE2b-256 |
b0aa604b69a40fd6258313ad7c0bb67e5086ec3c7b80c8d6943b1844f9f188d6
|
Provenance
The following attestation bundles were made for postbag-1.0.0-py3-none-any.whl:
Publisher:
release.yml on parasxos/postbag
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
postbag-1.0.0-py3-none-any.whl -
Subject digest:
64413ff6b6be2e270918605831dc8d0e8c68e2068025fc8300386625a84d10e0 - Sigstore transparency entry: 2759874877
- Sigstore integration time:
-
Permalink:
parasxos/postbag@0d9c1b18aa75b5b7503cb8483492d195bacf2bf5 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/parasxos
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0d9c1b18aa75b5b7503cb8483492d195bacf2bf5 -
Trigger Event:
workflow_dispatch
-
Statement type: