Claude Code Monitorツールの使い方—grepのバッファで32秒遅れる

ログに ERROR が出た瞬間に手を止めたい。かといって tail -f の画面を見張り続けるのは時間の無駄です。Claude Code v2.1.98 で追加された Monitor ツールが、この見張りを肩代わりします。

目次

tail -f を渡すと3イベントだけ届いた

検証環境は macOS、Claude Code v2.1.260。監視対象のログを吐くスクリプトはこれです。

$ cat /tmp/mon-test/writer.sh
#!/bin/bash
sleep 3; echo "$(date +%T) [warn]  retrying connection" >> /tmp/mon-test/deploy.log
sleep 3; echo "$(date +%T) [error] ERROR upstream timeout after 30s" >> /tmp/mon-test/deploy.log
sleep 3; echo "$(date +%T) [fatal] Traceback (most recent call last):" >> /tmp/mon-test/deploy.log
sleep 3; echo "$(date +%T) [info]  deploy finished status=succeeded" >> /tmp/mon-test/deploy.log

指示から組み立てられるコマンド

使うときにツール名を書く必要はありません。「/tmp/mon-test/deploy.log を見張って、エラーか終了状態が出たら教えて」と頼むと、Claude 側がフィルタ付きのコマンドを組み立てて Monitor に渡します。実際に発行されたのがこれ。

{
  "command": "tail -n 0 -f /tmp/mon-test/deploy.log | grep -E --line-buffered \"ERROR|Traceback|FAILED|succeeded|failed\"",
  "description": "deploy.log のエラーと終了状態",
  "timeout_ms": 20000,
  "persistent": false
}

tail -n 0 で既存の行を読み飛ばし、監視を仕掛けた以降の追記だけを対象にしています。

届いたイベント

ログには4行書き込まれましたが、会話に流れてきたのは3件でした。

<event>20:10:34 [error] ERROR upstream timeout after 30s</event>
<event>20:10:38 [fatal] Traceback (most recent call last):</event>
<event>20:10:41 [info] deploy finished status=succeeded</event>

落ちたのは [warn] retrying connection の1行。grep -E のパターンに乗らないためです。stdout の1行が1イベント、それだけ。timeout_ms に達すると打ち切りの通知が入ります。

<event>[Monitor timed out - re-arm if needed.]</event>

Bash run_in_background と Monitor の分かれ目

どちらもバックグラウンドで走ります。分かれるのは通知が何回欲しいか。

欲しい通知使うものコマンドの形
1回だけ(「起動したら教えて」)Bash の run_in_background条件成立で exit する until ループ
発生ごとに何度でもMonitortail -f / inotifywait -m / while true
発生ごと、ただし終わりがあるMonitor行を出しつつ完了で break するループ

1回で済む通知に tail -f を使わない

「サーバーが起動したら教えて」を tail -f log | grep -m 1 READY で書くと詰まります。-m 1 があるので grep はマッチした時点で終わるはず、と考えたくなるところ。実際に8秒観測した結果がこれです。

$ ( tail -n 0 -f app.log | /usr/bin/grep --line-buffered -m 1 READY ) & p=$!
$ sleep 8; kill -0 $p && echo "8秒経過してもプロセスが生存 = ハング"
READY server started
8秒経過してもプロセスが生存 = ハング

マッチ行は出ています。それでもパイプラインは終わりません。理由は tail 側にあって、grep が閉じたあとも次の書き込みを試みるまで SIGPIPE を受け取らない。ログが静かになれば、その書き込み自体が起きない。同じ条件を until ループで書くと 1.17秒で自然終了しました。

$ until grep -q READY app2.log; do sleep 0.2; done
>> 1.17秒で自然終了

止め方は TaskStop

persistent: true を付けた監視はセッションが終わるまで生き続けます。途中で降ろすなら TaskStop に task ID を渡すか、Claude に「あの監視を止めて」と伝えるだけ。task ID は Monitor の起動時に返ってきます(検証時は bn3ga3bl4)。バックグラウンドタスク全般の見え方はClaude Codeのタスク管理—TaskCreateとTodoWriteは何が違うのかに整理しています。

grep のバッファで32.5秒遅れる

Monitor を仕掛けたのにイベントが一向に来ない。ログには行が増えている。原因はパイプ途中のブロックバッファリングです。

16401バイト溜まるまで1行も出ない

20ms 間隔で1行77バイトを吐き続けるスクリプトを /usr/bin/grep に通し、最初のバッチが届くまでを測りました。

$ python3 gen.py | /usr/bin/grep ERROR | python3 count.py
最初のバッチ: 213行 / 16401バイト / 1行あたり 77バイト
最初の行が emit されてから届くまで: 32.53秒

1行目は最初の20ms で書かれています。それが会話に届くのは 32.53秒後。213行ぶん、16KB のバッファが埋まって初めて grep が flush するからです。awk も同じで、fflush() の有無で遅延が 33.03秒 と 0.000秒 に割れました。

$ python3 gen.py | awk '/ERROR/{print}' | python3 stamp.py
[1] 遅延 33.032s

$ python3 gen.py | awk '/ERROR/{print; fflush()}' | python3 stamp.py
[1] 遅延 0.000s

パイプの全段をフラッシュさせる

各段に明示的なフラグを付けます。grep なら --line-buffered、awk なら fflush()、sed なら -u。1段でも欠けると、そこで行が滞留します。

ここでハマったのが検証環境。Homebrew で入れた ugrep 7.8.4grep を乗っ取っていて、こちらは既定で行バッファ動作。--line-buffered の有無で差が出ず、最初は「バッファリングなんて起きない」と誤診しました。grep --version で実体を確かめてから測り直しています。

head はフラッシュできない

| head -3 を挟むと、3件溜まるまで1件も届きません。1秒間隔で5行出しているのに、3行が同じタイミングで到着します。

[1] +2.28s
[2] +2.28s
[3] +2.28s

届いた直後にストリームも終わる。件数を絞る用途で head を挟むと、リアルタイム性と継続監視を同時に失います。

成功マーカーだけの grep は失敗を取りこぼす

Monitor のツール説明には “Coverage – silence is not success.” と題した節があります。監視対象が落ちたときにフィルタが何も出さなければ、「まだ実行中」と区別がつきません。

# NG: クラッシュしてもハングしても無音になる
tail -f run.log | grep --line-buffered "elapsed_steps="

# OK: 進捗と、実際に手を打つ失敗シグネチャを一本の alternation に入れる
tail -f run.log | grep -E --line-buffered "elapsed_steps=|Traceback|Error|FAILED|assert|Killed|OOM"

実際に succeeded だけを見張らせて、対象を Traceback で落としてみました。5秒観測して、通知はゼロ。ログにはこう書かれています。

$ grep -E "succeeded|Traceback|Error|FAILED|Killed" crash.log
Traceback (most recent call last):
KeyError: 'token'

判断は「いまこのプロセスが落ちたら、自分のフィルタは何か出すか」で足ります。出ないなら広げる。ノイズが少し混ざるほうが、クラッシュを見落とすよりましです。

timeout_ms は既定5分、persistent で無期限

commandws は排他。同じ呼び出しに両方は書けません。

入力既定値効き方
timeout_ms300000 (5分)上限は 3600000 (1時間)。到達で kill
persistentfalsetruetimeout_ms を無視し、セッション終了か TaskStop まで継続
descriptionなし(必須)通知に毎回表示される。「errors in deploy.log」のように具体的に書く

Claude Code は 200ms 以内に出た stdout 行を1つの通知にまとめます。複数行のスタックトレースが分割されないのはこのため。逆にイベントを出しすぎる監視は Claude Code 側が自動で停止するので、その場合はフィルタを絞って張り直します。

WebSocket をスクリプトなしで繋ぐ

公式ドキュメントの “WebSocket source” 節にある通り、サーバー側が既にイベントを push しているなら、ポーリングスクリプトを書かずに直接繋げます。v2.1.195 以降で使えます。

{
  "ws": { "url": "wss://events.example.com/stream", "protocols": ["v1"] },
  "description": "deploy events"
}

フレームの扱いと打ち切り条件

  • テキストフレーム1つがイベント1件。複数行のメッセージでも分割されません
  • バイナリフレームは素通ししない。[binary frame, 512 bytes] のようなプレースホルダ行が届きます
  • 1 MiB を超えるメッセージが来ると watch 自体が終了します
  • ソケットが閉じると watch が終わり、close コードが渡されます

接続が拒否される宛先

URL は ws://wss:// で、認証情報の埋め込みと空白は不可、ASCII 文字のみ。加えて Claude Code は、プライベートアドレス・リンクローカルアドレス・クラウドメタデータアドレスを指す URL を拒否します。名前解決の結果がそれらに落ちるホスト名も同じ扱い。sandbox.network.deniedDomains に載ったホストも通りません。managed settings で allowManagedDomainsOnly が有効なら、許可リスト外は全部拒否されます。

Monitor が使えない環境

ツールが一覧に出てこないときは、実行環境と環境変数を疑います。Amazon Bedrock、Google Cloud の Agent Platform、Microsoft Foundry は対象外。DISABLE_TELEMETRYCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC が設定されている環境でも同じく消えます。

Claude Code は Monitor のコマンドを Bash と同じ権限ルールで評価するので、Bash 向けの allow / deny パターンがそのまま効きます。ただし auto mode では Monitor を名指しした allow ルールが外され、分類器が Bash と同じ基準で審査します。デーモン側の見え方はClaude Codeバックグラウンド実行の確認—agent viewとデーモンの中身で扱いました。

スケジューリング側とも繋がります。公式ドキュメント “Run prompts on a schedule” によれば、間隔を省いた動的な /loop を頼むと Claude が Monitor を直接使うことがある。ポーリングを回さない分、トークン消費が減ります。

まとめ

  • 通知が1回で済むなら Bash の run_in_background、発生ごとに欲しいなら Monitor
  • パイプの全段を行バッファにする。/usr/bin/grep は 16401バイト溜まるまで沈黙し、実測で 32.53秒遅れた
  • head はフラッシュできないのでフィルタに挟まない。tail -f | grep -m 1 はマッチ後もハングする
  • フィルタには失敗シグネチャを必ず入れる。無音とクラッシュは区別がつかない

既定の timeout_ms は5分。長時間の監視は persistent: true にして、用が済んだら TaskStop で降ろします。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次