前回の autopilot 記事で launchd から Claude を無人で走らせる構成を紹介しました。今回はその裏側 ―― 無人ジョブが黙って止まっていた事実を、誰も気づかないまま放置してしまう問題とその対処の話です。
無人自動化の本当の怖さはバグではなく、サイレント失敗です。launchd のジョブが exit 78 でクラッシュしていても、hooks のスクリプトが実行権限を失っていても、agentmemory がポートを開いたまま worker 不在の「半生状態」で動いていても、表面上は何も起きていないように見えます。この状態が6日間続いて誰にも気づかれなかった実例が automation-health.sh のコメントに残っています。
困りごと:黙って死ぬ
自律ループを組むと監視対象が増えます。私の構成だけでも:
- launchd ジョブ(skill-harvest / skill-curate / agentmemory など)
- Stop hook / PreToolUse hook の実体スクリプト
- 会話ログを長期記憶に変換する Stop hook
- Obsidian Vault の自動更新
- 週次・月次バッチ(env-audit / plugin-purge / dotfiles-snapshot など)
これらが全部正常かどうかを手で確認する気にはなれません。かといって個別に launchctl list を叩いても「全体像」が見えない。
必要なのは 全自動化をまとめて点検して、GREEN / WARN / FAIL の3値で教えてくれる1コマンドです。
設計:3値判定と OK/WN/NG ヘルパ
~/.claude/scripts/automation-health.sh のコアはシンプルです。
fail=0; warn=0
ok() { printf " ${GRN}✓${RST} %s\n" "$1"; }
wn() { printf " ${YEL}⚠${RST} %s\n" "$1"; warn=$((warn+1)); }
ng() { printf " ${RED}✗${RST} %s\n" "$1"; fail=$((fail+1)); }
チェック項目を ok / wn / ng のいずれかで評価していき、最後にこう判定します。
if [ "$fail" -gt 0 ]; then
printf " ${RED}✗ FAIL${RST} red=%d warn=%d\n\n" "$fail" "$warn"; exit 1
elif [ "$warn" -gt 0 ]; then
printf " ${YEL}⚠ WARN${RST} warn=%d (致命的問題なし)\n\n" "$warn"; exit 0
else
printf " ${GRN}✓ ALL GREEN${RST} 全自動化が正常稼働\n\n"; exit 0
fi
- ALL GREEN:全項目 OK。exit 0
- WARN:致命的ではないが要注意。exit 0
- FAIL:RED が1つでもあれば exit 1
exit code を分けることで、daily-brief.sh や CI から || で後続処理を繋げられます。
チェック項目:何を見ているか
スクリプトは9節に分かれています。実コードから主要な判定を抜粋します。
1. launchd ジョブの生死
for job in com.shun.skill-harvest com.shun.skill-curate; do
line=$(launchctl list 2>/dev/null | grep -E "\b${job}\b")
if [ -z "$line" ]; then
ng "$job: 未ロード (launchctl load し直しが必要)"
else
exitc=$(echo "$line" | awk '{print $2}')
if [ "$exitc" = "0" ] || [ "$exitc" = "-" ]; then
ok "$job: ロード済 / last exit=$exitc"
else
ng "$job: last exit=$exitc (前回失敗)"
fi
fi
done
launchctl list の2列目が last exit code です。- は「まだ起動していない(正常)」、0 は成功、それ以外は失敗です。
2. hooks スクリプトの実在と実行権限
hooks=(pre_git_guard.sh pre_secrets_check.sh pre_env_guard.sh \
post_audit_log.sh post_format.sh post_tsc_check.sh \
stop_notify.sh user_prompt_submit.sh)
for h in "${hooks[@]}"; do
f="$CLAUDE/hooks/$h"
if [ ! -f "$f" ]; then ng "$h: 不在"
elif [ ! -x "$f" ]; then wn "$h: 実行権限なし (chmod +x 推奨)"
else ok "$h"
fi
done
settings.json に hook を書いても、実体スクリプトが chmod +x されていないと silently skip されます。これを WARN で検出します。
3. skill-harvest のログ鮮度
hlog="$CLAUDE/skills/auto/.harvest.log"
a=$(age_h "$hlog") # (now - mtime) / 3600 を返すヘルパ
if [ "$a" -ge 0 ] && [ "$a" -le 48 ]; then
ok "最終稼働 ${a}h前: ${last##*] }"
else
wn "最終稼働 ${a}h前 (>48h: cron停止の疑い): ${last##*] }"
fi
nskills=$(find "$CLAUDE/skills/auto" -maxdepth 2 -name SKILL.md | wc -l | tr -d ' ')
ok "生成済 auto-skill: ${nskills} 個"
ログの mtime が 48h 以上古ければ WARN。「ログがある」と「最近走った」は別の話なので mtime で見ます。
4. 会話ログの生存と INDEX.md の鮮度
cnt=$(find "$CONV" -name '*.md' ! -name 'INDEX.md' | wc -l | tr -d ' ')
ok "会話ログ ${cnt} 件 / 最新 ${la}h前"
ia=$(age_h "$idx")
if [ "$ia" -le 24 ]; then ok "INDEX.md 鮮度 ${ia}h前"
else wn "INDEX.md が ${ia}h前 (extract が更新していない可能性)"; fi
5. Obsidian Vault の自動更新マーカー
if [ -f "$hot" ] && grep -q "recent:start auto-updated" "$hot"; then
ha=$(age_h "$hot"); ok "hot.md 自動更新マーカー有 / ${ha}h前"
else wn "hot.md の自動更新マーカーが見つからない"; fi
# index.md カバレッジ: 実ファイル数 vs 記載ページ数
real=$(find "$VAULT" -name '*.md' -not -path '*/.*' | wc -l | tr -d ' ')
stated=$(grep -oE '総ページ数:[0-9]+' "$vidx" | grep -oE '[0-9]+' | head -1)
if [ "$stated" = "$real" ]; then ok "index.md カバレッジ一致 (${real}p)"
else wn "index.md 記載 ${stated:-?}p ≠ 実 ${real}p"; fi
6. remember 記憶層の重複バースト検知
for f in now.md recent.md archive.md; do
[ -f "$REMEMBER/$f" ] && ok "$f 存在" || wn "$f 不在 (必須層)"
done
dup=$(grep -vE '^\s*$|^##|^#' "$nowf" | sort | uniq -c | sort -rn | head -1 | awk '{print $1}')
if [ "${dup:-0}" -ge 3 ]; then
wn "now.md に同一要約が ${dup} 回 (consolidate 遅延の兆候)"
else
ok "now.md 重複なし (consolidate 健全)"
fi
now.md は consolidate が遅れると同じ要約が繰り返し追記されます。3回以上の重複を WARN で早期検知します。
7. ディスク使用量と残骸ファイル
cdir_mb=$(( ${cdir_total_mb:-0} - ${disabled_mb:-0} ))
if [ "${cdir_mb:-0}" -lt 2000 ]; then ok "~/.claude 実効 ${cdir_mb}MB"
elif [ "${cdir_mb:-0}" -lt 5000 ]; then wn "~/.claude 実効 ${cdir_mb}MB (>2GB: 大きめ)"
else ng "~/.claude 実効 ${cdir_mb}MB (>5GB: 異常肥大)"; fi
orphan=$(find "$CLAUDE" -maxdepth 1 -name 'security_warnings_state_*.json' -mtime +7 | wc -l | tr -d ' ')
if [ "${orphan:-0}" -ge 5 ]; then
wn "security_warnings_state_*.json が ${orphan} 個 (>7日前: backups/ へ退避推奨)"
fi
reversible cache(.disabled-cache)は実効容量から除外して判定します。
8. 週次・月次バッチのログ mtime
declare -a CRON_JOBS=(
"weekly cleanup-misc:$CLAUDE/logs/cleanup-misc.log:192"
"weekly env-audit:$CLAUDE/logs/env-audit-latest.md:192"
"monthly plugin-purge:$CLAUDE/logs/plugin-purge.log:744"
"weekly plugin-auto-disable:$CLAUDE/logs/plugin-auto-disable.log:192"
"weekly dotfiles-snapshot:$CLAUDE/logs/dotfiles-snapshot.log:192"
"weekly agents-index:$CLAUDE/logs/agents-index.log:192"
"daily plugin-usage:$CLAUDE/scripts/plugin-audit-latest.md:48"
)
for entry in "${CRON_JOBS[@]}"; do
IFS=":" read -r name path budget <<< "$entry"
a=$(age_h "$path")
if [ "$a" -le "$budget" ]; then ok "$name: ${a}h前 (budget ${budget}h)"
else wn "$name: ${a}h前 (budget ${budget}h 超過 — launchd 配送失敗の可能性)"; fi
done
週次ジョブなら 192h(8日)、月次なら 744h(31日)を budget として、それを超えたら WARN です。
agentmemory の「半生状態」問題
agentmemory サーバのチェックだけ、判定が2段階になっています。
am_line=$(launchctl list 2>/dev/null | awk '$3=="com.shun.agentmemory"{print $1" "$2}')
am_pid=${am_line%% *}; am_exit=${am_line##* }
if [ -z "$am_line" ]; then
wn "com.shun.agentmemory が launchd に未ロード"
elif [ "$am_pid" = "-" ] && [ "$am_exit" != "0" ]; then
ng "com.shun.agentmemory 停止中 (last exit=$am_exit) — クラッシュループの可能性"
else
am_code=$(curl -s -o /dev/null -w '%{http_code}' -m 3 \
http://localhost:3111/agentmemory/health 2>/dev/null || echo 000)
if [ "$am_code" = "200" ]; then ok "稼働中 (pid=$am_pid / health 200)"
elif [ "$am_code" = "000" ]; then ng "プロセスは居るが port 3111 無応答"
else ng "port 3111 は開くが /agentmemory/health=$am_code — worker 不在の半生状態"; fi
fi
コメントに理由が書いてあります。
launchd の exit 78 クラッシュループが6日間誰にも気づかれず、さらに「ポートは開くが worker 不在で全API 404」の半生状態は死活監視では見えない。
launchd の状態チェックだけでは「プロセスがいるが実際には動いていない」ケースを検出できません。curl で HTTP 200 を確認する理由はここにあります。
launchd の PID チェックと HTTP ヘルスエンドポイントの両方を見ないと、「起動しているように見えて全 API が 404」という半生状態を見逃します。MCP サーバや API サーバを launchd で動かすなら、必ず /health エンドポイントを叩く一手間を足してください。
cron ↔ launchd 二重実行の検知
見落としがちな項目が9番目の二重実行チェックです。
cron_sh=$(crontab -l 2>/dev/null | grep -vE '^[[:space:]]*#' | \
grep -oE '/[^ ]+\.sh' | xargs -n1 basename | sort -u)
launchd_sh=$(grep -hoE '/[^<> ]+\.sh' \
~/Library/LaunchAgents/com.shun.*.plist | xargs -n1 basename | sort -u)
dup=$(comm -12 <(printf '%s\n' "$cron_sh") <(printf '%s\n' "$launchd_sh") | \
grep -vE '^[[:space:]]*$')
if [ -n "$dup" ]; then
ng "cron と launchd の両方に登録され二重実行されるスクリプト: $(printf '%s' "$dup" | tr '\n' ' ')"
fi
コメントには「2026-06-01: cron→launchd 移行で cron を消し忘れて両方に登録=毎サイクル二重実行が発生」とあります。移行後に古い crontab を消し忘れたパターンです。
daily-brief との連携
daily-brief.sh は毎朝 8:00 に launchd から起動し、冒頭の環境ヘルスに1行だけ組み込みます。
echo "## 🏥 環境ヘルス"
run_to 60 ~/.claude/scripts/automation-health.sh 2>&1 | tail -3 | head -1
tail -3 | head -1 は最終サマリ行(✓ ALL GREEN / ⚠ WARN / ✗ FAIL の行)だけを切り出しています。
さらに daily-brief.sh 側では、ライブ疎通プローブで RED を検出したときだけ ## 🚨 要対応 セクションが現れます。
RED_FLAGS=""
flag_red() { RED_FLAGS="${RED_FLAGS}\n- ❌ $1"; }
if [ -n "$RED_FLAGS" ]; then
echo "## 🚨 要対応(ライブ疎通でRED)"
printf '%b\n' "$RED_FLAGS"
fi
問題がないときこのセクションは存在しません。**「失敗中だけ現れる節」**として設計されています。brief は ~/Desktop/Daily Brief/today-brief-YYYYMMDD.md に書き出されるので、デスクトップを開いたときに RED が目に入る形になります。
automation-health.sh の exit 1 を使えば、launchd のラッパーで同様のパターンを追加できます。
bash ~/.claude/scripts/automation-health.sh || touch ~/Desktop/AUTOMATION_FAILED.md
bash ~/.claude/scripts/automation-health.sh && rm -f ~/Desktop/AUTOMATION_FAILED.md
失敗中だけファイルが存在し、直ったら消える ―― ダッシュボードを見なくてもデスクトップが状態を教えてくれます。
踏んだ落とし穴
-
launchctl listの last exit code0と-を同じに扱わないと誤 WARN → どちらも正常として OR 条件で弾く -
hooks スクリプトが
chmod +xされていなくても settings.json はエラーを出さない → WARN で検出するまで気づかなかった -
agentmemory はポートが開くと launchd が「起動中」と見なす → HTTP
/healthを叩くまで半生状態を検出できない -
env-audit の出力先を
scripts/からlogs/に移設したとき、ヘルスチェック側のパスも更新が必要(スクリプト内コメント「2026-06-11: 出力先を scripts/→logs/ に移設」より) -
index.md カバレッジチェックで
.backup等の隠しディレクトリを除外しないと恒久 WARN になる →-not -path '*/.*'で隠しディレクトリを除外 -
cron→launchd 移行後に crontab を消し忘れると二重実行 →
comm -12で両系統のスクリプト名を比較して検出
まとめ
- 自律化の死活確認は1コマンドで ALL GREEN / WARN / FAIL の3値に集約する
-
exit 1を機械判定のシグナルとして使い、daily-brief への1行埋め込みや FAILED ファイルの生成に繋げる - **「問題中だけ現れる節やファイル」**を設計することで、ダッシュボードを見なくても異常が目に飛び込む
- launchd の PID チェックだけでは「半生状態」を見逃す。HTTP ヘルスエンドポイントを必ず一緒に叩く
- バッチの死活は log の mtime を budget 時間と比較するだけで機械判定できる
次回は、このヘルスチェックと autopilot が生成した改善ログを統合して、週次で何が変わったかをまとめる ―― 週次レポート自動生成の仕組みを書く予定です。
Lily(@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています
- 作ったアプリは ポートフォリオ にまとめています📱
- 新着・開発の裏側は X @bokuwalily で発信しています🌍
- OSS: github.com/bokuwalily 🐙
- この仕組みで「作業」じゃなく「環境」を回して月120万に戻した話は noteに無料で 書いています
皆さんの ❤️ やシェアが励みになります!