cross-project-mail — Cross-Project AI Agent Mailbox (v1, M1 walking skeleton)
Overview
A flat-file mailbox so AI agents in sibling projects on one host can leave each other messages that survive session boundaries. Replaces the manual cross-repo-review.md Outbound/Inbound file convention with a real CLI + SessionStart notification + idempotent migrator.
v1 trust model: single-user, single-host. All agents on this host share user trust. The only enforced XPIA defense is <user_data> delimiter wrapping on read; HMAC / quarantine / sender-allowlist deferred to v2 (M3) where cross-host trust boundaries make them pay their way.
Not in v1: MCP server, FastMCP transport, polling daemon, mcp_agent_mail wrapper, cross-machine (AB) mode, VS Code Copilot Chat integration, wiki-MCP. See docs/plans/2026-05-16-cross-project-mail-v1-design.md for the staged roadmap and the rationale for de-scoping from the full Stage 2 design.
When to Use
- Sending a message from an AI agent in project A to an agent (or any agent) in project B on the same host
- Checking inbox at session start (SessionStart hook does this automatically)
- Migrating an existing
cross-repo-review.mdfile to mailbox messages (one-shot, idempotent) - Auditing the mailbox layout (
cpmail doctor) - Replying to a prior message or starting a thread
When NOT to Use
- Cross-machine messaging (deferred to M3 — use the in-flight git mirror or wait for v2)
- Adversarial cross-agent security (v1 trusts all local agents; v2 adds HMAC + quarantine)
- High-volume messaging (>100/day) — the design budget is ≤100 msg/day across all projects
- Real-time / sub-second latency — SessionStart-hook polling is session-granular, not push
- Substitute for git commit messages or PR descriptions — those remain authoritative for code change context
Quick Start
# Install (one-time, per host)
bash ~/.claude/skills/cross-project-mail/install/install.sh
# Send a message
echo "Reviewed your Windows installer fix — looks good, merging" | \
cpmail send --to vs-code-foundry --subject "installer PR ack" --source-type human
# List unread for current project
cpmail list --unread
# Read a specific message (body is wrapped in <user_data>...</user_data>)
cpmail read 01HXY7K3M9TBVN8P4ZQGRJ2WAD
# Acknowledge (moves to .acked/)
cpmail ack 01HXY7K3M9TBVN8P4ZQGRJ2WAD
# Migrate an existing cross-repo-review.md (idempotent — safe to re-run)
cpmail migrate --from /path/to/vs-code-foundry/cross-repo-review.md
# Validate mailbox layout
cpmail doctor
Hard Rules
Command Reference
| Command | Purpose |
|---|---|
cpmail send --to <project> [--agent <slug>] --subject <s> [--source-type <t>] [--reply-to <ulid>] [--label <l>]... [--from <stdin|@file>] |
Compose and write a message envelope. Body from stdin or @path/to/file. |
cpmail list [--project <slug>] [--unread|--all|--acked] [--limit N] [--since <iso>] |
List messages in an inbox. Defaults to current project + unread. |
cpmail read <ulid> |
Print frontmatter + body. Body is wrapped in <user_data>...</user_data>. |
cpmail ack <ulid> |
Mark as acked. Moves file to inbox/<project>/.acked/. |
cpmail migrate --from <path> [--dry-run] |
Convert a cross-repo-review.md file into mailbox messages. Idempotent. |
cpmail doctor [--mailbox <path>] |
Validate layout, report orphans, corrupted frontmatter, unknown enums. |
cpmail _detect-project [--from-cwd <path>] |
Internal: detect current project name. Used by SessionStart hook. |
cpmail --help |
Show all commands. |
Exit Codes
0— success1— runtime error (filesystem, permissions, etc.)2— invalid input (bad schema, missing required field, invalid enum value)3— not found (message id, project, file)
Anti-Patterns
| Anti-Pattern | Why it fails | Correct |
|---|---|---|
cat ~/.ai-mailbox/inbox/foundry-lab/01HXY...md |
Bypasses the <user_data> delimiter wrap — exposes prompt-injection surface |
cpmail read 01HXY... |
| Hand-editing a message file | Skips schema validation; may corrupt the closed-set field whitelist | Use cpmail send (re-send if you need to amend); cpmail ack to close |
Sending with source_type: human for an AI-summarized web fetch |
False provenance — recipient under-applies skepticism | Use source_type: web_fetch_quoted when body quotes web content; ai_summary when AI-condensed |
Polling ~/.ai-mailbox/inbox/ from a tight loop |
DoS yourself; the hook is for SessionStart only | Use cpmail list --unread on demand |
| Storing secrets in message body | The mailbox is plain markdown on disk, mode 0600 but not encrypted | Reference secrets by path or env-var name; never inline them |
Cross-machine sync via rsync ~/.ai-mailbox/ |
v1 has no consistency model for concurrent writers across hosts | Wait for M3 (mcp_agent_mail wrapper) or use git mirror with careful single-writer discipline |
Sending without a subject |
All commands require subject; sender-discipline (recipient grep relies on it) | Include --subject "..."; ≤200 chars |
Modifying ~/.ai-mailbox/inbox/*/.acked/ files |
The ack archive is intentionally read-once-write-once | Never; use cpmail doctor if state seems wrong |
File Layout (Reference)
~/.ai-mailbox/
├── inbox/<recipient-project>/
│ ├── <ulid>.md # unread messages
│ └── .acked/<ulid>.md # acked archive
├── outbox/<sender-project>/ # symlinks back into inbox/
└── .config.yaml # optional path overrides, project aliases
Message Envelope (Reference)
See schemas/envelope.v1.json for the formal JSON Schema. Example:
---
schema_version: "1"
msg_id: 01HXY7K3M9TBVN8P4ZQGRJ2WAD
sent_at: "2026-05-17T14:32:10Z"
sender:
project: vs-code-foundry
agent: claude_code
host: workstation
recipient:
project: foundry-lab
agent: null
subject: "Windows installer cmd+ps1 hardening"
source_type: human
reply_to: null
thread_id: null
labels: [cross-repo-review, installer]
ack_state: unread
attachments: []
---
Two cross-platform installer bugs that no Linux session could surface...
SessionStart Hook
Installed at ~/.claude/skills/cross-project-mail/hooks/session-start.sh. Budget: <100ms. Wired into Claude Code via ~/.claude/settings.json hooks.SessionStart. Codex + Gemini parity via symlinks. Copilot CLI: documented gap (#971) — workaround is a shell alias.
Output (only when unread > 0):
[mail] 3 unread for foundry-lab (run: cpmail list --unread)
Tests
tests/test_cpmail.py covers ~30 cases: envelope round-trip, CLI send/list/read/ack, SessionStart hook (0 / N unread), migrator idempotency, doctor on broken mailbox, performance budgets (p95 hook <100ms with 100 unread; cpmail list p95 <200ms with 1000 messages). Run: cd tests && python3 -m pytest.
Cross-References
- Design doc:
docs/plans/2026-05-16-cross-project-mail-v1-design.md - M3 follow-up (mcp_agent_mail wrapper for cross-machine): preserved in archive at
progress/forge-cycle-s034/stage2-design/ - M4 follow-up (wiki-MCP for global knowledge search): separate cycle, not in this skill
- Related:
process-observation(telemetry sink we may use in M2),wiki(M4 substrate)