最終更新: 2026-04-28
exit 78 で3時間溶かした話
launchd を使い始めて最初の週、私は exit 78 に3時間費やした。スクリプトはターミナルから動く。plist の書き方も合っている気がする。なのに launchd のパス・環境変数設定で詰まった時の解決策 launchctl start するたびに終了コード 78 が返ってくる——。
exit 78 は「設定が壊れている」というシグナルで、スクリプト自体のバグではない。ターミナルと launchd が別の環境で動いているせいで再現する。原因がわかってしまえば直すのは5分 ターミナルと launchd の環境ズレをデバッグする方法もかからない。でもその「わかるまで」で半日溶かせる。
この記事では、私がパイプライン10本を launchd に乗せる過程で踏んだ失敗パターン launchd で複数パイプラインを安定稼働させる実装例と、デバッグを3分以内に終わらせるための手順を書く。launchd での自動化スクリプト運用の延長として読んでほしい。
exit 78 が「設定エラー」である理由
launchd が報告する終了コードは、ジョブ自身が返した値をそのまま渡す。exit 78 は sysexits.h で定義されている EX_CONFIG に相当する——「設定が壊れている状態で起動しようとした」という意味だ。
ここで注意が要る。これはスクリプト内で sys.exit(78) を呼んだケースの話ではない。launchd 経由でのみ再現し、手動実行では問題なく動く。そういう症状の場合、犯人はほぼ必ず環境の差異だ。
参照: Apple Developer Documentation: Creating Launch Daemons and Agents
私が踏んだパターン3つ
パターン1: PATH がターミナルと launchd で全然違う
一番多かった。ターミナルで which python3 を打つと /opt/homebrew/bin/python3 が返ってくる。でも launchd のデフォルト PATH は /usr/bin:/bin:/usr/sbin:/sbin だけだ。Homebrew のパスが入っていない。
plist に EnvironmentVariables を書いていないと、Homebrew 経由でインストールしたコマンドが全部見つからなくなる。エラーログには env: python3: No such file or directory と出ていた。
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>
これを追加して unload → load で直った。追記してから動くまで10秒もかからなかった——3時間の大半はここを疑わずに過ごしていた。
パターン2: WorkingDirectory の typo か存在しないパス
plist に WorkingDirectory を指定していて、そのパスが存在しないか typo している場合も exit 78 になる。
<key>WorkingDirectory</key>
<string>/Users/ichinosetaito/Documents/AI_Automation_Base</string>
確認はこれだけ。
ls /Users/ichinosetaito/Documents/AI_Automation_Base
存在確認が通らなければディレクトリ名を見直す。~ の展開も launchd では効かないので絶対パスで書くこと。私は一度 ~/Documents/AI_Automation_Base と書いて30分無駄にした。チルダ1文字で30分——これが2回目の沼だった。
パターン3: venv がアクティブになっていない環境から呼ばれる
これが一番わかりにくかった。ターミナルから実行するときは仮想環境がアクティブになっているので動く。launchd から呼ばれるときはアクティブになっていないので、import anthropic とか import discord が ModuleNotFoundError で死ぬ。エラーログを仕掛けていなかったので、最初は何が起きているのかすら見えなかった。
解決策は venv の Python を直接指定するか、シェルスクリプトで source activate してから呼ぶかの2択で、私は前者に統一した。
<!-- venv の Python を直接指定する(私はこちら) -->
<key>ProgramArguments</key>
<array>
<string>/Users/ichinosetaito/Documents/AI_Automation_Base/.venv/bin/python3</string>
<string>/Users/ichinosetaito/Documents/AI_Automation_Base/01_Scripts/post_to_bsky.py</string>
</array>
シェルラッパー経由だと起動のたびにシェル1本分のオーバーヘッドが乗る。数十ms の差でしかないが、パイプライン10本が並走すると地味に積み上がる。venv の Python を直接指定する方がシンプルで依存も少ない。
exit コードと原因の対応表
| exit コード | 意味 | よくある原因 |
|---|---|---|
| 78 | EX_CONFIG(設定エラー) | PATH・WorkingDirectory・venv 未設定 |
| 127 | command not found | インタープリタのパスが間違い |
| 1 | 一般エラー | スクリプト内で例外発生 |
| 126 | 実行権限なし | chmod +x していない |
| 0 | 正常終了 | — |
exit 78 と 127 はほぼ環境差異の問題で、exit 1 になったらスクリプト側のバグを疑う。私のパイプライン10本で集計すると、exit 78 が障害全体の約6割を占めていた。残りの約2割が exit 1 で、127 や 126 は稀だった。
デバッグ手順——これをやれば大抵わかる
ログを見る。まずここから。
log show --predicate 'subsystem == "com.apple.launchd"' --last 1h | grep -i error
ただしこれだと出力が粗い。plist に StandardErrorPath を仕込んでおく方が圧倒的に速い。
<key>StandardOutPath</key>
<string>/tmp/myjob.out.log</string>
<key>StandardErrorPath</key>
<string>/tmp/myjob.err.log</string>
ロードして実行された直後に cat /tmp/myjob.err.log を見ると、何が足りないのかが出てくる。ModuleNotFoundError なら venv 問題。No such file or directory なら PATH か WorkingDirectory 問題。このどちらかで exit 78 の9割は説明できた。
私が最初の3時間で何をしていたかというと——StandardErrorPath を設定していなかったので、何も見えない状態で plist の文法ミスを探し続けていた。ログの出口を作る、それだけで世界が変わる。
plist を書いたら確認する5点
毎回このリストを一周する癖をつけてから、exit 78 で詰まる時間がほぼゼロになった。
- [ ]
ProgramArgumentsの Python パスが venv 内の絶対パスになっているか - [ ]
WorkingDirectoryに typo がないか、ディレクトリが実在するか(lsで確認) - [ ]
EnvironmentVariablesに PATH を明示しているか(Homebrew を使うなら/opt/homebrew/binも含める) - [ ]
StandardErrorPathでエラーログを出力しているか - [ ] plist を変更した後に
launchctl unload ~/Library/LaunchAgents/com.myjob.plist→launchctl load ~/Library/LaunchAgents/com.myjob.plistしているか
最後の「変更後に reload していない」も地味に多い。変更しても反映されていないのに「直らない」と悩むやつ——私は2回やった。unload と load をセットで実行するワンライナーをシェルの alias に登録してからは起きていない。
exit 78 は「環境の差異」シグナルと思えば怖くない
exit 78 が出たとき、スクリプトを疑うより先に環境を疑う。PATH・WorkingDirectory・venv のどれかが欠けているだけで、コード自体は正しいことがほとんどだ。
StandardErrorPath を必ず設定しておけばデバッグ時間が劇的に短くなる。私の場合、設定前は原因特定まで平均30分かかっていたのが、設定後は3分以内で済むようになった。体感ではなく実測の値だ。
今はパイプラインが10本以上 launchd で動いているが、exit 78 で詰まるのはたいてい新しい plist を書いたときの最初の数分だけだ。慣れたら怖くない。✨
関連記事
- Macのcronを捨ててlaunchdに乗り換えた話——自動化パイプライン9本を移行して気づいたこと
- macOSのlaunchdで自動化パイプラインを組んで詰まりまくった話——plistの書き方から実務の罠まで全部書く
- 副業で月5万円稼ぐためにAI自動化で実装した6つの仕組み