[BUG] Agent Teams: SendMessage silently succeeds when recipient name doesn't match team-lead's inbox polling target
Bug Description
When using Agent Teams, SendMessage with type: "message" silently succeeds even when the recipient value doesn't match the target agent's inbox polling name. Messages are written to an orphaned inbox file that no agent ever reads, resulting in silent message loss.
This is particularly problematic for teammate → team-lead communication: if an LLM agent uses a character name or alias (e.g., recipient: "alice") instead of "team-lead", the message is written to alice.json but the team lead's InboxPoller reads from team-lead.json.
Root Cause (Source Analysis)
Analyzed from cli.js v2.1.39 (minified bundle, identifier names preserved in debug strings):
1. validateInput — No recipient existence check
// SendMessage.validateInput (decompiled)
async validateInput(input) {
if ("recipient" in input && typeof input.recipient === "string"
&& input.recipient.trim().length === 0)
return { result: false, message: "recipient must not be empty" };
return { result: true }; // Any non-empty string passes
}
Only checks for empty string. No validation against actual team members.
2. Recipient normalization (Ni4) — Passes through arbitrary names
function normalizeRecipient(recipient) {
if (recipient.includes("@")) {
let parsed = parseAgentId(recipient);
if (parsed) return parsed.agentName;
}
return recipient; // "alice" passes through unchanged
}
3. getInboxPath (Kt) — Uses recipient as-is for file path
function getInboxPath(agent, team) {
let teamName = team || getTeamName() || "default";
let sanitizedTeam = sanitize(teamName);
let sanitizedAgent = sanitize(agent); // "alice" → alice
let dir = join(teamsDir(), sanitizedTeam, "inboxes");
return join(dir, `${sanitizedAgent}.json`); // → alice.json
}
4. writeToMailbox (V9) — Creates new inbox file without validation
function writeToMailbox(recipient, message, team) {
ensureInboxDir(team);
let path = getInboxPath(recipient, team);
if (!fileExists(path))
writeFileSync(path, "[]", "utf-8"); // Creates orphaned inbox
// ... writes message to file
}
5. InboxPoller read target (QE6) — Team lead reads a different file
function getPollingTarget(state) {
if (isTeammate()) return getAgentName();
if (isTeamLead(state.teamContext)) {
let leadId = state.teamContext.leadAgentId;
return state.teamContext.teammates[leadId]?.name || "team-lead";
}
}
The team lead polls team-lead.json (or its registered name), while the message was written to alice.json. The message is permanently lost.
Steps to Reproduce
- Create an agent team
- In the teammate's prompt, include a character name for the leader (e.g., "Report to Alice (team lead)")
- Teammate calls
SendMessagewithrecipient: "alice"and meaningful content - Observe:
SendMessagereturnssuccess: true - Team lead never receives the message content (only
idle_notificationsummary appears)
Expected Behavior
SendMessageshould validate that the recipient matches an actual team member name- If the recipient doesn't match, return an error (e.g.,
"Unknown recipient 'alice'. Available: team-lead, coder, reviewer") - At minimum, warn when creating a new inbox file for a name that doesn't match any registered team member
Actual Behavior
SendMessagereturns{ success: true, message: "Message sent to alice's inbox" }- Message is written to
~/.claude/teams/{team}/inboxes/alice.json - No agent polls
alice.json - Message is silently lost
Suggested Fix
In validateInput or at the writeToMailbox call site, add a check against known team members:
// Pseudocode for suggested fix
async validateInput(input, context) {
// ... existing empty check ...
if ("recipient" in input && input.type === "message") {
const teamName = getTeamName(context.teamContext);
const teamConfig = readTeamConfig(teamName);
if (teamConfig) {
const memberNames = teamConfig.members.map(m => m.name);
const normalized = normalizeRecipient(input.recipient);
if (!memberNames.includes(normalized)) {
return {
result: false,
message: `Unknown recipient "${normalized}". Available team members: ${memberNames.join(", ")}`,
errorCode: 9
};
}
}
}
return { result: true };
}
Impact
This affects any team where LLM agents use character names, aliases, or role-based names in prompts. The workaround is to explicitly specify recipient: "team-lead" in prompts, but this is fragile since LLMs naturally pick up contextual names.
Environment
- Claude Code version: 2.1.39
- OS: Windows 11 (also likely affects all platforms)
- Setting:
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1"
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗