launchd とは、macOS で常駐の仕事と時刻の仕事を起こして見張る、OS に組み込まれた仕組みです。

  • launchd・launchctl・cron の違いと、LaunchAgent(LaunchAgents)と LaunchDaemon の置き場所
  • launchctl の bootstrap・bootout・kickstart・print の使い方と、plist の最小の形
  • AIエージェントの席を Mac に常駐させて会社を回す当社が、launchd で実際に踏んだ 7 件と、そこから決めた手順

当社は Claude Code の席を Mac の上で何本も常駐させ、CI の作業員の組や時刻の便も同じ Mac の launchd に載せています。この頁は Apple の文書で launchd の仕組みを押さえた上で、当社の記録から「どこで壊れ、何を決めたか」を並べます。Claude Code を時刻で起こす 1 行そのものは Claude Code の定期実行、席を tmux に置いて常駐させる形は Claude Code と tmux の頁にあります。

launchd・launchctl・cron の違い

launchd は仕組みそのもの、launchctl は launchd に話しかける道具です。launchctl の man は「launchd と話し、daemon・agent・XPC service を管理し調べる」道具としています(macOS 付属の man ページ launchctl(1)・2026-11-01 に見た)。mac で cron を探している人にとっての要は、Apple が cron を勧めていない事です。Apple の文書は、時刻の仕事は launchd で足すのが望ましく、cron は「まだ使えるが勧めない・launchd に置き換えて非推奨」としています(Scheduling Timed Jobs・2026-11-01 に見た)。

見る所launchd(StartCalendarInterval)cron
Apple の扱い時刻の仕事を足す望ましい形まだ使えるが勧めない(launchd に置き換えて非推奨)
書く物1 つの仕事に 1 つの plist(XML)共有の crontab に 1 行
眠っている間に時刻が来た起きた時に走る。何回分たまっても 1 回にまとめる走らない。次の時刻まで待つ
電源が切れている間に時刻が来た走らない。次の時刻まで待つ走らない
落ちた時に起こし直すKeepAlive で決められる無い

出典: Scheduling Timed Jobs(2026-11-01 に見た)・macOS 付属の man ページ launchd.plist(5)(StartCalendarInterval・KeepAlive の項・2026-11-01 に見た)。

StartInterval(N 秒ごと)は、眠っている間に来た回をとばし、仕事が走っている最中に来た回もとばします。StartInterval と StartCalendarInterval は互いを知らず、別々に評価されます(man launchd.plist(5)・2026-11-01 に見た)。

LaunchAgent と LaunchDaemon は、plist を置く場所で決まる

plist の書き方は同じで、daemon か agent かは置くフォルダで決まります。agent はその人がログインしている間だけ動き、daemon は誰がログインしているかに関わらず動きます(Creating Launch Daemons and Agents・2026-11-01 に見た)。

置き場所種類持ち主
~/Library/LaunchAgentsその人の LaunchAgent(自分で置く)その人
/Library/LaunchAgents全員の LaunchAgent(管理者が置く)root
/Library/LaunchDaemons機体全体の LaunchDaemon(管理者が置く)root
/System/Library/LaunchAgents・/System/Library/LaunchDaemonsmacOS 自身の物(触らない)

出典: macOS 付属の man ページ launchctl(1)(FILES の項・2026-11-01 に見た)・Creating Launch Daemons and Agents(持ち主と、group・world に書き込みを許さない事・2026-11-01 に見た)。

要る鍵は Label(launchd の中で一意の名)と ProgramArguments(起こす命令)の 2 つです。plist はグループや他人が書き込めない形(600 か 400)にします(Creating Launch Daemons and Agents・2026-11-01 に見た)。毎朝 9 時に 1 本走らせる LaunchAgent の最小の形は次のとおりです。

<?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.example.morning</string>
  <key>ProgramArguments</key>
  <array><string>/usr/local/bin/morning.sh</string></array>
  <key>StartCalendarInterval</key>
  <dict><key>Hour</key><integer>9</integer><key>Minute</key><integer>0</integer></dict>
  <key>StandardOutPath</key><string>/usr/local/var/log/morning.log</string>
  <key>StandardErrorPath</key><string>/usr/local/var/log/morning.log</string>
</dict>
</plist>

StartCalendarInterval で書かなかった鍵(Day・Weekday・Month など)は「どれでも」と読まれます。Weekday は 0 と 7 が日曜です(man launchd.plist(5)・2026-11-01 に見た)。launchd から起こす物は、自分で fork して親を終わらせる形(daemon 化)にしてはいけません。そうすると launchd はその仕事が死んだと見なします(Creating Launch Daemons and Agents・2026-11-01 に見た)。

launchctl の使い方:bootstrap・bootout・kickstart・print

launchctl load・unload は今も動きますが、man は代わりに bootstrap・bootout・enable・disable を勧めています(man launchctl(1)・2026-11-01 に見た)。宛先は gui/<uid>(ログインしている人の領域)や system の形で書きます。

やりたい事命令man の言う中身
plist を launchd に入れるlaunchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/x.plist道で渡した定義を、その領域に入れる
launchd から外すlaunchctl bootout gui/$(id -u)/<ラベル>定義をその領域から外す
今すぐ走らせるlaunchctl kickstart gui/$(id -u)/<ラベル>起こす条件に関わらず今走らせる。-k は走っている物を殺してから起こし直す
様子を見るlaunchctl print gui/$(id -u)/<ラベル>状態・起こす時の環境・最後の終わり方を出す。出力の形は API ではなく、版で変わる
止めたままにするlaunchctl disable gui/$(id -u)/<ラベル>enable するまで読み込めない。再起動しても残る
plist 無しで常駐させるlaunchctl submit -l <ラベル> -- <命令>設定の file 無しで渡し、落ちたら起こし直させる

出典: macOS 付属の man ページ launchctl(1)(bootstrap・bootout・kickstart・print・enable・disable・submit・load の項・2026-11-01 に見た)。

plist の値を直した時は、ファイルを書き換えただけでは launchd に届きません。当社は直す前に plist の控えを取り、bootout → bootstrap で入れ直し、戻し方(控えの道と戻す 2 行)を記録に残してから次へ進みます(社の実測 2026-10-21・2026-10-22)。

当社の記録:席を Mac に常駐させて踏んだ 7 件

当社の Mac では、AI の席・CI の作業員の組・時刻の便が launchd の上で同時に動いています。2026-09 から 2026-10 に踏んだ 7 件を、起きた事と決まりの組で並べます。

日付起きた事決まり
2026-09-24/tmp 配下に置いた作業フォルダが中身ごと消えた残す物は /tmp 配下に置かない
2026-09-26Hour を含む予約が 16 時間ずれて鳴った新しい機体では Hour を含む最小の予約を 1 回撃って確かめる
2026-10-09kickstart -k で走行中の試験の job 2 本が死んだ子を持つ常駐は AbandonProcessGroup 無しで kickstart しない
2026-10-15nohup … & で起こした組が落ち、残った札が登録を約 2 分半塞いだ常駐させる物は launchd に載せる
2026-10-18launchctl getenv の exit 0 を「在る」と読み、報告を誤った在る無しは出力の長さで測る
2026-10-21常駐が launchd から抜け、作業員待ちの job が 10 本たまったbootstrap で入れ直す。値を変える時は控えと戻し方を残す
2026-10-25時刻の便を手で直に撃ったら、別の口座で走って失敗した手で撃つなら kickstart か、inherited environment を写してから

出典: 社の実測 2026-09-24・2026-09-26・2026-10-09・2026-10-15・2026-10-18・2026-10-21・2026-10-25(当社の運用の記録)。

kickstart -k が、子のプロセスグループごと試験を殺した

CI の試験を走らせる作業員の組を束ねる常駐の LaunchAgent を、launchctl kickstart -k で再起動しました。launchd は常駐の本体だけでなく、子のプロセスグループごと SIGTERM を送り、走行中の試験の job 2 本が死にました(社の実測 2026-10-09)。

launchd.plist の man は、仕事が死ぬと launchd は同じプロセスグループの残りのプロセスを殺し、AbandonProcessGroup を true にするとそれをしなくなる、としています(man launchd.plist(5)・2026-11-01 に見た)。当社は plist に <key>AbandonProcessGroup</key><true/> を足し、bootout → bootstrap で読み直して直しました。決まりは「常駐の子を持つ LaunchAgent は、AbandonProcessGroup 無しで kickstart しない」です(社の実測 2026-10-09)。作業員の組そのものの組み方は self-hosted runner の頁にあります。

抜けていた常駐を、launchctl bootstrap で戻した

CI の作業員の上限を決める常駐の LaunchAgent が、launchd から抜けていました。launchctl print は「Could not find service」を返し、その常駐のログは止まっていました。plist の値は変えずに launchctl bootstrap gui/<uid> <plist> で入れ直すと state = running に戻り、作業員待ちの job は 10 本・最長 20 分から、約 7 分後に 0 本へ引きました(社の実測 2026-10-21)。

止めた記録はどこにも無く、誰かが bootout して戻し忘れた疑いが残りました。ファイルが ~/Library/LaunchAgents に在っても、launchd に入っているとは限りません。「在るか」は plist ではなく launchctl print で聞きます(社の実測 2026-10-21)。

道具のシェルから nohup & で起こした組が、シェルと一緒に落ちた

作業員の組を、道具のシェルから nohup … & で起こしました。組はシェルの終わりと一緒に落ち、残った札が他の組の登録を約 2 分半塞ぎました。本番のデプロイ待ちの最中でした。launchctl submit で起こし直して通りました(社の実測 2026-10-15)。

launchctl の man は、submit を「設定の file 無しで渡し、落ちたら起こし直させる」形としています(man launchctl(1)・2026-11-01 に見た)。当社の決まりは「常駐させる物は launchd に載せる。道具のシェルから & で起こさない」です(社の実測 2026-10-15)。

時刻の便を手で直に撃ったら、launchd の環境変数が無かった

launchd から起こされる時刻の便を、席のシェルから手で直に撃ちました。launchd が渡している環境変数(どの口座で起こすかの印)が無く、便は別の口座で走って、失敗の便を 1 本作りました(社の実測 2026-10-25)。

直に撃つなら、launchctl print gui/<uid>/<ラベル> の inherited environment を写してから撃つか、launchctl kickstart だけを使います。kickstart なら launchd が自分の環境で起こすので、ずれません(社の実測 2026-10-25)。

launchctl getenv は、値が無くても exit 0 を返す

launchctl getenv X は、X が無くても exit 0 を返し、出力の長さが 0 になります。当社はこの exit code を「在る」と読んで設計の報告を誤り、訂正を出しました(社の実測 2026-10-18)。

man は getenv を「launchd が渡す環境変数の値を出す」とし、launchctl は subcommand が成功すれば exit 0 を返すとしています(man launchctl(1)・2026-11-01 に見た)。値が無い事は失敗ではありません。当社は在る無しを出力の長さで測り、在る変数(39 バイト)を陽性対照として一緒に撃ちます(社の実測 2026-10-18)。

v="$(launchctl getenv X)"; [ -n "$v" ] && echo 在る || echo 無い

タイムゾーンを後から変えた Mac で、予約が 16 時間ずれた

起動した後で /etc/localtime を Asia/Tokyo に変えた Mac では、ユーザーの予約を鳴らす側(UserEventAgent(Aqua))が古いタイムゾーンのままでした。StartCalendarInterval の Hour を含む予約が 16 時間ずれて鳴りました。Minute だけの予約は鳴るので、気づきにくい壊れ方です(社の実測 2026-09-26)。

手当ては、UserEventAgent(Aqua) を再起動し(KeepAlive ですぐ戻る)、その後で予約を bootout → bootstrap で入れ直す事です。launchctl print に registered と出ていても、日本時間どおりに鳴る証拠にはなりません(社の実測 2026-09-26)。man も print の出力は API ではないとしています(man launchctl(1)・2026-11-01 に見た)。

/tmp に置いた作業フォルダが、記録を残さず消えた

macOS の /tmp の掃除(LaunchDaemon の com.apple.tmp_cleaner・毎日 0 時)は、/tmp 配下の 3 つの時刻が全部 4 日以上前のファイルを、記録を残さず消します。作業フォルダを /tmp 配下に置いたら、中身ごと消えました(社の実測 2026-09-24)。launchd に載せた常駐の作業場所や控えの plist は、/tmp ではなく自分の Library かホームの下に置きます。

新しい Mac に常駐と予約を載せる前に撃つ確かめ

上の 7 件から、当社が新しい機体で載せる前に撃つ順を並べます。

  1. Hour を含む最小の予約(数分後の時刻に 1 行書くだけの物)を 1 本載せ、日本時間どおりに鳴るかを見る。ずれたら UserEventAgent(Aqua) を再起動して入れ直す(社の実測 2026-09-26)
  2. 子を持つ常駐の plist に AbandonProcessGroup が true で入っているかを見る(社の実測 2026-10-09)
  3. 常駐は全部 launchd に載せ、シェルの & で起こした物が残っていないかを見る(社の実測 2026-10-15)
  4. 載せた常駐が launchctl print で state = running かを見る。plist が在るだけでは入っていない(社の実測 2026-10-21)
  5. 環境変数の確かめは exit code ではなく出力の長さで、在る変数を陽性対照にして撃つ(社の実測 2026-10-18)
  6. 作業場所と plist の控えを /tmp 配下に置いていないかを見る(社の実測 2026-09-24)

試験が揺れて赤になった時の見分け方は flaky test の頁に当社の記録があります。

よくある質問

Mac で cron は使えますか?

使えますが、Apple は勧めていません。Apple の文書は cron を launchd に置き換えて非推奨とし、時刻の仕事は launchd の StartCalendarInterval で足すのが望ましいとしています(Scheduling Timed Jobs・2026-11-01 に見た)。launchd は眠っている間に来た時刻の分を、起きた時に走らせます。

launchctl load と launchctl bootstrap はどちらを使えばいいですか?

bootstrap です。launchctl の man は load・unload の代わりに bootstrap・bootout・enable・disable を勧めています(man launchctl(1)・2026-11-01 に見た)。当社は plist を直した時も、bootout → bootstrap で入れ直しています(社の実測 2026-10-21)。

LaunchAgent と LaunchDaemon の違いは何ですか?

LaunchAgent はその人がログインしている間だけ動き、LaunchDaemon は誰がログインしているかに関わらず機体全体で動きます。plist の書き方は同じで、置くフォルダ(~/Library/LaunchAgents か /Library/LaunchDaemons など)で決まります(Creating Launch Daemons and Agents・2026-11-01 に見た)。

launchctl kickstart -k で再起動したら、子のプロセスまで止まりました

launchd は仕事が死ぬと、同じプロセスグループの残りのプロセスも殺します。plist に AbandonProcessGroup を true で足すと、それをしなくなります(man launchd.plist(5)・2026-11-01 に見た)。当社はこれで試験の job 2 本を失い、AbandonProcessGroup を足して bootout → bootstrap で直しました(社の実測 2026-10-09)。

StartCalendarInterval の予約が、決めた時刻に鳴りません

まず眠りと電源を見ます。眠っていれば起きた時に走り、電源が切れていれば次の時刻まで走りません(Scheduling Timed Jobs・2026-11-01 に見た)。当社では、起動した後でタイムゾーンを変えた Mac で Hour を含む予約が 16 時間ずれ、UserEventAgent(Aqua) の再起動と入れ直しで直りました(社の実測 2026-09-26)。

出典

  1. Apple Developer「Scheduling Timed Jobs」(Daemons and Services Programming Guide) https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ScheduledJobs.html(2026-11-01 に見た・launchd が望ましい事・cron の非推奨・眠りと電源の扱い)
  2. Apple Developer「Creating Launch Daemons and Agents」(Daemons and Services Programming Guide) https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html(2026-11-01 に見た・daemon と agent の違いと置き場所・持ち主と 600/400・要る鍵・daemon 化しない事・KeepAlive)
  3. macOS 付属の man ページ launchd.plist(5)(man launchd.plist・2026-11-01 に見た・StartCalendarInterval・StartInterval・KeepAlive・AbandonProcessGroup)
  4. macOS 付属の man ページ launchctl(1)(man launchctl・2026-11-01 に見た・領域の書き方・bootstrap・bootout・kickstart・print・enable・disable・submit・getenv・load の代わり・FILES・EXIT STATUS)
  5. 社の実測 2026-09-24(当社の運用の記録・/tmp の掃除で作業フォルダが消えた事)
  6. 社の実測 2026-09-26(当社の運用の記録・タイムゾーンを変えた Mac で Hour を含む予約が 16 時間ずれた事と手当て)
  7. 社の実測 2026-10-09(当社の運用の記録・kickstart -k で試験の job 2 本が死んだ事と AbandonProcessGroup)
  8. 社の実測 2026-10-15(当社の運用の記録・nohup & で起こした組が落ち、登録を約 2 分半塞いだ事)
  9. 社の実測 2026-10-18(当社の運用の記録・launchctl getenv の exit 0 と出力の長さ・陽性対照 39 バイト)
  10. 社の実測 2026-10-21・2026-10-22(当社の運用の記録・抜けた常駐の bootstrap と待ちの job の引き・plist の控えと戻し方)
  11. 社の実測 2026-10-25(当社の運用の記録・手で直に撃った便が別の口座で走った事)