前回、外付けドライブのマウントとファイル同一性を扱った話を書きました。今回はもっと地味で、もっと厄介な話です ―― 一度直したはずの自動化が、直したことを示すコメントとログを残したまま、3週間サイレントで失敗し続けていた話です。
対象は npx skills@latest update -g、Claude Codeのスキルを最新化するCLIをlaunchdで週次実行しているだけの、地味なジョブです。ここに未認証GitHub APIのレート制限対策・ハング対策・失敗検知の3層を積んで「もう直った」と思っていました。ところが実ログを開いたら、直近3回の日曜実行が全部同じ理由で失敗していました。
困りごと:npx skills@latest update -g は未認証GitHub APIに弱い
~/Library/LaunchAgents/com.lily.skills-update.plist は、毎週日曜4:40にスキル更新スクリプトを起動するだけのplistです。
<key>Label</key><string>com.lily.skills-update</string>
<key>ProgramArguments</key><array><string>/bin/zsh</string><string>-lc</string><string>~/.claude/scripts/skills-auto-update.sh</string></array>
<key>StartCalendarInterval</key><dict><key>Weekday</key><integer>0</integer><key>Hour</key><integer>4</integer><key>Minute</key><integer>40</integer></dict>
呼んでいる skills-auto-update.sh は、単純に言えば npx skills@latest update -g --yes を叩くだけです。ただしこのCLI、未認証だとGitHubのAPIレート制限に普通に当たる上に、ネットワークの状態次第でfetchがハングします。スキル数が増えるほど当たりやすくなるので、放っておくと「気づいたら何ヶ月も更新されていない」が起きます。
設計:3層の自己修復
これに対して、スクリプトには3つの防御を積んでいます。実ファイルの該当部分です。
# skills CLI は未認証で GitHub API を叩くため、スキル数が増えるとレート上限に当たる。
# gh は認証済み(5000/h)なのでトークンを渡す。取れなくても従来どおり動く。
if command -v gh >/dev/null 2>&1; then
GH_TOKEN="$(gh auth token 2>/dev/null)"
if [[ -n "$GH_TOKEN" ]]; then export GH_TOKEN GITHUB_TOKEN="$GH_TOKEN"; fi
fi
# timeout 上限: npx の network fetch がハングするのを gtimeout で頭打ち(async hook なので非ブロッキング)。
# 45s だった頃はスキル数が増えて毎回タイムアウトし、5,833行の失敗ログを吐きながら
# 一度も更新できていなかった(2026-08-08 監査で実測4分00秒・exit 0 を確認)。
# STAMP は成功時のみ更新する。旧実装は失敗でも touch していたため、
# ネットワーク不調時に「更新が静かに失敗し続ける」状態になっていた(2026-06-11 監査)。
if command -v gtimeout >/dev/null 2>&1; then
if gtimeout 600 "$NPXBIN" skills@latest update -g --yes >/dev/null 2>>"$ERRLOG"; then
touch "$STAMP"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] skills update failed (timeout or error) — STAMP not updated, will retry next session" >> "$ERRLOG"
fi
3層はこう積み上がっています。
-
gh auth tokenを渡す ― 未認証(60/h)から認証済み(5000/h)に上げてレート制限を回避する -
gtimeout 600― 45秒では足りずスキル数が増えるほど詰まっていた実績があるので、10分で頭打ちにする -
STAMPは成功時のみ更新 ― 旧実装は失敗しても
touchしていたため「最後にいつ動いたか」が失敗時も進んでしまい、不調が完全に見えなくなっていた。これを潰すために、成功した時だけSTAMPを更新し、失敗時はskills-update.errに理由付きで書く設計にした
コメントを読む限り、2026-08-08の監査で「実測4分00秒・exit 0」を確認しているので、この設計は少なくとも一度は動作確認済みです。
コード内コメントに「なぜこう直したか」の来歴(45秒timeoutの失敗実績、5,833行のログ、2026-08-08の実測)が埋まっているのは良い設計です。ただしこれはその時点で検証した事実でしかなく、「以後もずっと動く」ことの証明にはなりません。この差を忘れると、コメントを読んだだけで「直っている」と錯覚します。
実ログを見る:3週連続で"timeout or error"
ここまでが設計意図です。では実際に何が起きていたか。~/.claude/logs/skills-update.err の中身がこれです。
[2026-09-06 04:40:38] skills update failed (timeout or error) — STAMP not updated, will retry next session
npm warn exec The following package was not found and will be installed: skills@1.5.26
[2026-09-13 05:27:06] skills update failed (timeout or error) — STAMP not updated, will retry next session
npm warn exec The following package was not found and will be installed: skills@1.7.0
[2026-09-20 04:41:35] skills update failed (timeout or error) — STAMP not updated, will retry next session
9/6・9/13・9/20 ―― すべて日曜、すべてplistの起動時刻(4:40前後)と一致します。つまりこの3週間、日曜のジョブが毎回起動し、毎回失敗し、毎回同じ理由をログに残していたということです。しかもnpm warn execの行が挟まっているのは、npx自体のbootstrap(skills@1.5.26→skills@1.7.0へのバージョン取得)は進んでいる証拠です。つまり失敗しているのはnpx起動の手前ではなく、その先のskills update本体、まさにレート制限かネットワークハングが疑われる箇所です。
STAMPファイルに残っていた矛盾
もう一つ実ファイルを見ます。成功時だけ更新されるはずの~/.claude/skills-last-updateです。
birth: Aug 8 11:54:13 2026 / mtime: Sep 10 17:02:10 2026
作成日は2026-08-08 ―― 設計コメントにある「実測4分00秒・exit 0」の監査日と一致します。ここまでは辻褄が合います。
問題はmtimeです。2026-09-10 17:02。これはplistのスケジュール(毎週日曜4:40)のどれにも該当しません。日曜でもないし、朝でもない。つまり、このSTAMPを最後に更新した実行は、launchdの週次自動実行から来たものではない可能性が高いということです。
その後の記録に残っている自動実行(9/6, 9/13, 9/20)は全部失敗しているので、少なくともこの3週間、自動化の経路だけを見れば一度も成功していません。STAMPは3週間前から時が止まったまま、それを誰も見に行っていませんでした。
なぜ「直したはず」が直っていないのか
ここが一番書きたかったことです。3層の自己修復は、それぞれ意図通りに動いています。
-
gtimeout 600は効いている(600秒以内にexit codeが返ってきてelse分岐に落ちている。もし本当にハングしているなら[timeout]の記録タイミングがもっと不規則になるはずですが、3回とも起動から1〜47分以内に規則的に記録されています) - STAMP success-onlyも効いている(実際、成功していないのでSTAMPは進んでいない ―― これは設計通りの正しい挙動です)
-
echo ... >> "$ERRLOG"も効いている(3週分きちんと記録が残っている)
つまりこの安全装置は、どこも壊れていません。 それでいて3週連続で更新は失敗し続けています。設計が直したのは「失敗しても何も分からない」状態から「失敗した理由と時刻が分かる」状態への変化であって、失敗そのものを直してはいなかったのです。「動くはず」のコードが実際にやったのは、サイレント障害をログ付き障害に変えることだけで、根治はしていませんでした。
残る一番怪しい変数はgh auth tokenです。スクリプトのコメントには「取れなくても従来どおり動く」とありますが、これは裏を返すと取れたかどうかを誰も確認していないということです。手元でgh auth statusを叩くと、認証済みアカウントとは別に、keyringアクセスでタイムアウトしているアカウントが存在することを確認しました。gh auth token自体はいまは正常終了(exit 0)しますが、これがlaunchdの非対話・早朝4:40という文脈でも毎回同じように解決される保証はありません。もしここでkeychainアクセスが詰まってGH_TOKENが空のまま渡っていたら、スクリプトは何も警告せず未認証パスにフォールバックし、まさに直したかったレート制限にまた当たることになります。ここは今回のログだけでは断定できず、次に手を入れるべき仮説として置いておきます。
macos-hook-path-timeoutスキルとの接点
このgtimeout依存には前史があります。~/.claude/skills/auto/macos-hook-path-timeout/SKILL.mdは、まさに今回のスクリプトを踏まえて自動生成された既存スキルです。
### 症状2: スクリプトの `timeout 60 ...` が効かない(`timeout: command not found`)
macOSは GNU coreutils の `timeout` を**標準搭載しない**。ECCのhooks.md等が前提にしていても不発。
**修正**: `brew install coreutils` で `gtimeout` が入る。スクリプトは
`command -v gtimeout >/dev/null && TIMEOUT_CMD="gtimeout 60"` の形で優先検出させる
(`post_tsc_check.sh` / `skills-auto-update.sh` はこの形。coreutils導入だけで設計通りに起動する)。
このスキルのVerification節は、gtimeoutコマンドの存在確認までしかしていません。
command -v gtimeout && echo "post_tsc_check 等の timeout 分岐が起動する"
つまりこのスキルが保証しているのは「gtimeoutが入っていればtimeout分岐は起動する」ところまでで、「その分岐に入った後、ジョブが実際に成功するか」は検証範囲の外です。今回の3週連続失敗は、まさにその外側 ―― gtimeoutは正しく起動し、正しく頭打ちにし、正しくログを吐いた上で、それでも中身のskills update自体が通らなかったケースでした。
踏んだ落とし穴
- 45秒timeoutを600秒に伸ばして直った「はず」だったが、それはネットワーク/レート制限そのものを直したわけではなかった → 時間を伸ばすのはハング対策にしかならず、原因が別(レート制限やGH_TOKEN未取得)なら何度伸ばしても同じ場所で落ちる
- STAMP success-onlyは「失敗を直した」のではなく「失敗を見える化した」だけ → ログを定期的に見に行く仕組み(cron監視やstatusline連携)とセットでないと、可視化された失敗は誰にも読まれず旧来と同じ「気づいたら何ヶ月も更新されていない」に戻る
-
npm warn execの有無で失敗箇所を切り分けられる → その行があればnpxのbootstrapは通っているので、原因はskills update本体(ネットワーク/API側)に絞れる -
取れなくても従来どおり動くという一文が一番弱いフォールバック →GH_TOKENが空でも黙って未認証パスに落ちるため、認証取得の成否自体をログに残していないと「対策済みのはずが実は無効化されていた」を見抜けない - STAMPのmtimeをcronスケジュールと突き合わせる → 更新時刻がジョブの起動時刻と一致しない場合、それは自動化経路からの成功ではない可能性が高い
- 既存のauto-skillのVerification節は「コマンドが存在するか」までしか見ていないことがある → 存在確認と「その後ジョブが実際に成功し続けているか」は別の検証軸
まとめ
- 3層の自己修復(GH_TOKEN注入・gtimeout・success-only STAMP)はコード上も実行ログ上もそれぞれ設計通りに動いていた
- それでも直近3回の週次実行(9/6・9/13・9/20)はすべて同じ理由で失敗し、STAMPは2026-09-10で止まったまま進んでいなかった
- 安全装置が変えたのは「サイレント障害」を「ログ付き障害」にしたことまでで、失敗そのものの根治ではなかった
- 「動くはず」(設計・過去の実測)と「実際に動いた」(生ログ・STAMPのmtime)は別物として、両方を定期的に突き合わせる必要がある
-
gh auth tokenがlaunchdの非対話文脈でも毎回解決されているかは未確認 ―― 次に潰すべき仮説として残した
次回は、このgh auth tokenがlaunchdの実行文脈(非対話・早朝・keychain未ロック)でも本当に毎回解決できているかを検証し、根本原因を特定できたかどうかを書く予定です。
Lily(@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています
- 制作物・記事は bokuwalily.com にまとめています🖥️
- AIで「寝てても回る仕組み」を作って月120万にした話は noteの有料記事 に💰
- OSS: github.com/bokuwalily 🐙
- 最新情報・お問い合わせは X @bokuwalily へ🌍
- AI導入・自動化の相談と実装テンプレ7本の配布は 公式LINE から💬
皆さんの ❤️ やシェアが励みになります!