This is the fourteenth article in my Claude Code tools series. The first thirteen articles took tools apart one by one. This article is different: it examines a cross-tool, orthogonal capability—the Background mechanism.
This series begins with a prerequisite article explaining what tools are and how Claude uses them. Here I apply the same four lenses—naming, tool descriptions, field descriptions, and schema—to a capability distributed across several tools.
Why Background deserves its own article
There is no tool literally named Background in Claude Code. That is a design choice, not an omission. Background behavior is spread across the ecosystem:
| Location | Form |
|---|---|
Bash run_in_background: true
|
Parameter |
Agent run_in_background: true
|
Parameter |
| Monitor | A tool whose purpose is continuous background listening |
| CronCreate | A future, background trigger |
| TaskStop | A tool for stopping background work |
| TaskOutput | Explicit output retrieval (now deprecated) |
<task-notification> |
Completion notification |
Looking at only one of these reveals only a fragment. Together they form one system.
Background is an execution mode, not a behavior
Every ordinary tool has a core behavior: Read reads a file, Bash runs a command, and Agent delegates to another Claude. Background is not another behavior. It is a mode of execution.
The same Bash or Agent action can run in two modes:
- Synchronous (default): call → block → receive the result → continue the conversation.
-
Background (
run_in_background: true): call → return a task ID immediately → the main Claude continues → the harness sends a notification when the work completes.
Making Background a family of separate tools (BackgroundBash, BackgroundAgent, and so on) would double the tool count and the decision burden. Claude Code instead parameterizes the existing behavior. This is the same orthogonal design used by Grep’s output_mode and Edit’s replace_all: keep the behavior fixed and switch the mode with a field.
The analogy to Unix is useful. fork() creates a child process, the task ID acts like a PID, and <task-notification> resembles SIGCHLD. Claude Code has built a process-management system at the harness layer, except a child may be a shell process, another Claude, or a WebSocket connection.
One task-ID system
Background Bash, background Agent, Cron jobs, and Monitor instances all receive a handle that the harness can track:
Bash(command: "long-training.py", run_in_background: true) → task_id
Agent(prompt: "...", run_in_background: true) → task_id
CronCreate(cron: "*/5 * * * *", recurring: true, prompt: "...") → job_id
Monitor(command: "tail -f log | grep ERROR", persistent: true) → monitor_id
All can be stopped through the same TaskStop interface. The notifications differ by type:
- Bash completion includes an output-file path.
- Agent completion includes the agent’s result.
- Cron fires a new turn using its prompt.
- Monitor turns each stdout line or WebSocket event into a message.
The interface is unified while the semantics remain specialized. That is good API design.
Three kinds of background work
Type A: a one-shot task with a known end
Examples: background Bash and background Agent.
The task starts now, ends later, and the harness sends one <task-notification> when it finishes. Typical uses include a long test, build, training run, research subagent, or dependency installation.
Bash(command: "...", run_in_background: true) → task_id
(conversation continues)
[task-notification: completed, output: /tmp/.../out.log]
Read("/tmp/.../out.log")
Type B: trigger at a future time
Example: CronCreate.
The job has not started yet. CronCreate records a prompt and a schedule; when the time arrives, the runtime starts a new turn with that prompt. This is useful for a reminder, a check five minutes from now, or tomorrow’s morning self-check.
That is the key distinction: Type A is an already-running task waiting to finish; Type B is a not-yet-started task waiting for its trigger.
Type C: continuous or long-lived listening
Example: Monitor, especially persistent: true.
The listener starts now, has no fixed end (or ends at timeout, stream close, or an explicit stop), and sends a notification for every event. Typical uses are following errors in a log, watching filesystem changes, subscribing to a WebSocket, or following a PR until merge.
| Dimension | One-shot task | Scheduled trigger | Continuous listener |
|---|---|---|---|
| Notifications | One on completion | N fires | Unbounded, one per event |
| Lifecycle | Started, then finishes | Not started, waiting | Started, stream continues |
| Interface | Bash / Agent | CronCreate | Monitor |
| Stop | Natural completion or TaskStop | CronDelete | TaskStop |
The anti-polling principle
Background should create a simple intuition:
When the harness can notify you, do not sleep. When a task is already in the background, do not synchronously wait for it.
Do not start a background task, sleep for a minute, and then read its output. The harness will notify you; keep working and read the output after the notification.
Do not repeatedly curl a CI endpoint followed by sleep 60. Choose the primitive that matches the semantics:
- a command that eventually exits → Bash
run_in_backgroundwith anuntilloop; - a stream of events → Monitor;
- one check at a known time → CronCreate.
Claude Code also warns that long leading sleep commands are blocked. This is a hard system constraint, not merely advice. The tool layer actively pushes Claude toward asynchronous thinking.
Task ID, Job ID, and Task (the to-do item)
The word “task” is overloaded:
| Name | System | Meaning |
|---|---|---|
Task (TaskCreate, TaskList, …) |
Task family | A conceptual to-do item |
task_id / <task-notification>
|
Background | A running task instance |
| ID returned by CronCreate | Cron family | A scheduled job |
Task-family tasks may not have started. Background tasks are concrete running processes or listeners. TaskStop and the former TaskOutput control the latter, not the to-do list. A useful memory aid is that TaskCreate/TaskList/TaskGet/TaskUpdate feel like CRUD, while TaskStop is runtime control.
A concurrent workflow
Imagine the user wants to start three things at once: run a 15-minute test suite, ask a subagent to research the auth architecture, and start a dev server while watching its log.
Bash(command: "pnpm test:all", run_in_background: true) → task_test
Agent(prompt: "research auth architecture", run_in_background: true) → task_auth
Bash(command: "pnpm dev", run_in_background: true) → task_dev
Monitor(
command: "tail -f /tmp/dev.log | grep -E --line-buffered 'error|warn'",
description: "dev server errors"
) → monitor_dev
Multiple tool calls in one message start concurrently. The main Claude remains available for conversation. The test completion produces a task notification; the research completion produces another; a dev-log error arrives immediately through Monitor; and TaskStop(task_dev) stops the server. Traditional REPL execution would serialize these tasks. Background turns them into parallel work, with the longest task determining the total elapsed time.
Boundaries
Background is not unlimited:
- Session-only: jobs die when the session ends. Use system cron, launchd, GitHub Actions, or a cloud scheduler for multi-day work.
- Notifications wait for idle: a task notification does not interrupt Claude while it is processing a user turn; it queues until the REPL is idle.
- Rate limiting: high-volume Monitor streams and accumulated output can be stopped or truncated. Strong filters are essential.
-
Concurrency limits: Bash and Agent jobs are queued beyond the runtime’s host-dependent limit (typically
min(16, cpu-2)). - Sandboxing remains active: background execution does not bypass the sandbox. A separate, explicit dangerous-sandbox option is required to change that boundary.
The four-layer design signal
Naming
The most important naming decision is the absence of BackgroundBash or BackgroundAgent. One Boolean, run_in_background, declares that Background is an execution mode, not a new behavior.
TaskStop is also deliberately generic. Stopping a background Bash task is not BashStop; stopping an Agent is not AgentStop. The single verb resembles kill <pid>: whatever was forked, the same control operation stops it.
The old TaskOutput has been de-emphasized in favor of reading the output file with Read. If an existing primitive covers the capability, the API does not add another verb. Cron and Monitor keep their specialized names externally while sharing the task infrastructure internally.
Tool-level descriptions
Descriptions carry most of the normative guidance:
- avoid unnecessary
sleepand polling; - use background Bash for one-shot waiting;
- use Monitor for streaming events;
- strong filters are required because every event costs conversation context;
- Cron jobs are session-only, not system cron jobs.
“Long leading sleep commands are blocked” upgrades a recommendation into an enforced constraint. Monitor’s warnings about buffering, rate limits, and raw logs do the same for event streams. These descriptions are cross-tool contracts encoded in individual tools.
Field-level descriptions
The most visible design signal is that Bash and Agent use the same field with opposite defaults:
| Tool | Default | Typical intent |
|---|---|---|
| Bash | false |
Most shell commands are short; synchronous is simpler |
| Agent | true |
Subagents usually take time; keep the main Claude free |
Bash’s field description says to use background only when the result is not needed immediately, promises a later notification, and warns not to append &. Agent’s description explains when to opt out and run in the foreground. The same field is a mirror: the defaults reflect the typical lifecycle of each tool.
The output path in <task-notification> naturally directs Claude to Read the result. Monitor’s persistent Boolean distinguishes a bounded listener from a session-long listener.
Schema validation
Background has a thin schema layer because it modifies existing synchronous behavior rather than replacing it. The important safeguards are runtime-level: Monitor’s one-hour timeout, persistent semantics, cron-expression validation, unified task IDs, concurrency limits, and sandbox enforcement.
The thinness is evidence for the central design choice. If Background were a new capability, it would need an independent schema. Because it is a mode, the existing command, prompt, and cron schemas are mostly enough; Background adds a flag or a trigger policy.
Relationship to neighboring tools
| Tool family | Background expression | Default | Role |
|---|---|---|---|
| Bash | run_in_background: false |
Foreground | Long commands without blocking the main loop |
| Agent | run_in_background: true |
Background | Subagents normally have a long lifecycle |
| Task family | IDs, TaskStop | N/A | State and a stop interface |
| Cron family | Time-driven wakeups | Scheduled | Time primitive |
| Monitor | Event-driven wakeups | Event stream | Event primitive |
The anti-polling principle connects Bash, Agent, and Monitor: do not poll a job that will notify, do not schedule checks for an Agent that is already running, and do not use a continuous tail -f for a one-shot event.
The first thirteen tools are mostly synchronous: Read, Edit, Write, Grep, Glob, WebFetch, WebSearch, the interaction tools, and so on. Background is the hidden implementation layer that lets those spatial primitives extend into asynchronous collaboration.
Conclusion: the invisible skeleton
Background’s elegance is not simply that it makes AI asynchronous. It is that every layer reinforces parameterization instead of tool proliferation:
-
Naming: no independent tool, just
run_in_backgroundand a unified TaskStop. - Tool descriptions: anti-polling, filter-first, session-only, and notification contracts are distributed across the relevant tools.
- Fields: Bash and Agent have opposite defaults because their normal lifecycles differ.
- Schema: deliberately thin, with timeout, persistence, concurrency, and sandbox safeguards as the backstop.
The missing schema is itself evidence. Background is not a new action; it is a mode attached to existing actions. Without it, Claude Code would be a one-action-at-a-time assistant. With it, Claude becomes a multi-threaded collaborator that can run long jobs, keep talking, react to events, and schedule future work.
The series now closes with a complete map: user alignment → locating → perceiving → executing → fallback → scaling through subagents, time, event streams, and background execution. The goal was never to enumerate tools, but to apply the same four-layer anatomy to each one. The reusable lesson is restraint: each tool does one small thing, and composition creates the collaboration system.
Top comments (0)