[BUG] Agent Teams: SendMessage silently succeeds when recipient name doesn't match team-lead's inbox polling target

Resolved 💬 4 comments Opened Feb 12, 2026 by becky3 Closed Mar 23, 2026

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

  1. Create an agent team
  2. In the teammate's prompt, include a character name for the leader (e.g., "Report to Alice (team lead)")
  3. Teammate calls SendMessage with recipient: "alice" and meaningful content
  4. Observe: SendMessage returns success: true
  5. Team lead never receives the message content (only idle_notification summary appears)

Expected Behavior

  • SendMessage should 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

  • SendMessage returns { 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"

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗