{"id":"ba4e186f-d1b5-4b59-8bf5-a08243e6a1db","slug":"postcard","kind":"skill","name":"Postcard","summary":"Asynchronous 1-to-1 postcard messaging between local AI agent sessions via a Git-backed ledger. Ships as a Python CLI and a Claude Code plugin.","owner":"Open-Agent-Tools","repoUrl":"https://github.com/Open-Agent-Tools/Postcard","license":"MIT","licenseTier":"permissive","trust":"community","stars":3,"match":null,"fewStars":true,"format":"skill","createdAt":"2026-04-22T15:35:40.000Z","addedAt":"2026-10-22T02:43:56.415Z","logoUrl":"https://avatars.githubusercontent.com/u/216968047?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["developer-tools","communication"],"tags":["agents","claude-code","claude-code-plugin","cli","ipc","messaging","multi-agent","python"],"runtime":"python","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-25T19:39:15.848Z","sample":false,"url":"https://conduits.dev/tools/postcard","language":"Python","transports":["stdio"],"install":{"pypi":"oat-postcard"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"[![tests](https://github.com/Open-Agent-Tools/Postcard/actions/workflows/tests.yml/badge.svg)](https://github.com/Open-Agent-Tools/Postcard/actions/workflows/tests.yml)\n[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![python: 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](pyproject.toml)\n\n<img width=\"800\" height=\"436\" alt=\"hero\" src=\"https://github.com/user-attachments/assets/c7b7cbdc-c912-4c9e-9462-74429a80ff1e\" />\n\n## Why postcard\n\nAI agent sessions don't talk to each other. If you're running sperate Claude Code sesisons\non three tiers of a web app:\n- one on the frontend\n- one on the backend\n- one on the Postgres schema \n\n... and your frontend agent needs the exact shape of a backend response, your options are: copy-paste between\nwindows, keep all three contexts in your own head, or stand up a message\nbroker.\n\nPostcard picks a different option: treat each session as an addressable\nnode on the local filesystem. No daemon, no network, no central service.\nA send is a git commit; an inbox is a directory. Every agent gets a\nrandom three-word address on startup and is discoverable via\n`oat-postcard directory`. Messages are one-way postcards (title ≤140\nchars, body ≤1400) with a fire-and-forget with an immutable audit trail.\n\nWhen mail arrives, a subagent called the Clerk triages it on the\nrecipient's next turn: routine items get filed into TODO, urgent ones\nget surfaced into the main agent's context. Nothing blocks, nothing\npolls, nothing phones home.\n\nIt's the *smallest coordination primitive* that works for agents sharing a\nfilesystem but not a process.\n\n## Install\n\nThere are two independent ways to use oat-postcard. They don't\nconflict — install one, the other, or both:\n\n### Use inside Claude Code \n\nInstall the **plugin**. Wires up slash commands, hooks, and the `postcard-reader` subagent. The bundled `bin/oat-postcard` shim is on PATH **only for Claude Code's own sessions** — your terminal won't see it. \n\n```\n/plugin marketplace add Open-Agent-Tools/Postcard\n```\nthen\n```\n/plugin install postcard@oat-postcard\n```\n\n### Use from your teminal shell\n\nInstall the **standalone CLI** with `uv`. Puts `oat-postcard` on your user PATH so it works from any terminal. Has no effect on Claude Code. \n```\nuv tool install git+https://github.com/Open-Agent-Tools/Postcard.git\n```\n\nInstall both if you want slash commands **and** a terminal CLI — they\nshare the same on-disk state (`~/.oat-postcard/`), so postcards sent\nfrom one path are visible to the other.\n\nRequires Python 3.9+ and git available on the machine (macOS's default\n`/usr/bin/python3` works).\n\n### Updating the plugin\n\n`/plugin install` reads a locally cached copy of the marketplace — it\ndoesn't re-fetch upstream. To pick up a new version:\n\n```\n/plugin marketplace update oat-postcard\n/plugin uninstall postcard@oat-postcard\n/plugin install postcard@oat-postcard\n```\n\nOr enable auto-update for this marketplace in the `/plugin` UI\n(Marketplaces tab → toggle on) — third-party marketplaces have it\noff by default. Once on, updates happen at Claude Code startup.\nForce it globally with `FORCE_AUTOUPDATE_PLUGINS=1` or suppress it\nwith `DISABLE_AUTOUPDATER=1`.\n\n### Updating the standalone CLI\n\n```sh\nuv tool upgrade oat-postcard\n```\n\n### Local dev\n\n```sh\ngit clone https://github.com/Open-Agent-Tools/Postcard.git\ncd Postcard\nuv sync\nuv run pytest\nuv run oat-postcard --help\n```\n\nTo test the plugin side locally from another project directory:\n\n```sh\nclaude --plugin-dir /path/to/Postcard\n```\n\n## Example\n\nThree Claude Code sessions running on the same machine — one on each\ntier of a web app: the React frontend, the FastAPI backend, and the\nPostgres schema. Each gets an auto-generated 3-word address at startup.\n\n**Session A** (frontend) is wiring up a user profile page and needs the\nshape of the backend response:\n\n```\n/whoami\n→ swift-amber-compass\n\n/directory\n→ * swift-amber-compass  pid=54321  /Users/you/app/web\n→   bright-jade-engine    pid=54789  /Users/you/app/api\n→   quiet-copper-reef     pid=55012  /Users/you/app/db\n\n/send bright-jade-engine \"GET /api/users/:id shape\" \"Building the\nprofile page. What does GET /api/users/:id return? Specifically, is\navatar_url nullable, and are timestamps ISO strings or epoch seconds?\"\n→ sent 8c4a1f03 to bright-jade-engine\n```\n\n**Session B** (backend) finishes its current turn. The Stop hook sweeps\nthe postcard into pending staging. On Session B's next prompt, the\nUserPromptSubmit hook injects an \"N pending postcards\" notice, and the\nmain agent delegates to the `postcard-reader` subagent. The subagent\njudges the postcard urgent (direct blocking question) and returns:\n\n> Filed 0 to TODO. Surfaced 1 urgent.\n> - [swift-amber-compass] GET /api/users/:id shape — confirm\n>   avatar_url nullability and timestamp format.\n\nSession B replies with the answer, then pings the database agent with a\nfollow-up question that surfaced while writing the reply:\n\n```\n/send swift-amber-compass \"re: GET /api/users/:id shape\" \"avatar_url is\nnullable (String | None); timestamps are ISO 8601 UTC strings. See\nschemas.UserRead.\"\n\n/send quiet-copper-reef \"users.avatar_url length\" \"Frontend is adding\navatar rendering. Migration 0014 used VARCHAR without a max — is there\nan intended cap, or should we tighten it to VARCHAR(2048)?\"\n```\n\nMeanwhile, the ledger accumulates a committed audit trail across all\nthree tiers — `oat-postcard log` replays the conversation, `oat-postcard\nreceipts` shows when each message was read and whether it was filed or\nsurfaced.\n\n## CLI\n\nCommon commands:\n\n```\noat-postcard send <address> \"<title>\" \"<body>\"   # send a postcard\noat-postcard reply <parent-id> \"<body>\"          # reply in-thread\noat-postcard inbox [--watch]                     # list mail to this session\noat-postcard log [--since 1h] [--watch]          # full send history\noat-postcard directory                           # active peers\noat-postcard whoami                              # this session's address\n```\n\nSee [docs/CLI_REFERENCE.md](docs/CLI_REFERENCE.md) for the full\nreference, including hook/subagent commands.\n\n## Onboarding\n\nNew to a project? Run `/postcard:onboard` inside Claude Code. It\ngives a short tour, appends a coordination hint to your project's\n`CLAUDE.md` (or `AGENTS.md`) so future agents in that project will\nreach for postcards proactively, and shows your current address and\nwho else is active.\n\nFor non-interactive use, `oat-postcard init` does the persistence step\nalone and is idempotent (re-running is a no-op; `--force` rewrites).\n\n## Housekeeping\n\n`oat-postcard cleanup` prunes:\n\n- `directory/<addr>.json` entries whose PID is no longer alive\n- `sessions/<id>.addr` sidecars not backed by a live directory entry\n- `pending/<session>/` dirs whose session is no longer live\n- `inbox/<address>/` dirs whose address is no longer live\n- `dropbox/` temp files older than 5 minutes (stuck writes)\n\nCleanup runs automatically at the start of every session (via\n`session-init`), so a crashed or abandoned session is reaped the next\ntime any Claude Code session starts. It can also be run manually.\n\n## Session identity\n\nThe CLI keys sessions by `$CLAUDE_SESSION_ID` (set by Claude Code) with\nfall-back to `tty:$(tty)` for standalone terminal use. The SessionStart\nhook generates a 3-word address, writes\n`~/.oat-postcard/sessions/<session-id>.addr`, and registers the session in\n`~/.oat-postcard/directory/<address>.json`. SessionEnd unregisters. Stale\nentries (dead PIDs) are pruned on every `directory` read.\n\n## Incoming mail flow (Clerk)\n\nThe Clerk is a subagent, not a hook — the hooks only raise the flag.\n\n1. Sender calls `oat-postcard send <addr> ...` → ledger commit + hardlink\n   into `~/.oat-postcard/inbox/<recipient>/`.\n2. After every turn on the recipient's session, the **Stop hook** runs\n   `clerk-sweep`, moving inbox files into\n   `~/.oat-postcard/pending/<session>/` (per-session staging).\n3. On the recipient's next user turn, the **UserPromptSubmit hook** checks\n   `clerk-pending --count`. If > 0, it emits `additionalContext` telling\n   the main agent: \"invoke the postcard-reader subagent\".\n4. The main agent uses the Task tool (`subagent_type: postcard-reader`).\n   The subagent:\n   - Runs `clerk-pending --json` to list staged mail.\n   - For each postcard, chooses **file to TODO** (`clerk-file <id>`) for\n     routine mail, or **surface** (`clerk-surface <id>` + include in\n     summary) for urgent mail.\n5. The subagent returns a summary; the main agent acts on any surfaced\n   urgent items.\n\n## Read receipts\n\nEvery `clerk-file` and `clerk-surface` emits a receipt file at\n`~/.oat-postcard/postcards/receipts/<postcard-id>.json` and commits it to\nthe ledger. Receipt fields: `postcard_id`, `action` (`file` | `surface`),\n`read_at`, `reader_address`, `reader_session_id`.\n\nThe ledger thus records both sends and reads — `oat-postcard log` shows\npostcards, `oat-postcard receipts` shows reads, and the underlying git\nhistory has everything interleaved with commit messages prefixed\n`receipt:` for receipts.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n"}