claude-code-system-prompts/system-prompts/tool-description-background-monitor-streaming-events.md
2026-04-13 14:53:06 -06:00

4.1 KiB

Start a background monitor that streams events from a long-running script. Each stdout line is an event — you keep working and notifications arrive in the chat. Events arrive on their own schedule and are not replies from the user, even if one lands while you're waiting for the user to answer a question.

Monitor is for the streaming case: "tell me every time X happens." For one-shot "wait until X is done," use Bash with run_in_background instead — you'll get a completion notification when it exits.

Your script's stdout is the event stream. Each line becomes a notification. Exit ends the watch.

Each matching log line is an event

tail -f /var/log/app.log | grep --line-buffered "ERROR"

Each file change is an event

inotifywait -m --format '%e %f' /watched/dir

Poll GitHub for new PR comments and emit one line per new comment

last=$(date -u +%Y-%m-%dT%H:%M:%SZ) while true; do now=$(date -u +%Y-%m-%dT%H:%M:%SZ) gh api "repos/owner/repo/issues/123/comments?since=$last" --jq '.[] | "(.user.login): (.body)"' last=$now; sleep 30 done

Node script that emits events as they arrive (e.g. WebSocket listener)

node watch-for-events.js

Script quality:

  • Always use grep --line-buffered in pipes — without it, pipe buffering delays events by minutes.
  • In poll loops, handle transient failures (curl ... || true) — one failed request shouldn't kill the monitor.
  • Poll intervals: 30s+ for remote APIs (rate limits), 0.5-1s for local checks.
  • Write a specific description — it appears in every notification ("errors in deploy.log" not "watching logs").
  • Only stdout is the event stream. Stderr goes to the output file (readable via Read) but does not trigger notifications — for a command you run directly (e.g. python train.py 2>&1 | grep --line-buffered ...), merge stderr with 2>&1 so its failures reach your filter. (No effect on tail -f of an existing log — that file only contains what its writer redirected.)

Coverage — silence is not success. When watching a job or process for an outcome, your filter must match every terminal state, not just the happy path. A monitor that greps only for the success marker stays silent through a crashloop, a hung process, or an unexpected exit — and silence looks identical to "still running." Before arming, ask: if this process crashed right now, would my filter emit anything? If not, widen it.

Wrong — silent on crash, hang, or any non-success exit

tail -f run.log | grep --line-buffered "elapsed_steps="

Right — one alternation covering progress + the failure signatures you'd act on

tail -f run.log | grep -E --line-buffered "elapsed_steps=|Traceback|Error|FAILED|assert|Killed|OOM"

For poll loops checking job state, emit on every terminal status (succeeded|failed|cancelled|timeout), not just success. If you cannot confidently enumerate the failure signatures, broaden the grep alternation rather than narrow it — some extra noise is better than missing a crashloop.

Output volume: Every stdout line is a conversation message, so the filter should be selective — but selective means "the lines you'd act on," not "only good news." Never pipe raw logs; use grep --line-buffered, awk, or a wrapper that emits exactly the success and failure signals you care about. Monitors that produce too many events are automatically stopped; restart with a tighter filter if this happens.

Stdout lines within 200ms are batched into a single notification, so multiline output from a single event groups naturally.

The script runs in the same shell environment as Bash. Exit ends the watch (exit code is reported). Timeout → killed. Set persistent: true for session-length watches (PR monitoring, log tails) — the monitor runs until you call TaskStop or the session ends. Use TaskStop to cancel early.