Claude Media
Claude Codeがlaunchdで固まるときの原因と対処法 — claude -pとzsh

Claude Codeがlaunchdで固まるときの原因と対処法 — claude -pとzsh

macOSのlaunchdやcronから動かしたclaude -pが最初のトークンの前で止まる症状を、zshのシェルスナップショットから切り分け、SHELLやCLAUDE_CODE_SHELLで避ける手順をまとめます。

macOSのlaunchdから夜間ジョブとしてclaude -pを動かすと、出力が0バイトのまま何分も動かない。同じコマンドをターミナルで打てば普通に終わる。この症状は、Claude Codeが起動時に作るzshのシェルスナップショットが関わる報告が2件あります。ただし原因が同じとは限らず、避け方も症状ごとに違います。

結論から言うと、ジョブ側で試す順番は次のとおりです。

  1. SHELL=/bin/shを付けて起動する(報告者の回避策。公式の仕様としては書かれていません)
  2. CLAUDE_CODE_SHELL=/bin/bashを設定する(zshスナップショットの別バグへの回避策)
  3. それでも止まるなら、--bareで起動する(ただしサブスクリプションのログインは使えません)

launchdから動かしたclaude -pは何が止まるのか

報告されている症状は「最初のトークンが出る前に止まる」です。ジョブの標準出力は空のまま、claudeプロセスだけが生きています。GitHubのissue #78903では、次の条件で再現しています。

  • macOS(Darwin 25.x、Apple SiliconのMac mini)で、サブスクリプション認証
  • 夜間のlaunchd LaunchAgentからclaude -p "<prompt>" --model <model> --strict-mcp-config --settings <file> --debug-file <file> </dev/nullを実行
  • 出力ログは0バイトで、10分以上たってウォッチドッグが強制終了するまで進まない
  • 同じ呼び出しをインタラクティブなターミナルから実行すると、毎回正常に終わる

止まっている間にプロセスを採取すると、メインスレッドはkevent64で待機していました。残っていた子プロセスは、~/.zshrcを読み込む/bin/zsh -c -l、つまりシェルスナップショットを作る側だけです。この.zshrcはnvm、brewのshellenv、補完の初期化を含む重いもので、対話シェル向けのガードもありませんでした。Keychainはアンロック済みで、APIにも届き、MCPサーバーも無効という状態でした。

launchdとターミナルの目立つ違いは、TTYがなく、環境変数が最小限であることです。

シェルスナップショットとは

Claude CodeはBashツールでコマンドを走らせるために、ユーザーのシェル設定(関数・エイリアス・オプション)を書き出したスナップショットを~/.claude/shell-snapshots/に置きます。実行時にはこれを読み込んでからコマンドを評価します。スナップショットを作る段階でシェルが終わらなければ、その後ろの処理も進みません。

このissueの報告者のセッションはBashツールを許可していませんでした。つまりスナップショットは使われないのに、起動時にその生成を待つ形になっています。

回避策1: SHELL=/bin/shを付けて起動する

報告者が確認した回避策は、claudeの起動時にSHELLを/bin/shにすることです。重いzshの初期化の代わりに、ほぼ空のPOSIXシェルのスナップショットが作られ、ハングは消えました。2026-07-13以降の夜間実行はすべて成功しています。

launchdのplistなら、EnvironmentVariablesに入れます。

<key>EnvironmentVariables</key>
<dict>
  <key>SHELL</key>
  <string>/bin/sh</string>
</dict>
<key>ProgramArguments</key>
<array>
  <string>/Users/you/.local/bin/claude</string>
  <string>-p</string>
  <string>Summarize yesterday's commits</string>
</array>
<key>StandardOutPath</key>
<string>/tmp/claude-nightly.out</string>
<key>StandardErrorPath</key>
<string>/tmp/claude-nightly.err</string>

cronなら、crontabの先頭か、コマンドの前に書きます。

SHELL=/bin/sh
0 3 * * * /Users/you/.local/bin/claude -p "Summarize yesterday's commits" </dev/null

注意点が2つあります。

  • 報告者自身が「SHELLを公式にサポートされた制御として書いた資料は、ドキュメントにもchangelogにも見つからなかった」と書いています。つまり未文書化の挙動で、リリースによって変わる可能性があります
  • 環境変数のドキュメントでは、シェルの自動検出は$SHELLがbashかzshを指すときにそれを使い、そうでなければPATHと標準の場所から最初に動くzsh、次にbashを選ぶと書かれています。/bin/shがこの手順でどう扱われるかはドキュメントに記載がなく、効いたのは報告者の環境の観察です

この回避策を入れたら、ジョブの実行時間をログに残しておくと、将来のバージョンで再発したときに気づけます。

回避策2: CLAUDE_CODE_SHELL=/bin/bashでzshスナップショットを避ける

別のissue #99254は、launchdではなくBashツールの全コマンドが固まる報告です。ただし原因はスナップショットで、launchdジョブの診断にも使える情報が含まれています。

報告では、Claude Code 2.1.288と2.1.285のネイティブインストール、macOSのdarwin-arm64、zsh 5.9で、Bashツールの呼び出しがlsでさえ「Running…」のまま戻りませんでした。止まっている間のプロセスを見ると、シェルのラッパーにlogin(/usr/bin/login)という子プロセスがぶら下がり、標準入力を待っていました。

スナップショットを手でsourceすると、setoptやaliasを付けずに名前だけが書かれた行が見つかります。

snapshot-zsh-....sh:225: command not found: promptsubst
snapshot-zsh-....sh:234: command not found: run-help=man
snapshot-zsh-....sh:236: command not found: which-command=whence

ログインシェルで有効になるzshのloginオプションが、loginという行だけで書き出され、これが/usr/bin/loginを実行してしまう、というのが報告者の診断です。ユーザーのコマンドには< /dev/nullが付くのに、sourceの段階には付かないため、loginが入力待ちで止まります。

回避策は、~/.claude/settings.jsonのenvでCLAUDE_CODE_SHELLをbashにすることです。

{
  "env": {
    "CLAUDE_CODE_SHELL": "/bin/bash"
  }
}

CLAUDE_CODE_SHELLは、Bashツールのコマンドを走らせるシェルを指定する公式の環境変数です。bashかzshのバイナリのパスを受け付け、fishなどは未対応です。値が動作するbash/zshのパスでなければ無視され、自動検出に戻ります。

launchdのジョブだけに限るなら、plistのEnvironmentVariablesに同じ名前で書く手もあります。settings.jsonのenvは、同じ名前のシェル変数より優先されるのが通常の挙動です。ただし環境変数のドキュメントには、シェルの値が残るセッションもあると書かれています。効かなければ起動方法を確かめてください。

報告者が挙げた修正案は2つで、スナップショットにオプションごとにsetopt、エイリアスごとにaliasを付けて書くことと、sourceの標準入力も/dev/nullにすることです。どちらも修正済みという記載は、issueにはありません。

回避策3: .zshrcを軽くする(未検証の仮説)

#78903の報告者は、重い.zshrcの初期化が、launchdの非対話の文脈でスナップショット生成を止めているのではと推測しています。この仮説は、本人が「未検証」と明記しており、重い.zshrcとガード付き.zshrcを比べるA/Bをこれから行う予定、としています。

したがって、これは確かめた解決策ではなく、試す価値のある切り分けです。.zshrcの先頭で、対話シェル以外は早く抜ける書き方があります。

# ~/.zshrc の先頭
[[ -o interactive ]] || return

注意すべきは、スナップショット生成は/bin/zsh -c -lで、-cなので対話シェルではない点です。このガードを入れると、スナップショットに入れたい関数やエイリアスまで読み込まれなくなります。Bashツールでzoxideのようなシェル関数を使いたい場合は、Claude Codeでzoxideなどシェル関数が使えない原因と対処法の症状が出る可能性があります。ジョブ用途でBashツールを使わないなら、影響は小さいはずです。

回避策が効かないとき: 2.1.243の別のハング

#78903のコメントには、SHELL=/bin/shでは直らない別の症状が書かれています。同じマシンと同じLaunchAgentで、バイナリのパスだけを替えた結果です。

バージョン結果
2.1.243結果rc=126。30秒でCPU時間が3センチ秒ほどのデッドロック
2.1.241結果rc=0。10秒で正常終了
2.1.240結果rc=0。10秒で正常終了

2.1.243でSHELL=/bin/shもSHELL=/bin/zshも試して、どちらも固まったとのことです。メインスレッドがkevent64で止まる点は同じですが、デバッグログの最後が[STARTUP] Loading commands and agents...で、/bin/zsh -c -lの子プロセスがなく、子プロセス自体が見当たらない、という違いがあります。コメントした人はこれを別の退行と見て、#89537として別に報告したと書いています。

その報告によれば、効かなかった設定は--strict-mcp-config、enabledPlugins:{}、lspServers:{}、フック・プラグインの転送の無効化、リモートコントロールの設定、pty割り当てです。避けられたのは--bareだけでした。

--bareで起動する場合の制約

--bareは、フック、スキル、カスタムコマンド、サブエージェント、プラグイン、MCPサーバー、自動メモリ、CLAUDE.mdの自動検出を飛ばして起動するモードです。スクリプトやSDK呼び出しに推奨されていますが、launchdのハング回避に使うなら次の点を確認してください。

  • OAuthの認証情報とシステムのKeychainを読みません。ANTHROPIC_API_KEYを環境変数で渡すか、--settingsのJSONでapiKeyHelperを指定する必要があり、サブスクリプションのログインは使えません
  • MCPサーバーはコマンドラインで渡したものだけが接続します(--mcp-config)
  • バックグラウンドタスクは動かず、タイムアウトに達したコマンドは停止します

つまり、Pro・Maxの定額枠で夜間ジョブを回している人には、--bareはAPI課金への切り替えを意味します。claude -pの基本はClaude Code -pモードでスクリプトやパイプラインを自動化する基本にまとめています。

どの症状かを切り分ける

止まり方によって、試す手が変わります。

観察疑うもの最初に試すこと
子プロセスに/bin/zsh -c -lが残っている疑うものzshのスナップショット生成最初に試すことSHELL=/bin/sh、CLAUDE_CODE_SHELL=/bin/bash
子プロセスにloginが残っている疑うものスナップショットのlogin行(#99254)最初に試すことCLAUDE_CODE_SHELL=/bin/bash
子プロセスがなく、Loading commands and agents...で止まる疑うもの2.1.243で報告された別の退行最初に試すことバージョンを下げる、または--bare
ターミナルでも止まる疑うものlaunchdとは別の問題最初に試すことスナップショットや設定の問題を疑う

プロセスの子を見るには、ジョブが止まっている間に次を実行します。

pgrep -fl claude
ps -o pid,ppid,command -ax | grep -E 'claude|zsh|login' | grep -v grep

--debug-fileを付けておけば、最後に出たデバッグ行がどの段階かを後から読めます。#78903の報告者も、全--debug-file出力とハング中プロセスのスタックサンプルを提供できると書いています。

ウォッチドッグとログを先に用意する

どの回避策でも、固まったときに気づける仕組みが先です。#78903の報告者は、10分以上待ってからウォッチドッグで強制終了していました。launchdにはタイムアウトの指定がないので、ラッパースクリプトで包む方法があります。

#!/bin/bash
# nightly-claude.sh
export SHELL=/bin/sh
timeout 900 /Users/you/.local/bin/claude -p "$1" </dev/null \
  >> /tmp/claude-nightly.out 2>&1 || echo "exit=$?" >> /tmp/claude-nightly.err

timeoutはGNU coreutilsのコマンドです。環境に無ければgtimeoutに置き換えるか、同等のウォッチドッグを別に用意します。</dev/nullを付けるのは、#78903の報告者の呼び出しがそうしていたためで、対話入力を待たせない目的です。

cronの書式やジッターについてはClaude Code cron式リファレンスと7日失効・ジッターの仕様にまとめています。ここで扱ったlaunchdやcronは、OS側のスケジューラーです。Claude Code自身のスケジュール機能とは別物です。

今後に備えて確認しておくこと

2つのissueは、どちらもopenのままで、修正バージョンの記載はありません。#78903にはstaleラベルも付いています。ジョブが止まるたびに疑える手を整理すると、次の順になります。

  1. psで子プロセスを見て、表の行に当てはめる
  2. SHELL=/bin/shとCLAUDE_CODE_SHELL=/bin/bashを、ジョブだけで有効にして比べる
  3. バージョンを一つ前に戻して動くかを確かめる(2.1.243の報告のように、バイナリだけを替えるA/Bが有効)
  4. 最後の手段として--bareとANTHROPIC_API_KEYに切り替える

claudeのアップデート後に夜間ジョブが静かに止まるのが、この症状の厄介な点です。出力の有無と実行時間を毎晩ログに残すだけでも、原因の切り分けは早くなります。

この記事を共有:XはてブLinkedIn