This is the thirteenth article in my Claude Code tools series. The previous article, [[Claude Code Tools Deep Dive (12) - Cron Family|The Cron Family]], explained how Claude can trigger actions across time. Cron is clock-driven: it fires at a scheduled moment, regardless of what is happening outside.
Engineering has another kind of waiting: wait until something happens, without knowing exactly when. For example:
- “Tell me when
ERRORappears in the logs.” - “Rebuild when the file changes.”
- “Notify me when the PR status changes.”
- “Report each CI check as it settles.”
These cases need an event-driven asynchronous waiting primitive. Claude places a set of sensors, and the runtime reports external events automatically. That is the purpose of Monitor.
Monitor
Monitor is Claude Code’s built-in event-stream listener. Together with Bash background execution and Cron, it completes the three basic asynchronous waiting primitives:
| Tool | Trigger | Meaning |
|---|---|---|
Bash run_in_background |
One task completes | “Tell me when the build is done.” |
| CronCreate | A clock reaches a time | “Remind me at 9.” |
| Monitor | An event stream, one stdout line per event | “Tell me every time X happens.” |
The first two wait for one point. Monitor waits for a line: the stream may continue indefinitely, until a timeout or Claude stops it.
What it solves
Monitor answers the question: how can Claude continuously perceive changes in the outside world?
- More than one notification: Bash background execution normally reports once; Monitor reports every event.
- Event-stream modeling: each stdout line is a notification, naturally aligned with Unix conventions.
- Two data sources: a shell command or a direct WebSocket connection.
- Forces filter design: the prompt makes Claude decide what deserves a notification and what should be ignored.
-
Persistent listening:
persistent: truecan keep a monitor alive for the whole session, useful for PRs and long-running logs.
Its fundamental difference from Cron is simple:
- Cron is clock-driven: time is the active party.
- Monitor is event-driven: the outside event is the active party.
Cron says “I will ask you when the time comes.” Monitor says “call me when something happens.” One is pull; the other is push.
A concrete example
Suppose the user says: “I am starting a 20-minute model training run. Watch the log, tell me about errors immediately, and report progress too.” This is a long-running job whose events occur at unknown times.
Bad alternative 1: sleep and inspect later
Bash(command: "sleep 1200 && cat train.log", timeout: 1300000)
If the process fails after three minutes, Claude does not know until the end. Early signals are lost.
Bad alternative 2: scheduled polling
CronCreate(cron: "*/2 * * * *", recurring: true, prompt: "check train.log and report ERROR")
Polling every two minutes introduces up to two minutes of latency and repeatedly rereads the file. Each Cron wakeup also consumes conversation context.
Bad alternative 3: a background tail
Bash(command: "tail -f train.log", run_in_background: true)
A background Bash job normally notifies once, when it exits. tail -f never exits, so it never sends the useful notifications.
The Monitor solution
Monitor(
command: "tail -f train.log | grep -E --line-buffered 'elapsed_steps=|Traceback|Error|FAILED|Killed|OOM'",
description: "training log: progress and errors",
timeout_ms: 1500000
)
The runtime starts the shell command and follows the log. grep lets only matching lines through. Each stdout line becomes a notification delivered to the conversation immediately. Claude can continue talking or do other work, while progress and failures arrive automatically. After 20 minutes the timeout ends the monitor, or the user can stop it earlier.
The key insight is that Monitor turns Claude from an active poller into a passive receiver. Every relevant external event is known immediately, without repeated polling or a blocked context.
Two data sources: command or WebSocket
Monitor has an unusually rich design: the source can be a shell command or a WebSocket.
Shell command is the common mode. Every line written to stdout is an event.
WebSocket can be used directly:
Monitor(
ws: { url: "wss://events.example.com/stream", protocols: ["v1"] },
description: "deployment event stream",
timeout_ms: 300000
)
The runtime opens the connection; every text frame is one event, while a binary frame is represented as [binary frame, N bytes]. Closing the connection ends the monitor.
Using websocat through command would work, but adds quoting, process, installation, and buffering problems. Built-in WebSocket support removes a process and normalizes the mapping from frames to events. It is a strong example of using a tool to eliminate fragile plumbing, and suggests concrete use cases such as agent-to-agent communication, deployment subscriptions, and long-lived push channels.
When to use Monitor
The tool prompt gives a useful selection rule:
Use Monitor when:
- every occurrence of X should generate a notification;
- every occurrence should be reported until a known terminal condition;
- you need to consume a WebSocket event stream.
Do not use Monitor when:
- you only need one completion notification: use Bash
run_in_backgroundwith a loop that eventually exits; - you need a clock trigger: use CronCreate;
- events arrive at a very high rate: tighten the filter, because rate limiting may stop the monitor.
The important warning is: “Don’t use an unbounded command for a single notification.” For “tell me once when the build is ready,” use a background Bash command such as until grep -q "Ready" dev.log; do sleep 0.5; done. Do not use tail -f ... | grep -m 1 "Ready": tail -f may remain alive after the match, leaving Monitor attached until timeout. Monitor is optimized for continuous events, not one-shot completion.
Technical design
Naming
Monitor is a neutral SRE term for continuous observation and alerting. It is broader than Watch, Tail, Subscribe, or Listen, and steers Claude toward “place a watch and report events,” not “grep the file once.”
The tool-level description is a mini operations guide
The prompt covers notification choice, event-stream semantics, output volume, buffering, data-source preference, and observability completeness.
It explicitly says that each stdout line is an event, then classifies tools by notification count:
-
One notification: Bash with
run_in_background. - One per occurrence indefinitely: Monitor with an unbounded command.
- One per occurrence until a known end: Monitor with a command that emits lines and exits.
It also teaches Unix buffering. Every pipeline stage must flush per line: use grep --line-buffered and awk with fflush(). Avoid head, which may wait for N matches before producing output. This tribal sysadmin knowledge is placed directly in the tool prompt so a naïve command does not make events appear delayed.
The deepest rule is “silence is not success.” A filter must match every terminal state, not only the happy path. A monitor that watches only elapsed_steps= stays silent through a crash, hang, or unexpected exit, making failure indistinguishable from “still running.” Before arming a monitor, ask: if the process crashed right now, would my filter emit anything? If not, widen it to include Traceback, Error, FAILED, Killed, OOM, and similar signals.
“Selective” also does not mean “only good news.” Select the lines you would act on, whether they describe progress or failure. If output becomes excessive, the runtime automatically stops the monitor; Claude should restart it with a tighter filter. Lines arriving within 200 ms are batched into one notification, so a multiline traceback remains readable as one event.
Finally, the prompt prefers the native ws source over command: 'websocat wss://…', avoiding an extra process and another buffering layer.
Fields and runtime rules
Monitor has five fields:
| Field | Meaning |
|---|---|
command |
Shell source; mutually exclusive with ws
|
ws |
WebSocket source with url and protocols
|
description |
Required label shown with every notification |
timeout_ms |
Defaults to 300,000 ms; maximum 3,600,000 ms |
persistent |
Defaults to false; true keeps it alive for the session |
The schema is moderate, but important constraints live in the runtime:
- Exactly one of
commandandwsmust be supplied. -
descriptionis required because it is visible in every notification. - A non-persistent monitor cannot exceed the one-hour timeout.
- Excessive output triggers rate limiting and an explicit stop.
- With
persistent: true,timeout_msis ignored; the monitor ends with the session or an explicitTaskStop.
These are loud failures or loud stops. A bad monitor cannot silently look healthy while doing nothing.
Division of responsibility
| Dimension | Bash background | CronCreate | Monitor |
|---|---|---|---|
| Waits for | One task to finish | A clock time | An event stream |
| Notifications | One on process exit | One per schedule match | One per event |
| Wakeup | Process exit | Scheduled moment | stdout line or WebSocket frame |
| Sources | Shell command | Cron expression | Command or WebSocket |
| Typical use | “Wait for CI” | “Check every five minutes” | “Alert on every log error” |
| Conservative bias | Notify on exit | Fire at the time | Emit only actionable signals |
Monitor completes the waiting model begun by the first twelve tools. Bash waits for a point, Cron waits for a time, and Monitor waits for a line. Compared with Tasks, Task state is pulled by Claude from storage; Monitor state is pushed from the outside world. Compared with Bash polling, Monitor upgrades sleep + poll into a structured event-stream primitive.
Summary
Monitor’s most impressive feature is not merely continuous listening. It embeds an observability methodology in the tool description:
- a minimal SRE-oriented name;
- a long prompt explaining notification choice, buffering, rate limiting, batching, and “silence is not success”;
- only five fields, each backed by a meaningful runtime decision;
- hard runtime protection for source selection, timeouts, and output volume.
The schema locks down the basic shape, while the prompt teaches Claude how to build a reliable watch: event-driven, failure-visible, resistant to conversation flooding, and available over both shell and WebSocket sources.
The next article examines [[Claude Code Tools Deep Dive (14) - Background Mechanism|Background mechanisms]], the final article in the series. It crosses tool boundaries and follows run_in_background through Bash, Agent, the Task family, and Monitor, showing how asynchronous execution becomes a first-class Claude Code semantic.
Top comments (0)