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/LaunchDaemons | macOS 自身の物 | (触らない) |
出典: 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-26 | Hour を含む予約が 16 時間ずれて鳴った | 新しい機体では Hour を含む最小の予約を 1 回撃って確かめる |
| 2026-10-09 | kickstart -k で走行中の試験の job 2 本が死んだ | 子を持つ常駐は AbandonProcessGroup 無しで kickstart しない |
| 2026-10-15 | nohup … & で起こした組が落ち、残った札が登録を約 2 分半塞いだ | 常駐させる物は launchd に載せる |
| 2026-10-18 | launchctl 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 件から、当社が新しい機体で載せる前に撃つ順を並べます。
- Hour を含む最小の予約(数分後の時刻に 1 行書くだけの物)を 1 本載せ、日本時間どおりに鳴るかを見る。ずれたら UserEventAgent(Aqua) を再起動して入れ直す(社の実測 2026-09-26)
- 子を持つ常駐の plist に
AbandonProcessGroupが true で入っているかを見る(社の実測 2026-10-09) - 常駐は全部 launchd に載せ、シェルの
&で起こした物が残っていないかを見る(社の実測 2026-10-15) - 載せた常駐が
launchctl printで state = running かを見る。plist が在るだけでは入っていない(社の実測 2026-10-21) - 環境変数の確かめは exit code ではなく出力の長さで、在る変数を陽性対照にして撃つ(社の実測 2026-10-18)
- 作業場所と 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)。
出典
- 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 の非推奨・眠りと電源の扱い)
- 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)
- macOS 付属の man ページ launchd.plist(5)(
man launchd.plist・2026-11-01 に見た・StartCalendarInterval・StartInterval・KeepAlive・AbandonProcessGroup) - macOS 付属の man ページ launchctl(1)(
man launchctl・2026-11-01 に見た・領域の書き方・bootstrap・bootout・kickstart・print・enable・disable・submit・getenv・load の代わり・FILES・EXIT STATUS) - 社の実測 2026-09-24(当社の運用の記録・/tmp の掃除で作業フォルダが消えた事)
- 社の実測 2026-09-26(当社の運用の記録・タイムゾーンを変えた Mac で Hour を含む予約が 16 時間ずれた事と手当て)
- 社の実測 2026-10-09(当社の運用の記録・kickstart -k で試験の job 2 本が死んだ事と AbandonProcessGroup)
- 社の実測 2026-10-15(当社の運用の記録・nohup & で起こした組が落ち、登録を約 2 分半塞いだ事)
- 社の実測 2026-10-18(当社の運用の記録・launchctl getenv の exit 0 と出力の長さ・陽性対照 39 バイト)
- 社の実測 2026-10-21・2026-10-22(当社の運用の記録・抜けた常駐の bootstrap と待ちの job の引き・plist の控えと戻し方)
- 社の実測 2026-10-25(当社の運用の記録・手で直に撃った便が別の口座で走った事)