CLAUDE LABEN
KEYS — v2.1.238 で keybindingFlavor 設定が加わりました。readline を指定すると Ctrl+W が Bash と同じく直前の空白まで削除します。既定の classic は従来どおりですPLUGINS — プラグインマーケットプレイスに headersHelper を置けるようになりました。カタログ取得のたびに短命トークンなどの HTTP ヘッダを生成でき、インストール時はコマンド内容を表示して確認を求めますRUNNER — self-hosted-runner に defer-shutdown-max-min が追加されました。SIGTERM を受けても接続中のセッションへは配信を続け、指定分数の後に残りを退避して終了しますMEMORY — 長時間の対話セッションでメモリが際限なく増える問題が直りました。サブエージェントのツール結果が直近の表示ウィンドウを外れた時点で解放されますMCP — mcp list と mcp get が、無効化したサーバーへヘルスチェックの接続をせず Disabled と表示するようになりました。無効にしたサーバーが一覧表示だけで起動する無駄がなくなりますPRICING — Claude Sonnet 5 の導入価格 100万トークンあたり入力2ドル・出力10ドルは8月31日で終了し、9月1日から入力3ドル・出力15ドルへ移ります。残り9日ですKEYS — v2.1.238 で keybindingFlavor 設定が加わりました。readline を指定すると Ctrl+W が Bash と同じく直前の空白まで削除します。既定の classic は従来どおりですPLUGINS — プラグインマーケットプレイスに headersHelper を置けるようになりました。カタログ取得のたびに短命トークンなどの HTTP ヘッダを生成でき、インストール時はコマンド内容を表示して確認を求めますRUNNER — self-hosted-runner に defer-shutdown-max-min が追加されました。SIGTERM を受けても接続中のセッションへは配信を続け、指定分数の後に残りを退避して終了しますMEMORY — 長時間の対話セッションでメモリが際限なく増える問題が直りました。サブエージェントのツール結果が直近の表示ウィンドウを外れた時点で解放されますMCP — mcp list と mcp get が、無効化したサーバーへヘルスチェックの接続をせず Disabled と表示するようになりました。無効にしたサーバーが一覧表示だけで起動する無駄がなくなりますPRICING — Claude Sonnet 5 の導入価格 100万トークンあたり入力2ドル・出力10ドルは8月31日で終了し、9月1日から入力3ドル・出力15ドルへ移ります。残り9日です
記事一覧/Claude Code
Claude Code/2026-08-22初級

長い処理を別セッションに任せて待つ — 通知が届かなかったときのための完了マーカー

Claude Code の notify_when_idle で別セッションの完了通知を受け取る手順と、通知が落ちても待ち続けないための完了マーカーの最小実装。3つの状態切り分けを実行結果つきで解説します。

Claude Code232自動化78ビルドシェルスクリプト運用14

個人開発で続けている壁紙アプリに新しいシリーズを追加するとき、素材1枚から8種類の派生画像を書き出すバッチを回します。App Store と Google Play の両方に出しているので、解像度違いも含めると枚数はすぐにまとまります。終わるまでに10分近くかかります。

その10分のあいだ、私は別の作業に移ったつもりでいて、実際には3分おきにターミナルへ戻っていました。終わったかどうかを見に行くためだけに。

Claude Code に長い作業を任せているときも同じでした。別のセッションでビルドを走らせ、こちらのセッションでは設計の話を続ける。構成そのものは快適なのに、完了を知る手段がないせいで、覗きに行く回数は結局減りませんでした。

v2.1.236 で入った notify_when_idle は、この「覗きに行く」動作をほぼ丸ごと消してくれます。ただし通知は届かないことがあります。届かなかったときに黙って待ち続けないための備えまで含めて、この記事で組み上げます。

notify_when_idle が引き受けること、引き受けないこと

notify_when_idle は、同じマシン上で動いている別の Claude Code セッションに対して「次にアイドルになったら一度だけ知らせてほしい」と依頼する仕組みです。クロスセッションのメッセージ送信に添えるオプションで、対象は macOS と Linux です。

設計として好ましいのは次の3点です。

  • オプトイン。頼んだときだけ動きます。既定で通知が飛び回ることはありません
  • 一度きり。以後ずっと知らせ続ける状態にはなりません
  • ポーリングなし。待つ側が定期的に問い合わせる必要がありません

一方で、引き受けてくれない範囲もはっきりしています。通知が伝えるのは「アイドルになった」という事実だけで、作業が成功したのか失敗したのかは含まれません。ビルドがコンパイルエラーで落ちたときも、セッションはアイドルになります。

もうひとつ、依頼した相手のセッションが落ちてしまった場合には、通知そのものが発生しません。プロセスが消えた以上、アイドルになる主体がいないからです。処理が長いほど、この経路を踏む確率は上がります。

つまり notify_when_idle は「待ち時間を減らす」ためのものであって、「結果を保証する」ためのものではありません。ここを分けて考えると、次に何を足せばよいかがはっきりします。

まず2セッション構成を組む

手順そのものは短いです。

  1. claude --version で v2.1.236 以上であることを確認します
  2. 長い処理を任せる側のセッションを開き、ビルドやバッチを投げます
  3. 依頼する側のセッションから、クロスセッションのメッセージ送信に notify_when_idle を添えて送ります
  4. 通知が来たら、結果を取りに行きます

オプションの正確な綴りや引数の並びは版によって変わりうるため、手元の版のヘルプで一度確認しておくことをおすすめします。私自身は更新のたびに一度だけ確認して、それきり触っていません。

この4手順だけでも、待ち時間の体感はかなり変わります。問題は、4番目の「通知が来たら」が成立しなかったときです。

通知が届かない経路は3つある

v2.1.238 より前は、いくつかの経路でメッセージが落ちても、送信側には成功したように見えていました。現在は落ちたことが送信側へ返ります。

経路何が起きているか送信側に返るもの
受信側が受信拒否相手が crossSessionInbound: "refuse" を設定しているrefused
レート制限短時間に送りすぎている落ちたことが伝わる
受信箱が満杯相手が読んでいないメッセージで埋まっている落ちたことが伝わる

以前は「送ったのに何も起きない」が「送れたが相手が忙しいのだろう」と区別できませんでした。この修正で、少なくとも依頼が成立しなかったことは分かります。

ただし、依頼が成立したかどうかと、作業が終わったかどうかは別の話です。相手のセッションが途中で落ちれば、依頼は成立していても通知は来ません。ここを埋めるのが、次に作る完了マーカーです。

完了マーカーを1ファイルだけ置く

やることは単純で、長い処理の外側に薄い包みをかぶせ、状態を1ファイルだけ書き出します。通知の有無に関係なく、そのファイルを読めば現在地が分かる、という状態を作ります。

#!/usr/bin/env bash
# run-marked.sh — 長い処理を包み、開始・心拍・終了を1つのファイルに残す
set -uo pipefail
MARK="${1:?マーカーファイルのパスを渡してください}"; shift
TMP="${MARK}.tmp.$$"
 
write_mark() {
  printf '{"state":"%s","pid":%d,"at":%d,"exit":%s}\n' \
    "$1" "$$" "$(date +%s)" "${2:-null}" > "$TMP"
  mv -f "$TMP" "$MARK"
}
 
write_mark running
 
# 2秒ごとに心拍を打つ。親が消えれば一緒に止まる
( while kill -0 "$$" 2>/dev/null; do sleep 2; write_mark running; done ) &
HB=$!
 
"$@"; CODE=$?
 
kill "$HB" 2>/dev/null
write_mark done "$CODE"
exit "$CODE"

使うときは、これまでのコマンドの前に置くだけです。

chmod +x run-marked.sh
./run-marked.sh /tmp/build.mark npm run build

書き方の理由を3つだけ説明させてください。

別名で書いてから mv -f する理由。同じファイルを直接 > で開くと、その瞬間に中身がいったん空になります。待つ側が運悪くそのタイミングで読むと、状態が空文字として観測されます。別名に書いてから rename すると、待つ側が読むのは常に「ひとつ前の完全な内容」か「新しい完全な内容」のどちらかになります。1行の違いですが、長時間ポーリングされる前提では効いてきます。

心拍を打つ理由running のまま更新が止まった状態と、running のまま動き続けている状態は、ファイルの中身だけでは区別できません。時刻を上書きし続けることで、待つ側が「更新が止まった」という異常を検出できるようになります。

終了コードを残す理由。通知が伝えてくれないのがまさにこれでした。成功と失敗を呼び出し側へ持ち帰るために、exit の値をそのまま書いておきます。

待つ側は3つの状態を切り分ける

待つ側は、マーカーを読んで「終わった」「まだ動いている」「心拍が途絶えた」を分けます。

#!/usr/bin/env bash
# wait-for.sh — マーカーを見て3つの状態を切り分ける
set -uo pipefail
MARK="${1:?マーカーファイルのパスを渡してください}"
TIMEOUT="${2:-1800}"   # 待ち時間の上限(秒)
STALE="${3:-10}"       # 心拍がこの秒数途絶えたら異常とみなす
START=$(date +%s)
 
field() { sed -n "s/.*\"$1\":\"\{0,1\}\([a-z0-9]*\)\"\{0,1\}.*/\1/p" "$MARK"; }
 
while :; do
  NOW=$(date +%s); ELAPSED=$(( NOW - START ))
  if [ -f "$MARK" ]; then
    STATE=$(field state); AT=$(field at); AGE=$(( NOW - ${AT:-NOW} ))
    case "$STATE" in
      done)
        CODE=$(field exit)
        echo "done exit=${CODE} waited=${ELAPSED}s"; exit "${CODE:-0}" ;;
      running)
        if [ "$AGE" -gt "$STALE" ]; then
          echo "stale 心拍が ${AGE}s 途絶えました waited=${ELAPSED}s"; exit 75
        fi ;;
    esac
  fi
  if [ "$ELAPSED" -ge "$TIMEOUT" ]; then echo "timeout ${TIMEOUT}s"; exit 124; fi
  sleep 1
done

手元の Linux 環境で3通り走らせた結果です。心拍の間隔は2秒、STALE は8秒にしています。

--- ケース1: 正常終了 ---
done exit=0 waited=6s
waiter exit=0
--- ケース2: 失敗 (exit 3) ---
done exit=3 waited=4s
waiter exit=3
--- ケース3: 途中で SIGKILL ---
stale 心拍が 9s 途絶えました waited=10s
waiter exit=75

ケース3が、この仕組みを足した理由そのものです。処理を包んでいたプロセスを強制終了したので、done は書かれません。通知も来ません。それでも待つ側は10秒後に「心拍が途絶えた」と判断して抜けています。マーカーの中身は {"state":"running","pid":4,...} のまま残っており、どこで止まったかも後から読めます。

正常に動いているあいだ、マーカーの時刻は2秒ごとに進みます。

{"state":"running","pid":64,"at":1787378969,"exit":null}
{"state":"running","pid":64,"at":1787378971,"exit":null}
{"state":"running","pid":64,"at":1787378973,"exit":null}

待つ側の終了コードは、そのまま次の判断に使えます。

状態判定条件終了コード次にすること
donestate が done処理の終了コードをそのまま返す成功/失敗で分岐する
stale心拍が STALE 秒を超えて止まった75再実行する
timeout全体の上限を超えた124打ち切って調べる

75 は一時的な失敗を表す慣習的な値で、再実行の合図として扱いやすい番号です。124 は timeout コマンドが使う値に揃えてあります。呼び出し側で番号を見るだけで、再実行すべきか調べるべきかを分けられます。

無人で回すときに変える2箇所

手元で使う分には既定値のままで足ります。スケジュール実行に組み込むときは、2箇所だけ調整してください。

STALE は心拍の間隔の3〜5倍に。CI コンテナや共有マシンでは、負荷が高い瞬間に心拍が1回飛ぶことがあります。間隔と同じ値にすると、正常なのに stale と判定されます。2秒間隔なら8〜10秒が扱いやすい範囲でした。

TIMEOUT は普段の所要時間の2倍程度に。上限を長く取りすぎると、止まっていることに気づくのが翌朝になります。派生画像のバッチは10分前後なので、私は1200秒にしています。

上流の処理が終わったかどうかを下流が自分で確かめる、という考え方をスケジュール実行全体へ広げた話は、無人スケジューラの完了台帳と依存バリアで扱っています。複数のタスクが日をまたいで連なる構成を組んでいる方には、こちらが近いはずです。

今日いちばん長く待たされている処理をひとつだけ選んで、run-marked.sh で包んでみてください。包むだけで、待つ側を書く前から「今どこにいるか」が読めるようになります。そこから先は、通知が届く日も届かない日も、同じやり方で待てます。

お読みいただきありがとうございました。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

Claude Code2026-06-13
編集した SKILL.md が反映されない — /reload-skills と .claude/skills 自動ロードで再起動なしに差し替える
SKILL.md を編集しても実行中のセッションには反映されません。.claude/skills 自動ロードと /reload-skills、SessionStart フックを組み合わせた再起動なしの差し替え運用と、版番号による検証手順を実運用の経験から整理します。
Claude Code2026-08-09
一時的な401が長期トークンを置き換える — ヘッドレス実行の資格情報に「出所」を持たせる
共有した資格情報ファイルは、たった一度の一時的な401で長期トークンを失うことがあります。24並列の実行に401を1回だけ注入して被害範囲を実測し、資格情報に出所フィールドを持たせるガードと隔離のコストを比較、24マイクロ秒で現在地を確定する起動時カナリアまで検証しました。
Claude Code2026-08-05
秘密を渡さず、リクエストだけ通す — サンドボックス資格情報マスキングの差し替えが効く境界
Claude Code のサンドボックス資格情報マスキングは、センチネル値を読ませて送信時に実値へ差し替える設計です。差し替えが効く認証方式と効かない方式、そしてボディに載せたときに生じる長さのずれを、最小プロキシを自作して実測しました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →