Agent teams: document a contract for external processes writing to teammate inbox files

Status Open
Maintainer reply None cached
Activity 3 comments · opened Aug 20, 2026

The agent-teams docs describe ~/.claude/teams/{team}/inboxes/{agent}.json and state that Claude Code validates entries on read and removes malformed ones — but there is no documented entry schema, no locking contract, and no statement about whether external writers are supported at all.

Observer tooling (herdmates: a mission-control board over team files) has exactly one write: a human-confirmed "nudge" message appended to a stuck teammate's inbox, done under an advisory lock with read-modify-atomic-rename. Today that operates in an undocumented seam: a validation change could silently discard entries, and concurrent writes between Claude Code and the external writer can lose updates because no shared locking protocol exists.

Ask, in order of preference: (1) document the inbox entry schema + a locking/append protocol external writers may rely on; (2) or a small CLI/IPC affordance to enqueue a message into a teammate's inbox; (3) or an explicit statement that external writes are unsupported, so tooling can stop offering them.

View original on GitHub ↗

3 Comments

caioniehues · 10 days ago

Related prior asks for the option-(2) shape (a small affordance for external processes to enqueue a message): #27441 and #53049 request an external-process message-injection API. Cross-session messaging (v2.1.224) doesn't cover this case — its socket is reachable only by a session's own child processes, and cross-session messages bypass the team inbox files entirely, so observer tooling still has no supported write path to a teammate.

kcarriedo · 10 days ago

Running into the same undocumented-seam problem on an orchestration layer that coordinates Claude Code agent teams via their inbox files.

The specific gap we hit: there is no defined locking protocol between Claude Code's own inbox writer and an external process. The atomic-rename approach (write to a temp file, rename over the target) is safe when Claude Code is NOT concurrently writing, but if Claude Code has the file open during a team step, the rename can win the race and Claude Code's write goes to the old fd, producing a silent drop.

A minimal documented contract would need to cover at least three things:

  1. The entry schema (so external writers can construct valid JSON -- is it exactly the schema in the source, and which fields are required vs optional?).
  2. The locking primitive -- either advisory file lock (flock) or an atomic-rename-into-position protocol explicitly blessed by the team.
  3. What Claude Code does on entry validation failure -- discard silently vs. log somewhere external tools can observe.

Without (3), it is very hard to debug whether your write was accepted without adding a polling loop that reads back the inbox after each write.

One thing that has helped us in the interim: write a separate sidecar file (e.g. inboxes/{agent}.pending.json) that the external process owns exclusively, and have a thin wrapper process move entries into the real inbox when it holds the lock. Adds a hop, but contains the race.

Happy to share the pattern in more detail if it would help the docs effort.

caioniehues · 10 days ago

We hit the same undocumented-seam problem coordinating Claude Code agent teams (herdmates). One finding that may help this thread: for processes that are children of a session, there's a push path that avoids the inbox-file race entirely, because Claude Code owns the write end to end.

A session exports CLAUDE_CODE_MESSAGING_SOCKET (per-session UDS) and CLAUDE_CODE_MESSAGING_TOKEN to its child processes. The wire protocol is NDJSON, at most two lines — and while it's not in the public docs, the v2.1.237 binary logs the literal recipe at inbox startup:

{"type":"auth","token":"<CLAUDE_CODE_MESSAGING_TOKEN>"}
{"type":"user","message":{"role":"user","content":"<text>"}}

Verified live on v2.1.237/Linux: a child process posted, and the message reached the receiving session's model as a real turn — enqueued mid-turn, delivered after, re-framed as "Another Claude session sent a message" (not as user input), with origin: {kind:"peer", from:"unknown", verifiedPeerPid:<pid>} — the kernel-verified poster pid, recorded even though the poster had exited before delivery.

Two details relevant to the debugging-observability ask in (3): the auth line is conditional (the inbox logs REQUIRED vs optional from its own state — when key-file publishing fails it accepts unauthenticated peers), and a post can be answered with a peer_message_status receipt (held/denied/expired), so unlike the inbox-file seam you can observe whether your message was accepted or held for user approval (permission-mode parity).

Honest limits of what we've verified: the probe was a direct child of the session (same relationship a hook has, but not a literal hook invocation); the hold path under bypassPermissions is untested; there's no visible way to name the sender (it shows from: "unknown"); and this doesn't cover the fully-external-process case this issue asks for — the socket/token only reach the session's own children. For the remaining inbox-file seam, +1 on documenting the entry schema and validation-failure behavior.