配布したhookがWindowsで静かに死ぬのを塞ぐ — process.exit と top-level await
AIエージェントのhookが「出力は正しいのに反映されない」形で無効化される真因と、ネットワークに依存しない回帰テスト・変異テストの型
約4.2万トークンの節約 (API料金換算で約63円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。
この巻物について
「配布したhookがWindowsで静かに死ぬのを塞ぐ — process.exit と top-level await」は、開発プロセスカテゴリのAI指示書(MDファイル)です。AIエージェントのhookが「出力は正しいのに反映されない」形で無効化される真因と、ネットワークに依存しない回帰テスト・変異テストの型この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約4.2万トークン(API料金換算で約63円)・93%のトークンを節約できます。
- カテゴリ
- 開発プロセス
- 対応AI
- claude-code、cursor、codex-cli
- ライセンス
- 商用利用可 (再販不可)
- 価格
- 無料
- ゼロから開発時
- 約4.5万トークン
- この巻物使用時
- 約3,000トークン
- 節約量
- 約4.2万トークン (約63円)
- 更新日
- 2026-08-20
使い方 (AIに渡す3つの方法)
いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/hook-windows-process-exit-top-level-await/raw を読み込んで、この指示書どおりに実装して"
中身
配布した hook が Windows で「静かに死ぬ」のを塞ぐ
AI コーディングエージェント(Claude Code など)の hook を複数台へ配布して運用する時、 エラーも出さず、機能だけが消える壊れ方をする。動いて見えるので誰も気づかない。 このMDは、実際に踏んだ1件の真因と、二度と踏まないための検証手順・回帰テストの型をまとめたもの。
症状
UserPromptSubmit hook(依頼文を見て参考情報を注入するスクリプト)が、
24時間に1回だけ動かない。それ以外は正常に動く。
Windows / Node v24 で実行すると:
Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 76
終了コードは 127(環境により -1073740791)。
真因: top-level await の中で process.exit を呼んでいた
hook スクリプトは stdin を読むため top-level await を使うことが多い。
// 壊れている例
let raw = ''; for await (const chunk of process.stdin) raw += chunk;
try {
if (!raw) process.exit(0);
const data = await fetch(API).then((r) => r.json()); // ← 非同期ハンドルが開く
console.log(JSON.stringify({ hookSpecificOutput: { ... } }));
} catch {}
process.exit(0); // ← ここで落ちる
process.exit() はイベントループを待たずに即殺す。top-level await の評価中に呼ぶと、
HTTP クライアントのソケット(libuv の async handle)が閉じかけの状態で強制終了され、
libuv の内部 assertion を踏む。
24時間に1回だけ落ちた理由: このスクリプトは取得結果を24時間キャッシュしていた。
キャッシュが生きている間は fetch が走らない=開いたハンドルが無いので落ちない。
ネットワークI/Oを伴う経路だけが落ちる。 だから普段は完璧に動いて見える。
なぜ「静かに」死ぬのか(ここが一番重要)
hook は stdout に JSON を書き、終了コード0で正常終了して初めて採用される。
- stdout には注入内容がちゃんと書けていた(実測 1243 バイト)
- しかし終了コードが 0 でないため、ホスト側は hook 失敗として扱い、出力を丸ごと捨てる
つまり「出力は正しいのに反映されない」。ログにも成果物にも痕跡が残らない。 exit code が出力の採用可否を決めるタイプの連携では、 クラッシュは「機能が静かに無効化される」という形で現れる。
修正
process.exit() を全廃し、async function main() + 早期 return で自然終了させる。
// process.exit を絶対に呼ばない: top-level await の評価中に呼ぶと Windows の Node が
// libuv の assertion で異常終了する。exit!=0 だとホストが hook 失敗として出力を捨てるため、
// 非同期I/Oを伴う経路だけが静かに死ぬ。早期リターンで自然終了させる。
async function main() {
let raw = ''; for await (const chunk of process.stdin) raw += chunk;
try {
if (!raw) return;
const data = await fetch(API).then((r) => r.json());
console.log(JSON.stringify({ hookSpecificOutput: { ... } }));
} catch {}
}
await main().catch(() => {});
懸念と実測: process.exit を外すと、HTTP クライアントの keep-alive が
イベントループを掴んで hook がハングしないか。→ 実測 600〜700ms で自然終了した
(hook の timeout は 6 秒)。ハングを恐れて exit を残すのは逆効果。必ず実測で確かめる。
回帰テストの型(ネットワークに依存させない)
外部APIを叩くスクリプトのテストを素直に書くとネットワーク依存になり、 「落ちる日と落ちない日がある」テストになって信用されなくなる。次の2層に分ける。
1) 構造テスト: 危険な書き方をソース文字列で機械的に禁止する
真因が「特定の書き方」なら、その書き方の存在自体をテストで禁止するのが最も安い。
test('hook: process.exit を使わない', () => {
const source = fs.readFileSync(hookPath, 'utf8');
assert(!/process\.exit\s*\(/.test(source), 'process.exit が残っている');
assert(/await main\(\)/.test(source), 'main() を await していない');
});
注意: この判定はコメントも引っかける。理由をコメントで残す時は
process.exit( という括弧付きの文字列を書かない(実際に自分のコメントで落とした)。
逆にコメントを除外する実装にすると判定が緩むので、文言側を調整するほうが安全。
2) 挙動テスト: 外部依存はキャッシュを事前投入して切る
取得結果をキャッシュする設計なら、テストが先にキャッシュを書いておけば fetch は走らない。
モックサーバは不要。同期的に子プロセスを起動するテスト(spawnSync)と
自前のHTTPスタブは同一プロセスでは併用できない(spawnSync がイベントループを止めるため
スタブが応答できずデッドロックする)ので、この方法が最も素直。
function seedCache(prefix, items) {
const home = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); // 本物の設定を壊さない
fs.mkdirSync(path.join(home, '.config'), { recursive: true });
fs.writeFileSync(path.join(home, '.config', 'cache.json'),
JSON.stringify({ __catalog__: { at: new Date().toISOString(), items } }));
return home;
}
テストは必ずホームディレクトリを一時ディレクトリへ差し替える
(HOME を直接上書きせず、APP_HOME のような専用の環境変数を実装側に用意しておく)。
本番の設定・キャッシュ・認証情報を書き換えるテストは、一度事故ると原因究明に何倍もかかる。
3) テストが本当に効いているか(変異テスト)
追加したテストは、わざと壊して FAIL することを確認するまで信用しない。
- 修正を元に戻す(
process.exitを復活させる)→ 該当テストが FAIL するか - 必須リストから項目を消す → 該当テストが FAIL するか
「全部PASS」だけを見て満足すると、何も検査していない空テストが残る。
Windows / Linux の両方で通すこと
この不具合は Windows でしか出ない。 コード生成をコンテナや Linux 上のエージェントに任せている場合、 生成側は原因となる挙動を一度も観測できない。
- 実装を委譲しても、OS依存の検証は必ず手元の実機で行う
- テストは Windows と Linux の両方で走らせる(
node --versionが違えば結果も変わり得る) - 外部CLIを呼ぶ時はシェル経由の引用符に依存しない(
execFileSyncの配列渡しにする)
配布物にありがちな「静かに死ぬ」経路(同種のチェックリスト)
- 起動判定の文字列比較:
import.meta.urlとprocess.argv[1]の素の比較は、 配布パスが symlink / junction だと外れてmain()が一度も呼ばれない(出力ゼロ・exit 0)。 realpath で解決し、Windows では大文字小文字を無視して比較する。 - 設定ファイルの BOM: UTF-8 BOM 付きで保存された
.envは、素朴なパーサだと 先頭のキー名が\uFEFFKEYになり「未設定」と判定される。全APIキーが黙って無効化される。 パーサを1本に統一し、BOM を自動除去する。 - 取得失敗の巻き添え:
pull → 配布 → 登録を1つの try に入れると、 取得が1回失敗した端末は以後ずっと配布も登録もされない。取得だけ独立した try/catch にする。 - 新機能を「必須リスト」に載せ忘れる: 自己修復の仕組みがあっても、 必須リストに入っていない項目は修復されない。登録処理と必須リストの両方に載せる。 これもテストで縛れる(必須リストから外すと FAIL するテストを書く)。
- hook の timeout: 重い処理は timeout で毎回殺されるが、 ホスト側が黙って続行するため気づかない。実測で所要時間を測り、 長い処理は切り離して非同期に投げる。
完了の判定基準
次の全部を実行した結果で言えるようになるまで「動いた」と言わない。
- 通常経路・キャッシュ切れ経路・空入力・壊れた入力・上限到達の全経路で exit 0
- 各経路の所要時間を計測し、timeout の内側に収まっている
- Windows と Linux の両方でテストが通る
- 追加したテストを意図的に壊して FAIL することを確認した
- 本番の設定ファイル・キャッシュ・認証情報が変更されていないことを確認した
- 配布経路(各端末が実際に取得するURL)を取得し直し、新しい版が返ることを確認した (CDN が数分間 旧版を返すことがある。配布側で吸収してから「配布可」と言う)
よくある質問
+「配布したhookがWindowsで静かに死ぬのを塞ぐ — process.exit と top-level await」とは何ですか?
AIエージェントのhookが「出力は正しいのに反映されない」形で無効化される真因と、ネットワークに依存しない回帰テスト・変異テストの型
+どれくらいトークン(費用)を節約できますか?
ゼロから開発すると約4.5万トークンかかりますが、この巻物を使えば約3,000トークンで済みます。差し引き約4.2万トークン(API料金換算で約63円)・93%の節約です。
+どうやって使いますか?
無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。
+どのAIツールに対応していますか?
claude-code、cursor、codex-cli に対応しています。
+商用利用できますか?
ライセンスは「商用利用可 (再販不可)」です。
🤝 自分でAIを動かすのは、まだ不安…という方へ
この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。
関連する巻物
ドキュメント駆動開発プロセス CLAUDE.md — 作るものを固めてから書かせる
「AIが暴走して意図と違うものを作る」を根絶する開発プロセス指示書。UI仕様→機能設計→実装の順をAIに強制し、1ファイルごとに承認ゲートを挟む。受託開発・チーム開発向け。
AIに指示書マーケットを自動参照させ、終了時に自動出品させるMD
開発依頼を受けた瞬間にマーケットの完成済み指示書を検索してAIに読ませ、セッション終了時には汎用ノウハウを自動出品させる仕組みの作り方。全台配布・秘密情報スキャン・実際に踏んだ配布バグ3つの回避込み。
「そのPCにしか直せない障害」をAIに自分で気付かせて着手させる
特定の1台にしかリポジトリが無い機能は、修正手順を書いても誰にも実行されず放置される。SessionStart hook で当該PCのAIだけに指示を出し、完了後は指示書へ状態を書き戻して再実装事故を防ぐ型。走査の時間予算とセッション跨ぎの再開、メール一致だけの自動承認がなりすまされる理由と署名キー方式、状態問い合わせAPI、鍵の自動配布、no-op通知の抑止まで、実際に94件の滞留を解消した実例に基づく手順。
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア