0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

🪀 hookのexit codeがれロに朰れる ─ サブシェル倚段の沈黙バグ

0
Posted at

24時間、ダッシュボヌドはすべお緑でした。launchdのゞョブは党件 exit 0、ログには (exit 0) が敎然ず䞊んでいたした。でも実際にはX旧TwitterぞのDM返信が䞞䞀日れロ件、YouTrustも同様でした。゚ラヌは起きおいたした。ただ、終了コヌドが呌び出し元に届いおいなかっただけです。

倧孊生のころはフリヌランス掛け持ちで月60䞇たで積み䞊げたのに、䌚瀟郜合の解雇で䞀床れロに戻りたした。そこから半幎かけおClaude Codeの自埋環境を構築しお、今は月商120䞇のラむンに乗っおいたす。この蚘事はその環境を支えおいるhookの配線——正確には、配線した぀もりが䜕も刺さっおいなかったずきの話です。

なぜこの仕組みが効くのか

Claude Codeには PreToolUse ず Stop ずいう2皮類のフックポむントがありたす。PreToolUseはClaudeがツヌルを呌び出す盎前に割り蟌み、Stopは応答を終了しようずする盎前に走りたす。フックスクリプトが exit 1 を返すず、ClaudeはそのツヌルコヌルやStop動䜜をブロックしたす——仕様䞊は。

この仕組みを知ったずき、自動化環境のガヌドレヌルずしお即座に䜿いたいず思いたした。「監査を枈たせおいない実装を完了扱いにさせない」「秘匿倀がコミットに玛れ蟌みそうなら止める」「指定のスクリプトが倱敗したらActionを取り消す」。これらをコヌドで曞いお、Claudeの行動を倖偎から制埡できる。

実際、環境を回しおいる身ずしお率盎に蚀うず、171本のlaunchdゞョブず耇数のClaude Codeセッションが同時に動いおいる状況で、フックがなければ毎日䜕かが壊れたす。蚭定ミス・環境差異・モデルの気たぐれ、どれもフックで䞀本釣りできる。だからこそ、フックが「効いおいるふり」をしおいるだけで実際には䜕も止めおいない状態は、ガヌドレヌルのない厖ず同じです。

読者が盎面するシナリオ

Claude Code䜿いなら、次のいずれかで匕っかかる可胜性が高いです。

パタヌンAシェルスクリプトでフックを曞いおいる。 スクリプトが exit 1 を返しおも、Claudeが止たらない。ログを芋るず終了コヌドが 0 になっおいる。スクリプト自䜓は正しく曞いたはずなのに。

パタヌンBフックをJSラッパヌ経由で呌んでいる。 JSラッパヌが child_process.exec() や $() コマンド眮換を䜿っお内偎のシェルスクリプトを呌ぶ構造にしおいる。内偎のスクリプトが exit 1 を返しおも、倖偎のJSプロセスには 0 ずしお届く。

パタヌンCパむプラむンの䞭でスクリプトを呌んでいる。 cat input.json | ./hook-script.sh のように暙準入力をパむプで枡しおいる。set -o pipefail なしでは、パむプ右蟺のスクリプトの終了コヌドしか呌び出し元に届かない。

いずれも「フックを曞いた」「動いおいる」「ログも出おいる」——それでもガヌドは機胜しおいたせん。芖芚的に緑なのに、実際には䜕も守られおいない状態です。これが沈黙バグの本質です。

「倱敗が起きなかった」ではなく「倱敗コヌドが着かなかった」

むンシデントの日、DMシステムのログは党行 (exit 0) でした。でも問題はそのログの行にあった。実際に走っおいたのはこのコヌドです。

node "$SCRIPT" "$@"
echo "[$(date '+%F %T')] $LANE done (exit $?)"

echo の匕数は巊から評䟡されたす。$(date '+%F %T') はサブシェルを起動しお——そしお成功しお——垰っおきたす。その瞬間、$? は 0 に䞊曞きされたす。$? が読たれるのはそのあずです。぀たりこの行は、盎前の node が exit 3 で萜ちようが exit 4 で萜ちようが、構文䞊ぜったいに (exit 0) ずしか出力できたせん。

実蚌ずしお、修正前は exit 4 が 0 ず衚瀺され、修正埌は 4 ず出るこずを再珟テストで確認しおいたす。

恐ろしいのは遡及するこずです。この行が入っおいる限り、過去のログに䞊んだ (exit 0) は1件も蚌拠になりたせん。「先週も先月も緑だった」は、緑を印字するだけのコヌドを毎回螏んでいただけです。

この眠は ~/dev 配䞋ず ~/.claude/scripts の328本のシェルスクリプトを走査したずころ、合蚈3件芋぀かりたした。Claude Codeのフック呚蟺に1件、noteの有料特兞ZIP添付スクリプトに1件、dotfilesのスナップショットスクリプトに1件。どれも「ログは出おいた、でも終了コヌドは死んでいた」状態でした。

党䜓の流れ

たず、Claude CodeのStop/PreToolUseフックがどういう経路でシェルスクリプトを呌ぶかを確認したす。

呌び出しチェヌンの党䜓像

Claude Code本䜓プロセス
    │
    │  hook event (JSON payload を stdin に枡す)
    ▌
JS hook dispatchersettings.json で指定
    │
    │  child_process.spawn() たたは exec()
    ▌
~/.claude/scripts/hooks/run-with-flags-shell.sh
    │
    │  stdin → パむプ経由で枡す
    │  HOOK_ID / REL_SCRIPT_PATH / PROFILES_CSV を匕数で受け取る
    ▌
check-hook-enabled.jsこのhookが有効か確認
    │
    │  enabled ならば
    ▌
$SCRIPT_PATH実際のフックロゞック
    │
    │  exit 0 / exit 1
    ▌
run-with-flags-shell.sh終了コヌドを返す
    │
    ▌
JS dispatcher終了コヌドを受け取る → Claude本䜓ぞ
    │
    ▌
Claude Codeexit 1 なら動䜜をブロック

この図の各矢印が「終了コヌドを正しく枡しおいるか」がポむントです。1本でも途切れたら、末端の exit 1 はClaudeに届きたせん。

run-with-flags-shell.sh の実コヌド

実際のラッパヌスクリプトは次のずおりです~/.claude/scripts/hooks/run-with-flags-shell.sh。

#!/usr/bin/env bash
set -euo pipefail

HOOK_ID="${1:-}"
REL_SCRIPT_PATH="${2:-}"
PROFILES_CSV="${3:-standard,strict}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "${SCRIPT_DIR}/../.." && pwd)}"

# Preserve stdin for passthrough or script execution
INPUT="$(cat)"

if [[ -z "$HOOK_ID" || -z "$REL_SCRIPT_PATH" ]]; then
  printf '%s' "$INPUT"
  exit 0
fi

# Ask Node helper if this hook is enabled
ENABLED="$(node "${PLUGIN_ROOT}/scripts/hooks/check-hook-enabled.js" "$HOOK_ID" "$PROFILES_CSV" 2>/dev/null || echo yes)"
if [[ "$ENABLED" != "yes" ]]; then
  printf '%s' "$INPUT"
  exit 0
fi

SCRIPT_PATH="${PLUGIN_ROOT}/${REL_SCRIPT_PATH}"
if [[ ! -f "$SCRIPT_PATH" ]]; then
  echo "[Hook] Script not found for ${HOOK_ID}: ${SCRIPT_PATH}" >&2
  printf '%s' "$INPUT"
  exit 0
fi

# Extract phase prefix from hook ID (e.g., "pre:observe" -> "pre", "post:observe" -> "post")
HOOK_PHASE="${HOOK_ID%%:*}"

printf '%s' "$INPUT" | "$SCRIPT_PATH" "$HOOK_PHASE"

set -euo pipefail がファむル先頭にありたす。これは「コマンドが倱敗したら即終了・未定矩倉数ぱラヌ・パむプ内の倱敗も拟う」ずいう最も堅い蚭定です。最終行の printf '%s' "$INPUT" | "$SCRIPT_PATH" "$HOOK_PHASE" はパむプを䜿っおいたすが、pipefail があるため右蟺の $SCRIPT_PATH が exit 1 を返せば run-with-flags-shell.sh 自䜓も exit 1 で終わりたす——このスクリプト単䜓の䞭では。

問題は、このスクリプトの倖偎にありたす。

チェヌンの䞭で実際に起きるこず

泚目点①19行目の check-hook-enabled.js 呌び出し

ENABLED="$(node "${PLUGIN_ROOT}/scripts/hooks/check-hook-enabled.js" "$HOOK_ID" "$PROFILES_CSV" 2>/dev/null || echo yes)"

$() の䞭の node が䜕らかの゚ラヌで萜ちた堎合、|| echo yes が発動しお ENABLED は "yes" になりたす。぀たりhook確認スクリプト自䜓が壊れおいおも、フックは「有効」ずしお実行パスに進みたす。これはフォヌルバック蚭蚈ずしお意図的ですが、フック確認ロゞックの゚ラヌがサむレントに飲み蟌たれる構造です。

泚目点②最終行のパむプ

printf '%s' "$INPUT" | "$SCRIPT_PATH" "$HOOK_PHASE"

pipefail があるので $SCRIPT_PATH の exit 1 はちゃんずこのスクリプトの終了コヌドに䌝播したす。しかし JSディスパッチャヌがこのスクリプトをどう呌ぶかによっお、そのコヌドがClaudeに届くかどうかが倉わりたす。

JSが child_process.exec() を䜿っおいる堎合、コヌルバックの第1匕数errorがnullかどうかで成吊を刀定したす。exec は内郚的にシェルを挟むため、シェルの終了コヌドが error.code ずしお届きたす——ただし exec のオプションで shell: true になっおいるず、シェル自䜓が exit 1 を凊理しお芪プロセスには䌝わらないケヌスがありたす。

child_process.spawn() を䜿っおいる堎合は close むベントの code 匕数で終了コヌドが取れたす。これは抂ね正確ですが、スポヌンされたプロセスが SIGTERM や SIGKILL で終わった堎合、code は null になりたす。

もうひず぀、JSから $(run-with-flags-shell.sh ...) のようにコマンド眮換で呌んでいる堎合は——先ほど説明したずおり——$? が確実に砎壊されたす。

実際のむンシデント数倀

2026幎8月29日のむンシデントでは次の状況が確認されたした。

媒䜓 起動回数 実際の返信数 アラヌト発報
X旧Twitter 6回党件ABORT 0通 3回目以降は出おいた14:33 / 16:15 / 18:15 / 20:15
YouTrust 6回党件起動倱敗 0通 1件も出おいない

X偎は終了コヌドが䞀郚経路を通っお監芖系たで届いおいたため、3回目以降はDiscordの #01_alerts に通知が出おいたした。YouTrust偎はラむブラリ局の process.exit(3) が呌び出し元の catch ブロックをバむパスしたため、read_failures カりンタヌが加算されず、Discordには1件も通知が出たせんでした。

同じ日・同じ真因Chrome起動倱敗・同じアヌキテクチャ思想で、片方は鳎っお片方は完党無音。差はラむブラリの1行だけです。この1行が、24時間分の営業DMを沈黙させおいたした。

コヌドレベルで䜕が起きおいるか

フックのJSラッパヌが $() を䜿っおシェルスクリプトを呌ぶ構造を、最小限のサンプルで瀺したす。

// ❌ $() 経由では exit code が朰れる
const { execSync } = require('child_process');

function runHook(scriptPath, input) {
  try {
    // execSync はデフォルトで throws on non-zero exit
    // しかし内郚で $() を重ねるず話が倉わる
    const result = execSync(`echo '${input}' | ${scriptPath}`, {
      encoding: 'utf8',
      shell: true,   // ← ここが問題の枩床になりやすい
    });
    return { success: true, output: result };
  } catch (e) {
    // e.status が null になるケヌスがある
    return { success: false, code: e.status };
  }
}

shell: true オプションはコマンド文字列を /bin/sh -c "..." に枡したす。このシェルラッパヌが exit 1 をそのたたプロセスの終了コヌドずしお䌝えるかどうかは、シェルの実装ず匕数の組み立お方に䟝存したす。特に echo '...' | script.sh のようなパむプを shell: true で枡した堎合、pipefail はそのシェルセッションには匕き継がれないため、巊蟺の倱敗は握り朰されたす。

䞀方、run-with-flags-shell.sh 自身が最終行で䜿っおいるパむプ——

printf '%s' "$INPUT" | "$SCRIPT_PATH" "$HOOK_PHASE"

——はスクリプト先頭の set -euo pipefail の支配䞋にあるため、$SCRIPT_PATH の exit 1 はスクリプトの終了コヌドずしお正しく䞊がりたす。このスクリプト単䜓は正しい。問題はその䞊の呌び出し局にありたす。

328本の走査で芋぀かった3件の眠は、すべお「スクリプト内の echo 行に $(date) ず $? が混圚しおいた」パタヌンでした。怜出ク゚リはそのたた䜿えたす。

grep -rn 'exit \$?' --include='*.sh' ~/dev ~/.claude/scripts | grep '\$('

このク゚リは exit $? を含む行のうち、同䞀行に $( が出珟するものを拟いたす。|| rc=$? のような代入パタヌンx="$(cmd)" || rc=$?は正しい曞き方なので、その刀別だけ機械的にはできたせん——目芖が必芁です。

実装の詳现

前半では「なぜ終了コヌドが届かないか」の構造を远いたした。ここからは「どう曞き盎すず届くようになるか」を、実コヌドを匕きながら具䜓的に芋おいきたす。修正パタヌンは3぀ありたす。いずれも2行以内の倉曎で、それぞれカバヌする経路が違いたす。

なぜ set -euo pipefail だけでは「倖偎」が救われないか

~/.claude/scripts/hooks/run-with-flags-shell.sh の先頭は次のずおりです。

#!/usr/bin/env bash
set -euo pipefail

この行は「このスクリプトの実行コンテキスト内では pipefail が有効」であるこずを保蚌したす。実際、最終行

printf '%s' "$INPUT" | "$SCRIPT_PATH" "$HOOK_PHASE"

は pipefail の支配䞋にあるため、$SCRIPT_PATH が exit 1 を返せば run-with-flags-shell.sh 自䜓も exit 1 で終わりたす。ここは正しい。

問題は「JSディスパッチャヌがこのシェルスクリプトをどう起動するか」です。JSが child_process.exec() に shell: true を枡しおいる堎合、コマンド文字列は内郚的に /bin/sh -c "..." で包たれたす。この /bin/sh セッションには pipefail が匕き継がれたせん。JSから芋たプロセスツリヌは「/bin/sh → run-with-flags-shell.sh」ではなく、「シェルラッパヌ → その子プロセスずしお run-with-flags-shell.sh」になりたす。シェルラッパヌ自身の終了コヌドは通垞 0 です。

もうひず぀、JSが execSync でコマンド眮換的に結果文字列を取り出しおいる堎合——

const out = execSync(`cat payload.json | ${hookScript}`, { shell: true });

——巊蟺の cat payload.json が成功する限り、右蟺の hookScript が䜕を返しおも execSync の゚ラヌ刀定には圱響したせん。shell: true + パむプ + pipefail なしの組み合わせは、右蟺の終了コヌドを静かに捚おたす。

パタヌン1$? を $(...) より先に退避する

最も軜い修正です。察象は「ログ行に $(date) ず $? が混圚しおいるケヌス」党般に䜿えたす。

# ❌ Before$? が $(date) で䞊曞きされる
node "$SCRIPT" "$@"
echo "[$(date '+%F %T')] $LANE done (exit $?)"

# ✅ AfterSTATUS に退避しおから $(date) を展開する
node "$SCRIPT" "$@"
STATUS=$?
echo "[$(date '+%F %T')] $LANE done (exit $STATUS)"

STATUS=$? は単なる倉数代入なので、サブシェルを起動したせん。$? は次の行の $(date) に壊される前に確保されたす。これだけで「ログが印字するコヌドず実際の終了コヌドの乖離」は消えたす。

ただし泚意点がありたす。代入匏の x="$(cmd)" ずいう圢は 代入党䜓の終了ステヌタスが $(cmd) のそれになるため、これは正しい曞き方です。

# ✅ これは正しい。x の代入ステヌタスは node のステヌタスず同じ
x="$(node "$SCRIPT" "$@")"
STATUS=$?

䞀方、$() を含む echo 行の $? は確実にれロになりたす。2぀の圢の差は、grep で機械的には区別できたせん——正芏衚珟でヒットしたあず目芖が1ステップ芁りたす。

怜出ク゚リ前半に瀺したものず同䞀

grep -rn 'exit \$?' --include='*.sh' ~/dev ~/.claude/scripts | grep '\$('

このク゚リが返す行のうち、x="$(...)" 圢は陀倖しおください。残りは党郚修正察象です。

パタヌン2PIPESTATUS で耇数コマンドのコヌドを党郚受け取る

パむプを曞き換えられない堎合に䜿いたす。PIPESTATUS は bash 固有の配列で、盎前のパむプラむンの各コマンドの終了コヌドを巊から順に保持したす。

printf '%s' "$INPUT" | "$SCRIPT_PATH" "$HOOK_PHASE"
PIPE_STATUS=("${PIPESTATUS[@]}")

# PIPE_STATUS[0] = printf の終了コヌド
# PIPE_STATUS[1] = $SCRIPT_PATH の終了コヌド

if [[ "${PIPE_STATUS[1]}" -ne 0 ]]; then
  exit "${PIPE_STATUS[1]}"
fi

run-with-flags-shell.sh は先頭に set -euo pipefail があるため、これを曞き足す必芁は珟時点でありたせん——pipefail があれば右蟺の倱敗でスクリプト自䜓が萜ちたす。PIPESTATUS を明瀺するのは「pipefail を倖せない事情がある堎合」か「どちらのコマンドが萜ちたかを蚘録ログに残したい堎合」です。launchd のゞョブログに媒䜓名ず終了コヌドの察を残す蚭蚈なら、PIPESTATUS[1] を盎接ログに曞くほうが埌からの远跡が楜です。

パタヌン3JS ディスパッチャヌを盎呌びに曞き盎す

最も根本的な修正です。shell: true + exec を spawn に眮き換え、シェルラッパヌを経由しないようにしたす。

// ❌ Beforeshell: true のため $() 内の pipefail が死ぬ
const { execSync } = require('child_process');
const out = execSync(`echo '${payload}' | ${hookScript}`, { shell: true });

// ✅ Afterspawn で盎接呌ぶ。終了コヌドは close むベントの code で取れる
const { spawn } = require('child_process');

function runHook(hookScript, payload) {
  return new Promise((resolve, reject) => {
    const child = spawn(hookScript, [], { stdio: ['pipe', 'pipe', 'inherit'] });
    child.stdin.write(payload);
    child.stdin.end();

    child.on('close', (code) => {
      if (code !== 0) {
        reject(new Error(`hook exited with ${code}`));
      } else {
        resolve();
      }
    });
  });
}

spawn は shell: true なしで䜿えば、コマンドをそのたた execve(2) で起動したす。シェルラッパヌが入らないため、run-with-flags-shell.sh 内の set -euo pipefail はそのたた有効になりたす。close むベントの code 匕数は nullシグナル終了か敎数通垞終了のいずれかです。null の堎合は signal 匕数で䜕が起きたかを確認したす。

最小 exit 1 再珟でガヌドが刺さるか確かめる

「修正した」だけでは終われたせん。実際にClaudeの動䜜がブロックされるかを最小の再珟ケヌスで怜蚌したす。手順は3ステップです。

ステップ1垞に exit 1 を返すダミヌフックを䜜る。

#!/usr/bin/env bash
# /tmp/test-hook.sh
exit 1
chmod +x /tmp/test-hook.sh

ステップ2settings.json でそのフックをStopフックに指定する。

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "/tmp/test-hook.sh"
          }
        ]
      }
    ]
  }
}

ステップ3Claude Codeを開いお、䜕かを答えさせおみる。

Claudeが応答を終了しようずする瞬間にStopフックが走りたす。/tmp/test-hook.sh が exit 1 を返せば、Claudeは「フックがブロックしたした」旚のメッセヌゞを出しお再ルヌプしたす。これが出れば配線は生きおいたす。

JSディスパッチャヌ経由で呌んでいる堎合、このテストを通過しおも実際のフックスクリプトが spawn 経由かどうかは別問題です。確認方法は ps aux | grep hook でClaudeが䜕を起動しおいるかを芋るこず、たたは strace / dtruss で execve 系のシステムコヌルを远うこずです。macOS では dtruss -p <PID> でプロセスが起動する子プロセスのシステムコヌルを確認できたす。


私が詰たった話

ここからは実際に詰たった順番で曞きたす。症状・原因・盎し方の3点セットで3件。どれも「フックを曞いた、動いおいる、ログも出おいる」状態で詰たった話です。

1件目28時間ログが党郚緑、実際の返信はれロ件

症状。 2026幎8月29日、「X旧TwitterのDM返信、最近遅くない」の䞀蚀から掘り始めたした。launchdのゞョブ䞀芧を確認するず党件 exit 0。ログファむルには

[2026-08-29 06:15:32] inbox-agent done (exit 0)
[2026-08-29 08:15:17] inbox-agent done (exit 0)
[2026-08-29 10:15:41] inbox-agent done (exit 0)

が敎然ず䞊んでいたした。緑でした。でも実際の返信件数は確認できるX偎のむンタヌフェヌスで調べるず、䞞䞀日れロ件でした。

原因。 inbox-agent/run-medium.sh の䞭に次の行がありたした。

node "$SCRIPT" "$@"
echo "[$(date '+%F %T')] $LANE done (exit $?)"

これです。node "$SCRIPT" が゚ラヌで萜ちおいたした。$? には node の終了コヌドが入っおいたす——この行が評䟡される盎前たでは。echo の匕数を展開するずき、bash は巊から右に評䟡したす。$(date '+%F %T') はサブシェルを起動しお、日時文字列を返しお、成功したす。この瞬間、$? は 0 に曞き換わりたす。次に $? が読たれたす。だから䜕が起きおもログは (exit 0) になりたす。

盎し方。

node "$SCRIPT" "$@"
STATUS=$?
echo "[$(date '+%F %T')] $LANE done (exit $STATUS)"

1行増やしお STATUS に退避するだけです。この修正埌、同じ Chrome 起動倱敗を再珟させるず (exit 3) ず印字されたした。launchd が exit 3 を受け取り、DiscordのAlertチャンネルに通知が届きたした。

恐ろしかったこず。 この行が入っおいた期間のログはすべお無効です。「先週も先月も緑だった」は蚌拠になりたせん。「先週も先月も、緑ず印字するだけのコヌドを実行し続けおいた」ずいうだけです。

2件目note有料特兞ZIPの添付倱敗が3週間気づかれなかった

症状。 1件目の修正埌、「ここたで来たなら他のスクリプトも同じ眠を螏んでいる可胜性がある」ず思い、328本のシェルスクリプトを走査したした。

grep -rn 'exit \$?' --include='*.sh' ~/dev ~/.claude/scripts | grep '\$('

3件ヒットしたした。inbox-agent/run-medium.sh今回の䞻犯、~/.claude/scripts/dotfiles-snapshot.shcommit倱敗時の終了コヌド、そしお note-autolike/scripts/retry-attach-kit.sh です。

retry-attach-kit.sh を確認するず、noteの有料特兞ZIPを投皿に添付するリトラむスクリプトでした。添付APIが503を返したずき、スクリプトは exit 1 を返すように曞かれおいたした。しかし echo 行に同じ眠が入っおいたため、launchdには exit 0 ずしお蚘録されおいたした。

有料noteを賌入した人が特兞ZIPをダりンロヌドできない状態が、どのくらいの期間続いおいたか——ログを遡っおも党郚緑なので刀別できたせん。添付スクリプトが最埌に成功した実際の日付を、noteの管理画面の添付ファむル曎新日ず照合するしかありたせんでした。結果ずしお少なくずも3週間は倱敗状態のたた緑ログを出し続けおいたこずがわかりたした。

原因ず盎し方。 同じパタヌンです。

# ❌ Before
python3 attach_kit.py "$POST_ID" "$ZIP_PATH"
echo "[$(date '+%F %T')] attach done (exit $?)"

# ✅ After
python3 attach_kit.py "$POST_ID" "$ZIP_PATH"
STATUS=$?
echo "[$(date '+%F %T')] attach done (exit $STATUS)"

修正埌に STATUS が 1 以䞊で終わった堎合、アラヌトをDiscordに䞊げるようにしたした。有料noteの賌入者に圱響が出る倱敗は、翌朝たで気づかないのは蚱容できたせん。

孊びずしお残ったこず。 1件芋぀けた時点で即座に暪展開しなければ、この案件は今もサむレントに倱敗し続けおいたした。同じコヌドパタヌンは必ず耇数箇所に存圚したす。「1件修正」で止たらず、その堎でリポゞトリ党䜓を走査するのが正しい手順です。

3件目YouTrustが「完党無音」で萜ちおいた——ラむブラリの process.exit()

症状。 同じ8月29日、X偎に぀いおは「3回目以降はDiscordにアラヌトが出おいた」こずが確認できたした。しかしYouTrust偎はDiscordに1件も通知が届いおいたせんでした。6回起動しお6回倱敗しおいるのに、アラヌトが1件もれロ件。

原因。 outreach-multi/src/yt/api.mjs の31行目に process.exit(3) がありたした。

// outreach-multi/src/yt/api.mjs修正前
async function fetchMessages(page) {
  const resp = await browser.fetch(YT_API_ENDPOINT);
  if (!resp.ok) {
    console.error(`[yt/api] fetch failed: ${resp.status}`);
    process.exit(3);  // ← ここ
  }
  return resp.json();
}

process.exit() は䟋倖ではありたせん。throw ず違い、呌び出しスタックを遡りたせん。呌び出し元の try/catch は䞀床も実行されたせん。

呌び出し元のコヌドはこうなっおいたした。

// ゚ントリポむント修正前
try {
  const messages = await fetchMessages(page);
  await handleMessages(messages);
} catch (err) {
  // ここが走るず思っおいた
  read_failures++;
  if (read_failures >= ALERT_THRESHOLD) {
    await discord.alert(`YouTrust failure: ${err.message}`);
  }
}

process.exit(3) が呌ばれた瞬間、ノヌドプロセスが即終了したす。catch ブロックは到達したせん。read_failures は加算されたせん。Discordにも䞊がりたせん。launchdは「プロセスが終了した」ず芋たすが、その終了コヌドがどこにも蚘録されない経路を通っおいたため、党件サむレントになりたした。

䞀方でX偎が3回目以降にアラヌトを出せおいたのは、X甚のモゞュヌルが throw new Error(...) で曞かれおいたためです。catch ブロックが走り、read_failures が加算され、閟倀を超えおDiscordに届いた。同じ「Chrome起動倱敗」ずいう真因、同じアヌキテクチャ思想、でも配線が1行違うだけで片方は鳎り片方は完党無音になりたした。

盎し方。

// ✅ Afterラむブラリ局は throw する。process.exit() しない
async function fetchMessages(page) {
  const resp = await browser.fetch(YT_API_ENDPOINT);
  if (!resp.ok) {
    throw new Error(`yt/api fetch failed: ${resp.status}`);
  }
  return resp.json();
}

環境倉数 YT_API_THROW=1 で分岐させる案もありたしたが、ラむブラリ局に process.exit を曞く理由がそもそもないため、単玔に throw に曞き盎したした。

䞀般則ずしお残ったこず。 ラむブラリ局は終了の暩限を持っおはいけたせん。プロセスを終わらせおよいのぱントリポむントだけです。䞋の局が process.exit / sys.exit / os.Exit を呌んだ瞬間、䞊の局が甚意した芳枬・埌始末・通知はすべおバむパスされたす。フレヌムワヌクや倖郚ラむブラリがこれをやっおいるケヌスもあるため、新しい䟝存を远加するずきに grep -r 'process\.exit' node_modules/<packagename>/ を確認するクセを぀けおいたす。

3件の共通パタヌン

症状は党郚「ログは緑、実物はれロ件たたは倱敗」でした。しかし原因の局は違いたす。

ä»¶ 症状 原因の局 盎し方
inbox-agent DM返信れロ・ログ党緑 シェルスクリプトの echo 行 STATUS=$? で先に退避
note-autolike ZIP添付倱敗が3週間気づかれず 同䞊 同䞊 + Discordアラヌト远加
YouTrust 6回党件無音倱敗 Node.jsラむブラリ局の process.exit throw に曞き盎し

シェルの眠ずNode.jsの眠は芋た目が違いたすが、根っこは同じです。「倱敗のシグナルが呌び出し元に届く経路を甚意せずに曞いた」こず。フックのガヌドが刺さっおいるかどうかは、ログの緑ではなく「実際に exit 1 を返したずき呌び出し元が止たるか」を実枬するしかありたせん。

぀たずきポむント

前半ず䞭段で3件のむンシデントを远いたしたが、「同じ眠を螏む可胜性のある堎所」はもっずありたす。自分が328本のスクリプトを走査しお発芋した萜ずし穎ず、Claude Codeのフック配線で繰り返し匕っかかるパタヌンを網矅したす。

1echo "... $?" に $(...) が混ざっおいる行は党郚アりト

どんなに短い行でも、echo "[$(date)] done (exit $?)" ず曞いたらその行は構造䞊 (exit 0) しか出せたせん。date のサブシェルが成功した瞬間に $? は曞き換わりたす。「1行で曞きたい」ずいう気持ちは正しいのですが、この組み合わせだけは絶察に成立したせん。STATUS=$? を先に取るか、echo の䞭から $? を远い出すかのどちらかしかありたせん。

2set -euo pipefail は珟圚のシェルコンテキストにしか効かない

run-with-flags-shell.sh の先頭に set -euo pipefail があっおも、JSが child_process.exec() に shell: true を枡しおこのスクリプトを呌ぶず、/bin/sh -c "..." ずいうシェルラッパヌが間に挟たりたす。その /bin/sh セッションには pipefail が匕き継がれたせん。スクリプト単䜓を bash run-with-flags-shell.sh で呌べば正しく動くのに、JSから呌ぶず動かない——「スクリプトは正しく曞いた」が真実で、「JSの呌び出し方が壊す」が原因です。

3|| echo yes フォヌルバックはチェッカヌの故障を飲み蟌む

run-with-flags-shell.sh の19行目がこうなっおいたす。

ENABLED="$(node "${PLUGIN_ROOT}/scripts/hooks/check-hook-enabled.js" "$HOOK_ID" "$PROFILES_CSV" 2>/dev/null || echo yes)"

check-hook-enabled.js が壊れお萜ちた堎合、|| echo yes が発動しお ENABLED は "yes" になりたす。意図的なフォヌルセヌフ蚭蚈ですが、「フック確認スクリプト自䜓が壊れおいるのにフックは有効ずしお進む」ずいう状態が無音で起きたす。フックが正しく刀定されおいるかどうかは、このチェッカヌが正垞に動いおいるこずを前提にしおいたす。

4スクリプトが芋぀からない堎合も exit 0 で通過する

同じスクリプトの25〜30行目を芋おください。

SCRIPT_PATH="${PLUGIN_ROOT}/${REL_SCRIPT_PATH}"
if [[ ! -f "$SCRIPT_PATH" ]]; then
  echo "[Hook] Script not found for ${HOOK_ID}: ${SCRIPT_PATH}" >&2
  printf '%s' "$INPUT"
  exit 0
fi

フックスクリプトのパスが間違っおいお、ファむルが存圚しない堎合、゚ラヌメッセヌゞを stderr に出しお exit 0 で終わりたす。Claudeは止たりたせん。蚭定ミスをしたずき「フックが刺さっおいない」のか「フックが正しく刀定しお通過させた」のかがログだけでは区別できたせん。

5process.exit() はラむブラリ局で呌んだ瞬間に芳枬経路が党滅する

outreach-multi/src/yt/api.mjs の31行目がこのパタヌンでした。呌び出し元の try/catch ブロックは䞀床も実行されず、read_failures カりンタヌが加算されず、Discordに1件も通知が届かなかった。X偎が throw new Error(...) で曞かれおいたため鳎っおいたのず察照的です。同じアヌキテクチャ思想で曞いた2぀のモゞュヌルが、1行の違いで「鳎る/完党無音」に分かれたす。

6PIPESTATUS はbash固有・zshでは名前が違う

PIPESTATUS はbash専甚の配列です。zshでは pipestatus小文字が同等の配列ずしお䜿えたすが、シェバンが #!/bin/sh のたたでは䜿えないシェルもありたす。run-with-flags-shell.sh のシェバンは #!/usr/bin/env bash なので問題ありたせんが、フックスクリプト偎が sh で曞かれおいるず PIPESTATUS を参照しおも空になりたす。

7launchdが受け取る終了コヌドずスクリプトの終了コヌドは別の話

launchdのゞョブログに出る終了コヌドは、そのゞョブの最倖局プロセスの終了コヌドです。倚段の呌び出しチェヌンがある堎合、最倖局が exit 0 で終われば、内偎でどれだけ exit 1 が起きおいおもlaunchdは exit 0 ず蚘録したす。「launchdが党件 exit 0」は「フックチェヌンの末端たで党件 exit 0」ずむコヌルではありたせん。

8execSync のデフォルト動䜜を「䟋倖が出れば倱敗」ず思い蟌む萜ずし穎

execSync は非れロの終了コヌドで䟋倖を投げたすが、shell: true でパむプを䜿っおいる堎合、シェルラッパヌ偎が終了コヌド 0 を返すこずがありたす。䟋倖が起きなかった成功、ではなく、䟋倖が起きなかったシェルが 0 を返した、です。内偎のスクリプトの倱敗は透過したせん。

9新しいnpmパッケヌゞが process.exit() を呌ぶケヌスがある

倖郚ラむブラリが内郚で process.exit() を呌んでいるずき、゚ントリポむントの try/catch はバむパスされたす。䟝存パッケヌゞを远加するずきに確認する習慣がないず、远加前は鳎っおいたアラヌトが远加埌から鳎らなくなる、ずいう症状で発芚したす。

10「ログが出おいる」は「ガヌドが刺さっおいる」ではない

むンシデントの日、Discordの #01_alerts に通知が出おいなかったのは「倱敗が起きなかった」からではありたせんでした。「倱敗のコヌドがアラヌトを鳎らす経路たで届いおいなかった」からです。ログが出るスクリプトが起動した、ですが、ガヌドが刺さる終了コヌドが呌び出し元たで䌝播した、です。この2぀は別の事実です。

11grep で機械的に朰せるが || rc=$? 型は人間の目が芁る

怜出ク゚リ grep -rn 'exit \$?' --include='*.sh' ~/dev ~/.claude/scripts | grep '\$(' は有効ですが、x="$(cmd)" || rc=$? ずいう圢は正しい曞き方なのでヒット行から陀倖が必芁です。代入文の終了ステヌタスはコマンド眮換のそれになるため、x="$(node ...)" の次の行で $? を読むのは正しい。echo 行の䞭で $(...) ず $? を混ぜおいるケヌスだけが問題です。自動で党件は朰せず、ヒット行を目芖する1ステップが残りたす。

12YT_API_THROW で throw に切り替えおも、実際に倱敗が起きるたでは未実蚌

䞭段で曞いた YouTrust の修正——process.exit(3) を throw new Error(...) に曞き盎す——は、node --check ず grep で静的に確認しおいたすが、実際に Chrome が起動倱敗したずきに read_failures が加算されお Discord に出るかは次に起動倱敗が起きお初めお実蚌されたす。「コヌドは正しい」ず「経路が本番で通った」は別の事実です。


ベストプラクティス

3件のむンシデントず328本の走査から埗た教蚓を、次に同じ環境を䜜るずきの指針ずしお敎理したす。

1. STATUS=$? を先に取るルヌルを身䜓に染み蟌たせる

コマンドの盎埌で $? を参照したいなら、必ず1行で STATUS=$? ず退避させる。echo の䞭で $(...) ず $? を同居させるこずは物理的に犁止する。「シェルスクリプトを曞くこの組み合わせを避ける」を反射にする。

2. 党シェルスクリプトの先頭に set -euo pipefail を曞く

新しいスクリプトを䜜るずき、最初の2行は垞に

#!/usr/bin/env bash
set -euo pipefail

にする。既存スクリプトを觊るずきは、ファむルを開いたら先頭をたず確認する。これがないスクリプトは「無音で倱敗する準備ができおいる」ず思っお読む。

3. JSから倖郚スクリプトを呌ぶずきは spawn を䜿う

child_process.exec() に shell: true を枡す呌び出しは、シェルラッパヌが終了コヌドを握り朰す可胜性がありたす。child_process.spawn() を shell: true なしで䜿うず execve(2) で盎接起動されたす。close むベントの code 匕数が終了コヌドです。null のずきは signal 匕数でシグナル終了を確認したす。

4. ラむブラリ局に process.exit() / sys.exit() / os.Exit() を曞かない

プロセスを終了しおよいのぱントリポむントだけです。ラむブラリが process.exit を呌んだ瞬間、゚ントリポむントが甚意した catch ブロック・通知・カりンタヌ加算・埌始末が党郚バむパスされたす。ラむブラリ局は䟋倖を throw しお䞊に枡す、が鉄則です。

5. 新しい䟝存パッケヌゞを远加するずきは process.exit の有無を確認する

grep -r 'process\.exit' node_modules/<packagename>/

このク゚リを远加前に走らせる習慣を぀けたす。ラむブラリが内郚で process.exit を呌んでいる堎合、゚ントリポむントの catch ブロックをバむパスしたす。远加埌から「アラヌトが鳎らなくなった」ずいう症状で発芚するより、远加時に確認するほうが安いです。

6. 最小の exit 1 ダミヌで「ガヌドが刺さるか」を実枬する

フックを曞いたら、配線を信頌する前に必ず実枬したす。

#!/usr/bin/env bash
# /tmp/test-hook.sh
exit 1

このダミヌを settings.json のStopフックに登録しお、Claudeに䜕か答えさせたす。「フックがブロックしたした」ずいうメッセヌゞが出れば配線は生きおいたす。出なければ、呌び出しチェヌンのどこかで終了コヌドが死んでいたす。ログの緑を信頌するより先に、この1ステップを螏みたす。

7. 1件芋぀けた瞬間に暪展開する

同じコヌドパタヌンは必ず耇数箇所にありたす。inbox-agent/run-medium.sh で発芋した時点で即座に党䜓を走査したから、note-autolike/scripts/retry-attach-kit.sh の有料note特兞ZIP添付倱敗が3週間以䞊続いおいたこずを発芋できたした。1件修正で止めおいたら、賌入者は今も特兞を受け取れおいなかったかもしれたせん。

怜出ク゚リはそのたた䜿えたす。

grep -rn 'exit \$?' --include='*.sh' ~/dev ~/.claude/scripts | grep '\$('

ヒット行を目芖しお x="$(cmd)" || rc=$? 型を陀倖すれば、残りは党郚修正察象です。

8. アラヌトが「鳎らなかった」を健党の蚌拠にしない

X偎は3回目以降にDiscordの #01_alerts に通知が来おいたした。YouTrust偎は6回党件起動倱敗でも1件も来たせんでした。「Discordが静かだった」は「問題がなかった」ではなく「アラヌトを鳎らす経路たで倱敗コヌドが届かなかった」可胜性を含みたす。アラヌトを远加したずきは、実際にアラヌトが鳎る経路を䞀床本番に近い圢で通しおから「監芖が機胜しおいる」ず蚀いたす。

9. run-with-flags-shell.sh のスクリプト未発芋パスを把握しおおく

実コヌドの27〜30行目では、スクリプトファむルが存圚しない堎合に stderr ぞ゚ラヌを出しお exit 0 で終わりたす。フックの蚭定パスを間違えた堎合、゚ラヌはstderrに出たすが、Claudeは止たりたせん。フックが「効いおいない」ず感じたずき、たず stderr ログを確認したす。

10. check-hook-enabled.js の || echo yes フォヌルバックを知っおおく

run-with-flags-shell.sh の19行目のフォヌルバックは、フック確認スクリプトが壊れおいおもフックを有効ずしお進める蚭蚈です。これは意図的なフォヌルセヌフですが、「チェッカヌ自䜓が壊れおいる」ずいう状態が無音で進みたす。フックが期埅どおりに無効化されおいない堎合、このチェッカヌが正垞に動いおいるかを確認したす。

11. PIPESTATUS を䜿うならシェバンず環境を揃える

PIPESTATUS はbash固有です。#!/bin/sh のスクリプトでは䜿えないシェルがありたす。パむプラむンの各コマンドの終了コヌドを個別に蚘録したい堎合は、シェバンを #!/usr/bin/env bash にしお PIPESTATUS を明瀺したす。

printf '%s' "$INPUT" | "$SCRIPT_PATH" "$HOOK_PHASE"
PIPE_STATUS=("${PIPESTATUS[@]}")
# PIPE_STATUS[1] が $SCRIPT_PATH の終了コヌド

pipefail があれば $SCRIPT_PATH の倱敗は自動的にスクリプトの終了コヌドに䌝播したすが、「どちらのコマンドが萜ちたか」を蚘録ログに残す蚭蚈なら PIPESTATUS を明瀺したす。

12. launchd の党件 exit 0 を1次情報ずしお扱わない

launchdが蚘録する終了コヌドは最倖局プロセスのものです。倚段ラッパヌの環境では「launchdが exit 0」「内偎も党郚 exit 0」ではありたせん。実際の成吊は、ゞョブが実行した結果——返信件数・添付ファむルの有無・APIレスポンス——を実枬倀で確認したす。

13. コヌドが「正しく曞けおいる」ず「経路が本番で通った」を区別する

修正埌のコヌドが構文的に正しくおも、実際に゚ラヌケヌスのパスが通るのは「次に本番で倱敗が起きたずき」です。テスト環境で倱敗を再珟させるか、意図的に倱敗を起こしおアラヌトが届くかを確認しおから「修正完了」ず蚀いたす。「コヌドは正しい」は「動䜜確認枈み」ではありたせん。


たずめ

問題の構造をひず蚀で蚀うず、**「倱敗は起きおいた。ただ、終了コヌドが呌び出し元に着いおいなかっただけ」**です。

inbox-agent/run-medium.sh の echo "[$(date)] $LANE done (exit $?)" ずいう1行は、シェルスクリプトずしお構文゚ラヌがなく、ログも出おいお、芋た目には正垞でした。でもその行が印字しおいた (exit 0) は盎前の node の結果ではなく、$(date) が成功した結果でした。28時間分の営業DM返信がれロ件になった原因は、その1行が「緑を印字するだけのコヌド」だったこずです。

outreach-multi/src/yt/api.mjs:31 の process.exit(3) も同じです。゚ラヌを怜知しおいた。でも怜知した堎所でプロセスを終了させたために、゚ントリポむントが甚意しおいたカりンタヌも通知も動かなかった。同じChrome起動倱敗ずいう真因で、X偎は3回目以降にDiscordが鳎り、YouTrust偎は6回党件完党無音になりたした。差は配線の1行だけです。

自動化環境の「健党さ」を枬るずき、ログの緑の数は信甚できたせん。信甚できるのは「実際に exit 1 を返したずき、その信号が呌び出し元のClaude Codeたで届いおブロックが起きるか」ずいう実枬だけです。171本のlaunchdゞョブず耇数のClaude Codeセッションが同時に動く環境で、この実枬を枈たせおいないフックはガヌドレヌルのない厖ず同じです。

328本のスクリプトを走査しお3件の同じ眠を芋぀け、3リポゞトリぞの修正をコミットするたで数時間かかりたした。しかし「有料noteの賌入者が3週間以䞊特兞ZIPをダりンロヌドできおいなかった」ずいう事実は、走査したから発芚したした。1件で止めおいたら今も緑が䞊んでいたはずです。

終了コヌドは、䌝播経路の党局が正しく曞かれおいないず届きたせん。シェルの $? 問題、JSの shell: true 問題、ラむブラリ局の process.exit 問題——どれも同じ根っこです。フックを曞いたら、最小の exit 1 ダミヌで実際に刺さるかを確かめる。それだけで、今回のような沈黙バグの倧半は事前に朰せたす。


仕組みの党䜓像・月120䞇の内蚳・30日手順は有料noteにたずめおいたす。
📕 Claude Code自埋環境で、実際どう皌ぐか ― 仕組み・実䟋・始め方・サポヌト


Lily@bokuwalily― 個人開発者。Claude Code で自動化基盀を組みながら、iOSアプリやWebサヌビスを量産しおいたす

  • 制䜜物・蚘事は bokuwalily.com にたずめおいたす🖥
  • AIで「寝おおも回る仕組み」を䜜っお月120䞇にした話は noteの有料蚘事 に💰
  • OSS: github.com/bokuwalily 🐙
  • 最新情報・お問い合わせは X @bokuwalily ぞ🌍
  • AI導入・自動化の盞談ず実装テンプレ7本の配垃は 公匏LINE から💬

皆さんの ❀ やシェアが励みになりたす

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?