[BUG] Bash run_in_background=true silently drops completion notifications for 2nd+ concurrent tasks

Resolved 💬 2 comments Opened Mar 28, 2026 by winfred5432 Closed Apr 28, 2026

What's Wrong

I run multiple concurrent Bash background tasks from a long-lived streaming session (daemon mode). Only the first task's task_notification is ever delivered. The rest complete successfully — I can verify their output files — but their notifications never arrive. No errors, no warnings. Silent drop.

Agent run_in_background=true works perfectly in the exact same scenario. Every notification lands.

I discovered this after debugging why my background monitoring pipeline was silently losing results. The asymmetry between Bash and Agent was the giveaway.

Steps to Reproduce

Launch 3 concurrent Bash background tasks from a streaming session:

import { query } from "@anthropic-ai/claude-agent-sdk";

async function* streamingGen() {
  yield {
    type: "user" as const,
    message: {
      role: "user" as const,
      content:
        "Launch three Bash background tasks simultaneously:\n" +
        '1. `sleep 10 && echo "task_a done"` with run_in_background=true\n' +
        '2. `sleep 20 && echo "task_b done"` with run_in_background=true\n' +
        '3. `sleep 30 && echo "task_c done"` with run_in_background=true\n\n' +
        "After launching all three, say: THREE_LAUNCHED. Do NOT check on the tasks."
    }
  };
  // Simulate daemon: stay alive long enough for all tasks to complete
  await new Promise((r) => setTimeout(r, 60000));
}

const q = query({
  prompt: streamingGen(),
  options: { maxTurns: 30, permissionMode: "bypassPermissions", includePartialMessages: true }
});

let notifCount = 0;
for await (const msg of q) {
  const m = msg as Record<string, unknown>;
  if (m.type === "system" && m.subtype === "task_notification") {
    notifCount++;
    console.log(`task_notification #${notifCount}: ${m.task_id} status=${m.status}`);
  }
}
console.log(`Total: ${notifCount} / 3 expected`);
// Expected: 3
// Actual:   1

Expected: 3 task_notification messages
Actual: 1 task_notification message (tasks 2 and 3 silently dropped)

The bug does not reproduce when using Agent tool instead of Bash — Agent notifications are always delivered reliably.

Root Cause

The CLI's result stashing condition only matches local_agent and local_workflow task types:

// cli.js (simplified)
if (
  getTaskEntries(appState).some(
    (t) => (t.type === "local_agent" || t.type === "local_workflow") && isPending(t)
  )
) {
  P = message; // stash result → polling loop stays alive
} else {
  outputStream.enqueue(message); // deliver immediately → i() exits
}

When a Bash background task is pending:

  • Task type is local_bashnot matched → result delivered immediately → i() exits
  • Later task completions enqueue their task-notification with priority "later"
  • But i() is already gone — nobody re-enters to dequeue them
  • Notifications are stuck in the queue forever

When an Agent background task is pending:

  • Task type is local_agentmatched → result stashed → polling loop stays alive
  • All subsequent notifications are consumed correctly

The first Bash notification sometimes arrives because the task may complete during i()'s execution window — it gets picked up before i() exits. Tasks 2 and 3 complete after i() has already exited.

Suggested Fix

Add local_bash to the result stashing condition:

// Before:
(t.type === "local_agent" || t.type === "local_workflow") && isPending(t)

// After:
(t.type === "local_agent" || t.type === "local_workflow" || t.type === "local_bash") && isPending(t)

Alternatively, make the task-notification enqueue for Bash tasks trigger i() re-entry directly (the way "now" priority commands do), so notifications are consumed regardless of polling loop state.

Environment

  • Claude Code Version: 2.1.81
  • SDK: @anthropic-ai/claude-agent-sdk 0.2.81
  • Platform: Anthropic API (first-party)
  • OS: macOS (Apple Silicon)
  • Shell: zsh

Workaround

Use Agent tool with run_in_background=true for all background tasks that require reliable completion callbacks. Agent wraps the Bash command and uses local_agent task type, which correctly triggers the polling loop.

This works, but it's a meaningful overhead difference — wrapping every background shell command in an Agent just to get reliable notifications is not ideal.

View original on GitHub ↗

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