作業環境・自動化

Macのcronを捨ててlaunchdに乗り換えた話——自動化パイプライン9本を移行して気づいたこと

Macのcronを捨ててlaunchdに乗り換えた話——自動化パイプライン9本を移行して気づいたこと

この記事は約 11 分で読めます

📖 目次
  1. 📌 cron で詰まった日のこと
  2. 📌 そもそも launchd と cron は何が違うのか
  3. 📌 実際に使っているplistの構造
  4. 📌 cronでは絶対ハマる PATH 問題
  5. 📌 スリープ中に実行できるかどうか
  6. 📌 launchd の使い方——3コマンド覚えれば十分
  7. 📌 実際に詰まったポイント
  8. 📌 cronを使う場面が残るとしたら
  9. 📌 まとめ
  10. 📌 関連記事

最終更新: 2026-04-28

cron で詰まった日のこと

去年の秋、BlueSky自動投稿スクリプトを夜中に動かそうとして、crontabを書いた。

30 7 * * * /u​sr/b​in/python3 /Users/taito/scripts/post_bsky.py

動かない。ターミナルで叩くと普通に動く。cron越しだと何も起きない。ログもない。本当に、何も。

3時間調べた結果、地雷が3つ同時に踏まれていた——「macOSはスリープ中にcronが実行されない」「PATHが違う」「.zshrcが読まれないので ollama のパスが通っていない」。どれか1つなら30分で直せた。3つ同時は堪えた。

その日launchdに書き直したら、30分で動いた。それ以来、Macで定期実行するものはlaunchd一択にしている。今は9本のパイプラインが全部launchdで回っている。


そもそも launchd と cron は何が違うのか

Macにおけるcronは「動く場合もある」という存在だと思ったほうがいい。Apple的にはlaunchdが正式な後継で、macOS 10.4 Tigerのころから推奨されている。cronは後方互換で残っているだけ——という立ち位置だ。

launchdで何が変わるかというと、ログ取得が .plist に2行書くだけで終わる。

<key>StandardOutPath</key>
<string>/tmp/my_job.log</string>
<key>StandardErrorPath</key>
<string>/tmp/my_job_err.log</string>

cronでこれをやるには、スクリプトごとに >> /tmp/log 2>&1 を末尾に書くか、スクリプト内でloggingを仕込むかの二択になる。設定ファイルに書き忘れて、ジョブが死んでも何のエラーも出ない状態を1週間放置したことがある。launchdに移ってから、そういう事故はなくなった。


実際に使っているplistの構造

今動いているパイプラインのひとつ、note記事の自動投稿ジョブはこういう形になっている。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.taito.note-poster</string>

  <key>ProgramArguments</key>
  <array>
    <string>/u​sr/b​in/python3</string>
    <string>/Users/taito/Documents/AI_Automation_Base/01_Scripts/sns/post_note_from_queue.py</string>
  </array>

  <key>StartCalendarInterval</key>
  <array>
    <dict>
      <key>Hour</key><integer>7</integer>
      <key>Minute</key><integer>0</integer>
    </dict>
    <dict>
      <key>Hour</key><integer>12</integer>
      <key>Minute</key><integer>30</integer>
    </dict>
    <dict>
      <key>Hour</key><integer>20</integer>
      <key>Minute</key><integer>30</integer>
    </dict>
  </array>

  <key>StandardOutPath</key>
  <string>/Users/taito/Documents/AI_Automation_Base/04_Config/logs/note_poster.log</string>
  <key>StandardErrorPath</key>
  <string>/Users/taito/Documents/AI_Automation_Base/04_Config/logs/note_poster_err.log</string>

  <key>EnvironmentVariables</key>
  <dict>
    <key>PATH</key>
    <string>/opt/homebrew/bin:/u​sr/local/bin:/u​sr/bin:/bin</string>
    <key>HOME</key>
    <string>/Users/taito</string>
  </dict>
</dict>
</plist>

StartCalendarInterval に配列で複数時刻を渡せるのが地味に使いやすい。7:00 / 12:30 / 20:30の3回投稿を、1つのplistだけで管理できる。cronだと 0 7,12,20 * * * で書けるといえば書けるが、3ヶ月後に見直したとき「これ何時だっけ」となる。plistは冗長だが読める。


cronでは絶対ハマる PATH 問題

ターミナルで打つと動くのに、cron経由だと command not found になる——これが最初の壁だ。

原因はcronがlogin shellを起動しないこと。~/.zshrc~/.zprofile も読まれない。Homebrewで入れたツール(python3、node、各種CLI)は /opt/homebrew/bin にあるが、cronの実行環境のPATHにそこは含まれていない。私が詰まったのも ollama コマンドが見つからないというエラーだった。ターミナルでは動く、cron経由では死ぬ。典型的なやつ。

launchdでも同じ問題は起きる。ただ解決が明示的でわかりやすい。plistの EnvironmentVariables に書くだけで、そのジョブの環境変数が固定できる。一度テンプレを作れば、9本のパイプライン全部に流用できる。

cronで同じことをやるには、crontabの先頭に SHELL=/bin/zshPATH=... を書くか、スクリプトの冒頭で source ~/.zshrc を呼ぶかになる。後者はCI環境で予期しない挙動を引き起こすことがあって、やりたくない。


スリープ中に実行できるかどうか

MacBook Pro M5で運用しているので、これは実際に問題になった。

cronはスリープ中に指定時刻を過ぎると、そのジョブを静かにスキップする。起きてから実行されることはない。気づかないまま1週間、BSky投稿が止まっていたことがある。ログが一切なかったので、止まっていること自体に気づくまで3日かかった。

launchdも厳密にはスリープ中は動かない。ただ RunAtLoadStartInterval を組み合わせると、「起動・復帰後に経過時間ベースで実行する」という動きをさせられる。

<key>RunAtLoad</key>
<true/>

完璧な解決ではないが、cronの「黙ってスキップ」よりはずっとマシだ。今はMacが復帰したタイミングで遅れ分を流す構成にしている。


launchd の使い方——3コマンド覚えれば十分

設定ファイルの置き場所はここ一択(ユーザースコープの場合):

~/Library/LaunchAgents/

操作は3つのコマンドだけ覚えれば回る。

# 登録(起動)
launchctl load ~/Library/LaunchAgents/com.taito.note-poster.plist

# 停止・解除
launchctl unload ~/Library/LaunchAgents/com.taito.note-poster.plist

# 動いているか確認
launchctl list | grep com.taito

launchctl list の出力は PID\t終了コード\tLabel の3列。PIDが数字なら今動いている、0なら停止中、終了コードが0以外なら異常終了——という読み方をする。終了コードが78で詰まったこともあるが、それは別の記事に書いた。


実際に詰まったポイント

半年運用して、詰まったポイントは主に2つだった。

ひとつはplistのXML構文ミス。閉じタグを1つ忘れただけで、launchctl load がエラーも出さずに無視する。静かに失敗するので、しばらく気づかなかった。plutil -lint で事前チェックする習慣を付けてからは、このパターンでは詰まらなくなった。

plutil -lint ~/Library/LaunchAgents/com.taito.note-poster.plist

もうひとつはカレントディレクトリの問題。launchd経由で実行すると、カレントディレクトリが / になる。スクリプト内で open('data.json') のような相対パスを書いていると、FileNotFoundError で死ぬ。エラーログを見て「ファイルがない?さっきターミナルで確認したのに」と首をかしげていた。原因がカレントディレクトリだとわかったのは30分後。以来、絶対パスか os.path.dirname(__file__) しか使わない。


cronを使う場面が残るとしたら

正直、今の構成でcronを使う場面はほぼない。

強いて言えば、LinuxのVPSとMacの両方で同じスクリプトを動かしたい場合だ。launchdはMac専用なので、同一の定期実行設定を両環境に持ち込みたいならcronで書いておくほうが移植コストが下がる。ただそういうケースは私の環境ではほとんど発生していない。

Mac専用のパイプラインであれば、launchd一択だと思っている。ログが取れる、PATH問題を明示的に解決できる、plistで設定が自己文書化される——この3点だけで、移行する理由として十分だった。


まとめ

cron → launchdの移行で一番変わったのは、「自動化が壊れていることに気づける」環境になったことだ。

以前はcronがスキップしてもログが何もなくて、気づかず1週間投稿が止まっていた。launchdになってからはエラーログをDiscord Botが拾って、朝イチで「昨夜の実行でエラー出てます」と通知が来る構成になっている。9本のパイプラインが毎朝ちゃんと動いているかどうかを、コーヒーを飲みながら確認できる。

自動化は動いて終わりじゃない——壊れたときに気づける仕組みまで込みで、完成だと思っている。✨


📘 この記事のテーマをさらに深掘りした本

Claude Codeで副業自動化——月5万円を目指した全記録

非エンジニアが1年で月10万円に到達した全パイプラインと失敗談

Kindleで読む →


一ノ瀬泰斗のアバター
一ノ瀬泰斗
AI自動化エンジニア / Python個人開発者

Claude Code × Ollama × launchd で SNS・ブログ・KDPを全自動化。実測データと失敗談を軸に、月5万円収益化のリアルな記録を発信中。

💬 自動化の相談・小規模受託も受付中:「launchd で毎朝 AI が動く仕組みを作りたい」「KDP の自動出版を組みたい」など、X (@taito_automate) の DM からお気軽にどうぞ。


関連記事

✨ AUTHOR'S KDP BOOKS

かかる人向ケ、10分でわかるAI自動化入門

Claude Code / Ollama / launchd の実践テクニックをコンパクトにまとめたシリーズ。非エンジニアの会社員向けに書いてます。

Amazonで見る ›

✨ FOLLOW ME

AI自動化の実験・失敗・実測データを毎日発信中

𝕏 フォローする