The most obvious way to send new guidance to a running AI coding agent is to inject another message into its active conversation.
I deliberately chose not to do that.
When an agent is already implementing, testing, or reasoning through a task, active steering can be useful—but it also changes the conversational state immediately. A late instruction can interrupt a coherent plan, arrive in the middle of a tool operation, or blur the line between the task the agent accepted and the new context being introduced.
I wanted a different interaction model:
Leave guidance now. Let the agent read it at the next safe checkpoint.
That idea became CodexPigeon, an open-source desktop app and CLI that gives each Codex worktree a small, repository-local mailbox.
The human writes to an inbox. The agent reads it when control returns to a reasonable checkpoint, acknowledges the message, and can write a reply or receipt. The app observes Codex state, but it never starts, interrupts, steers, or injects into an active turn.
This article explains why I chose a mailbox protocol, how the ownership boundaries work, and what I learned while designing asynchronous human-agent communication.
The problem: useful context often arrives late
Long-running software tasks rarely remain perfectly static.
While an agent is working, a human may notice that:
- one module must not be changed;
- a test environment has become unavailable;
- a release gate should be checked before publishing;
- a requirement was misunderstood;
- another developer changed a related file;
- a destructive operation needs explicit approval;
- the final answer should include a particular validation result.
The information is relevant, but the timing is awkward.
Interrupting immediately is not always necessary. Waiting until the current turn ends may be too late.
The product question became:
Can a human update the working context without treating the active chat stream as the only communication channel?
Why I rejected active-turn injection
Codex exposes methods that could conceptually be used to start, steer, interrupt, or inject items into conversations.
CodexPigeon explicitly refuses to use them.
The read-only App Server allowlist contains only:
initialize
thread/list
thread/read
thread/loaded/list
hooks/list
Methods such as these are rejected by design:
turn/steer
turn/start
turn/interrupt
thread/inject_items
This is not because active steering is universally wrong. It is because CodexPigeon has a narrower contract.
The product is a mailbox companion, not a second chat client.
A narrow contract makes the behavior easier to reason about:
- the app cannot silently alter an active conversation;
- the user can inspect the exact message written to disk;
- the agent decides when it is safe to consume the message;
- guidance remains attached to the repository and worktree;
- communication state is visible outside the chat UI;
- the security review can focus on a small set of file and read-only process boundaries.
The core rule: one thread, one worktree, one mailbox
Each active worktree receives a .codex-mailbox directory:
.codex-mailbox/
INBOX.md
OUTBOX.md
RECEIPTS.md
STATE.json
HOOK_STATE.json
README.md
.gitignore
The operating rule is:
1 thread = 1 worktree = 1 .codex-mailbox/
This prevents guidance for one task from leaking into another task that happens to share the same repository.
Git worktrees are a useful boundary because they already represent separate working copies, branches, and task contexts. The mailbox follows that boundary instead of inventing a global message bus with complicated routing rules.
File ownership is the security model
The protocol separates files by writer:
| File | Writer | Purpose |
|---|---|---|
INBOX.md |
CodexPigeon app or CLI | Human guidance for the agent |
OUTBOX.md |
Codex agent | Human-readable replies |
RECEIPTS.md |
Codex agent | Acknowledgement and action status |
STATE.json |
CodexPigeon app or CLI | Technical state and optional repeats |
HOOK_STATE.json |
Hook runtime | Reminder throttling and runtime state |
The app must never write to OUTBOX.md or RECEIPTS.md.
The agent must never write to INBOX.md.
This avoids two processes editing the same logical record and makes provenance visible. When a line appears in the inbox, it came from the human-side tool. When a receipt appears, it came from the agent-side workflow.
The boundary is simple enough to explain in one table, which is usually a good sign for a protocol.
A message is plain Markdown
I chose Markdown rather than a hidden database protocol because the mailbox should remain inspectable with normal tools.
An inbox message looks like this:
## msg_20260520T153000_01HY4YF9F0Q2A3N4B5C6D7E8F9
from: human
created_at: 2026-05-20T15:30:00.000Z
priority: normal
status: unread
scope: current_task
Message:
Do not touch the auth module during the billing refactor.
If auth changes are needed, ask in OUTBOX.md first.
The ID combines a readable timestamp with a ULID-style suffix. That gives messages a useful chronological hint while avoiding collisions during rapid writes.
Messages are append-only. The app does not later rewrite status: unread inside the original inbox block.
Instead, current status is derived from agent receipts.
Receipts are more useful than mutating the inbox
A receipt can say whether the agent accepted, rejected, deferred, applied, or was blocked from applying a message.
Example:
## receipt_20260520T153245_01HY4YJ4Q8E2GHK2Z6MBWR6A1R
message_id: msg_20260520T153000_01HY4YF9F0Q2A3N4B5C6D7E8F9
seen_at: 2026-05-20T15:32:45.000Z
decision: accepted
action_status: applied
summary: Updated the current plan to avoid auth module changes.
The UI derives message state from these receipts:
- no receipt → unseen;
- accepted → accepted;
- rejected → rejected;
- deferred → deferred;
- needs confirmation → needs confirmation;
- applied → applied;
- blocked → blocked.
This has two advantages.
First, the original human message remains immutable.
Second, acknowledgement and execution are separate concepts. An agent may understand an instruction but be unable to apply it, or it may defer the instruction until a later stage.
A single “read” flag would lose that distinction.
OUTBOX is intentionally separate
The agent can also write a human-readable reply:
## reply_20260520T153250_01HY4YJ9W9A2F33H4Y9N7DR5F0
from: agent
to: msg_20260520T153000_01HY4YF9F0Q2A3N4B5C6D7E8F9
created_at: 2026-05-20T15:32:50.000Z
Understood. I will avoid auth and ask here first if that changes.
Receipts are structured status. OUTBOX is conversation-like feedback.
Keeping them separate prevents the UI from having to infer operational state from free-form prose.
Safe checkpoints instead of immediate delivery
The mailbox only works if the agent checks it at useful moments.
CodexPigeon installs a managed section into AGENTS.md that asks the agent to inspect the inbox:
- before a major architectural decision;
- after a meaningful implementation step;
- after tests or a long shell command;
- before the final response.
It also installs project-local hooks:
SessionStart
PostToolUse
Stop
The hooks can create missing mailbox files, detect unread messages, and remind the agent to perform a final check.
But hooks are not treated as a hard security boundary. They are reminder infrastructure.
The primary contract remains:
- repository instructions tell the agent how to behave;
- ownership rules say which side writes each file;
- App Server methods remain read-only;
- the agent consumes mailbox content as human guidance, not executable shell input.
Preserving existing repository instructions
Installing a tool into a real repository should not overwrite the project's existing operating rules.
CodexPigeon updates only a managed block inside AGENTS.md:
<!-- CODEXPIGEON_MAILBOX_START -->
...
<!-- CODEXPIGEON_MAILBOX_END -->
Everything outside that block is preserved.
The same principle applies to .codex/hooks.json. Existing non-CodexPigeon hook groups remain in place, while only the managed CodexPigeon groups are replaced or updated.
This was an important implementation detail. A tool that helps coordinate an agent should not destroy the instructions that define how that agent works.
The desktop app has two integration planes
The architecture separates two different responsibilities.
1. Mailbox integration
The app and CLI can:
- install the mailbox into a workspace;
- validate messages;
- append inbox entries;
- parse inbox, outbox, and receipt Markdown;
- watch files for changes;
- derive message status;
- manage optional repeated sends;
- inspect whether hooks and instructions are installed correctly.
2. Read-only Codex integration
The App Server client can:
- discover threads;
- read thread metadata;
- observe loaded threads;
- inspect hooks;
- enrich the UI with activity status.
These planes meet in the interface, but they do not share mutation powers.
The desktop app can help a user choose the correct worktree and observe the related Codex task. It still writes guidance only through the mailbox.
Package boundaries
The project is a TypeScript monorepo with several focused packages.
packages/mailbox-core
This is the protocol implementation:
- Markdown parsing and serialization;
- message ID generation;
- path normalization;
- append locking;
- file watching;
- installer behavior;
- message validation;
- repeated-message state.
packages/codex-app-server
This is the read-only JSON-RPC client. It enforces the method allowlist at runtime rather than relying only on developer discipline.
packages/cli
The CLI exposes commands such as:
send
watch
install
snapshot
doctor
automation list
automation stop
automation run-due
packages/hooks
This contains the managed AGENTS.md block, hook configuration, Python hook runtime, mailbox README, and ignore templates.
apps/desktop
The Electron application owns native dialogs, filesystem watching, IPC, and the React interface.
The renderer does not receive unrestricted Node access. A narrow preload bridge exposes the operations the UI needs.
Why parsing Markdown still required discipline
Human-readable files do not remove the need for a real parser.
The mailbox uses a Markdown syntax tree rather than splitting strings on headings or blank lines.
The parser treats each H2 heading as a new message, reads initial key: value metadata, and then captures the body.
Malformed blocks are skipped instead of being automatically rewritten.
That matters because mailbox files are both machine-readable and human-editable. A defensive parser should tolerate partial mistakes without corrupting the rest of the history.
Cross-process appends and race conditions
The app and CLI may both be active.
Repeated-message scheduling may also attempt to append while a human sends a manual message.
Inbox writes therefore use a cross-process lock. Missing files are created before append, and both manual and repeated messages use the same validation and write path.
Separate file ownership removes the largest race condition: the human-side tools and agent never append to the same file.
Optional repeated messages
CodexPigeon can repeat guidance at an interval while the desktop app or an explicit CLI runner remains active.
A repeat is stored in STATE.json with fields such as:
- next run time;
- last send time;
- interval;
- source message ID;
- send count;
- status;
- whether warnings were explicitly allowed.
When due, the scheduler appends a new normal inbox message. It does not use a hidden channel and does not mutate the original message.
Repeated sending is disabled by default and has a minimum interval to prevent accidental hot loops.
The intended use is reminders such as:
Re-check the deployment gate before finalizing.
It is not intended for continuously pushing destructive commands into a task.
Message validation
Mailbox content is prompt-like input. It can still ask the agent to do something unsafe.
CodexPigeon warns on messages that appear to contain:
- secrets;
- destructive operations;
- risky production actions;
- credential-related instructions;
- unusually large content.
Warnings block sending by default.
The CLI requires an explicit override, and the UI requires a deliberate “send anyway” action.
This is a user-safety layer, not a perfect secret scanner. The project still tells users not to paste credentials into mailbox messages.
Important limitations
The mailbox model has honest constraints.
An agent cannot read while trapped inside a blocking tool call
If a shell command runs for twenty minutes without returning control, the agent cannot inspect new mailbox messages during those twenty minutes.
That is a consequence of safe-checkpoint delivery rather than active interruption.
Markdown is not a transactional database
The files should remain reasonably small and append-only. The design favors transparency and local inspectability over high-throughput messaging.
Worktree handoffs can lose ignored runtime files
Mailbox runtime files are intentionally ignored by Git because they may contain private working context. Some worktree or repository handoff patterns therefore will not carry them automatically.
The app and hooks recreate missing files when needed, but the runtime history is local by design.
Hooks require a trusted project
If Codex does not trust the repository, project-local hook layers may be ignored. The UI and doctor command need to make that situation visible.
What I learned
1. Not every human-agent interaction belongs in chat
A repository task has state outside the conversation: files, plans, test results, worktrees, and constraints. A repo-local mailbox can be a better place for durable working guidance.
2. Refusing capabilities can strengthen a product
CodexPigeon would be more powerful if it could directly steer or interrupt turns. It would also be a different and harder-to-reason-about product.
The explicit refusal to use mutation APIs created a clearer identity.
3. Ownership tables are powerful design tools
The rule “the app writes inbox, the agent writes outbox and receipts” removed entire classes of ambiguity and write races.
4. Acknowledgement is not the same as execution
Structured receipts made it possible to distinguish seen, accepted, deferred, applied, blocked, and rejected states.
5. Transparent protocols are easier to debug
When something goes wrong, the user can open a Markdown file and inspect what was sent, what the agent acknowledged, and which side owns the next action.
Current status
CodexPigeon is a working MVP for local macOS and Linux development.
It includes:
- an Electron and React desktop application;
- a CLI;
- a mailbox parser and installer;
- read-only Codex thread discovery;
- project-local hooks;
- diagnostics and doctor commands;
- automated tests for protocol, installer, hooks, and App Server boundaries.
Signed desktop packages and a Windows packaging path remain future work.
ademisler
/
codexpigeon
Non-interrupting mailbox companion for Codex worktrees on macOS and Linux, with repo-local inbox files, read-only App Server discovery, and safe checkpoint reminders.
CodexPigeon
Non-interrupting mailbox companion for Codex worktrees
Built by Adem Isler · MIT licensed · macOS and Linux development builds
What This Is
CodexPigeon lets a human leave repo-local guidance for a Codex task without injecting into the active chat turn.
It gives each selected repo/worktree a .codex-mailbox/ folder. The app and CLI
append human messages to INBOX.md; the Codex agent reads those messages at
safe checkpoints through installed project instructions and hooks, then writes
status back to agent-owned files.
The product invariant is strict:
- no active chat steering
- no chat message injection
- no turn interruption or turn start APIs
- no App Server write APIs
- no app writes to agent-owned
OUTBOX.mdorRECEIPTS.md
Open Source
This repository is intended to be inspectable. The mailbox protocol is plain Markdown, runtime files are repo-local, and all Codex App Server access is restricted to read/status/discovery methods.
The current release is a working…
I would particularly value feedback on these questions:
- Which safe checkpoints make sense during a long coding task?
- Is plain Markdown the right tradeoff for human-agent working state?
- Which receipt states would be useful beyond accepted, deferred, applied, and blocked?
- Where should the boundary sit between asynchronous guidance and active steering?
This is the third article in my series documenting the products I have built, moving from browser utilities toward AI-native developer tools and local desktop systems.

Top comments (0)