macOS には「決まった時刻にスクリプトを動かす」「ログイン時にプログラムを起動する」
「常駐させて動かし続ける」といった処理を担う launchd という仕組みが標準で入っている。
この launchd に「いつ・何を・どう動かすか」を伝えて操作するためのコマンドが
launchctl で、設定は plist という XML ファイルに書く。
Linux の cron や systemd に近い役割だが、macOS では launchd がこれらをまとめて引き受けている。 自分で実際に使っていたところ、いくつかわかりにくい挙動で詰まった部分があった。そこで、launchd の基本から実際に詰まったところまでまとめて記録しておく。
本記事の内容は macOS Sequoia 15.7.7 で動作を確認している。
launchctl と launchd とは
launchd は macOS が起動するときに最初に立ち上がる、すべてのプロセスの親にあたる
管理プロセス。システムの起動処理、常駐サービス(デーモン)の管理、ユーザーがログインしたときの
アプリ起動、定期実行のスケジューリングなどを一手に引き受けている。普段は意識しないが、Mac の中で
動いているバックグラウンド処理の多くはこの launchd が面倒を見ている。
launchctl は、その launchd に対して「このジョブを登録して」「今すぐ動かして」
「状態を見せて」と指示を出すためのコマンドラインツール。自分でジョブを追加したいときは、
やりたいことを plist ファイルに書き、それを launchctl で launchd に登録する、
という流れになる。
cron との違いも押さえておくとよい。cron は「定期実行」に特化しているが、launchd は定期実行に加えて 「ログイン時に1回だけ実行」「落ちたら再起動して常駐」「特定フォルダが変化したら実行」など、 起動のきっかけを幅広く指定できる。ログ出力先や環境変数も plist にまとめて書けるので、 cron より少し記述は多いが、その分こまかく制御できる。
LaunchAgent と LaunchDaemon の違い
launchd で動かすジョブには LaunchAgent と LaunchDaemon の 2 種類があり、どちらにするかで「いつ・誰の権限で動くか」が変わる。最初はこの違いでつまずきやすいので、 先に整理しておく。
| 種類 | 動くタイミング | 実行権限 | 向いている用途 |
|---|---|---|---|
| LaunchAgent | ユーザーがログインしている間 | ログインユーザー | 個人の定期処理、GUI を伴う処理、通知 |
| LaunchDaemon | OS 起動時から(ログイン不要) | root(システム) | 常駐サービス、全ユーザー共通の処理 |
個人で「毎日この時刻にスクリプトを回したい」という程度なら、まず LaunchAgent を 選んでおけばよい。画面に誰もログインしていない状態でも動かしたい、root 権限が必要、といった場合に LaunchDaemon を検討する。置き場所もこの種類によって変わる(後述の 配置場所を参照)。
plist の基本構造
plist(Property List)は macOS の設定ファイル形式で、launchd の設定も XML 形式の plist で書く。
中身は <key>(項目名)と、その値(<string> や
<integer> など)のペアを <dict>(辞書)の中に並べていく構造になっている。
次は実用的な項目をひととおり入れた例で、各行にコメントで意味を添えている。
<?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.myjob</string>
<!-- 実行するコマンドと引数。1要素目が実行ファイル -->
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/my-script.sh</string>
<string>--option</string>
<string>value</string>
</array>
<!-- 実行スケジュール(毎日9時0分) -->
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>9</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<!-- 実行時のカレントディレクトリ -->
<key>WorkingDirectory</key>
<string>/usr/local/var/myjob</string>
<!-- 環境変数を渡す -->
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin</string>
</dict>
<!-- 標準出力・標準エラーの書き出し先 -->
<key>StandardOutPath</key>
<string>/tmp/myjob.log</string>
<key>StandardErrorPath</key>
<string>/tmp/myjob.err</string>
</dict>
</plist>
値の型は決まったタグで表す。文字列は <string>、整数は <integer>、
真偽値は <true/> / <false/>、複数の値を並べる配列は
<array>、入れ子の辞書は <dict> を使う。型を間違える
(たとえば数値を <string> で書く)と読み込みに失敗するので、後述の
plutil -lint で必ず確認する。
plist で使えるキー一覧(取りうる値)
launchd の plist で指定できる主なキーをまとめる。実際には Label と
ProgramArguments(または Program)さえあれば動くので、残りは
必要に応じて足していけばよい。
必ず/よく使うキー
| キー | 型 | 説明 |
|---|---|---|
Label | 文字列 | ジョブの一意な識別子(必須)。他と重複しなければ任意の文字列でよいが、慣習として逆ドメイン形式(com.example.myjob)にし、plist のファイル名とも揃える。 |
ProgramArguments | 文字列の配列 | 実行するコマンドと引数。1 要素目が実行ファイルのパス、2 要素目以降が引数。コマンドは絶対パスで書く。 |
Program | 文字列 | 実行ファイルのパスだけを指定する簡易版。引数が要らないとき用。ProgramArguments があればそちらが優先される。 |
RunAtLoad | 真偽値 | true でロードした瞬間に 1 回実行する。動作確認や、起動直後に走らせたい処理に使う。 |
StartInterval | 整数(秒) | 指定した秒数ごとに繰り返し実行する。例:3600 で 1 時間ごと。 |
StartCalendarInterval | 辞書 / 配列 | cron のように時刻・曜日を指定して実行する。詳細は次節。 |
StandardOutPath | 文字列 | 標準出力(コマンドの通常の出力)を書き出すファイルパス。 |
StandardErrorPath | 文字列 | 標準エラー(エラーメッセージ)を書き出すファイルパス。失敗の原因調査に使うので入れておくとよい。 |
WorkingDirectory | 文字列 | スクリプトを実行する際のカレントディレクトリ。相対パスを扱うスクリプトで重要。 |
EnvironmentVariables | 辞書 | ジョブに渡す環境変数。launchd 経由では PATH が最小限になりがちなので、必要なら明示する。 |
起動制御・常駐に使うキー
| キー | 型 | 説明 |
|---|---|---|
KeepAlive | 真偽値 / 辞書 | true でプロセスが終了するたびに再起動(常駐用)。辞書で条件を絞れる(後述)。 |
Disabled | 真偽値 | true でジョブを無効化。load しても起動しない。 |
LaunchOnlyOnce | 真偽値 | true でロード中に一度だけ実行する。 |
ThrottleInterval | 整数(秒) | 連続再起動の最小間隔。デフォルトは 10 秒。短時間で何度も落ちるジョブの暴走を防ぐ。 |
ExitTimeOut | 整数(秒) | 停止シグナルを送ってから強制終了するまでの猶予。デフォルトは 20 秒。 |
ProcessType | 文字列 | スケジューラ上の優先度。Background / Standard / Adaptive / Interactive のいずれか。重い処理は Background にする。 |
Nice | 整数(-20〜20) | プロセスの nice 値(CPU 優先度)。数値が小さいほど優先される。 |
WatchPaths | 文字列の配列 | 指定したパス(ファイル / フォルダ)に変更があると実行する。 |
QueueDirectories | 文字列の配列 | 指定ディレクトリにファイルが存在する間、実行を繰り返す。フォルダ監視処理に使う。 |
StartOnMount | 真偽値 | true で、ボリュームがマウントされるたびに実行する。 |
UserName / GroupName | 文字列 | 実行するユーザー / グループ。主に LaunchDaemon で「root 以外で動かしたい」ときに使う。 |
RootDirectory | 文字列 | chroot して実行するルートディレクトリ。 |
AbandonProcessGroup | 真偽値 | true で、親プロセス終了時に子プロセスを道連れにせず残す。 |
StartCalendarInterval のスケジュール指定
定期実行で中心になるのが StartCalendarInterval。中に時刻や曜日のフィールドを書き、
指定したフィールドが一致したときに実行される。書かなかったフィールドはワイルドカード
(毎回マッチ)扱いになる。たとえば Minute だけ書けば「毎時その分」、
Hour と Minute を書けば「毎日その時刻」になる。
各フィールドの取りうる値は次のとおり。
| フィールド | 取りうる値 | 意味 |
|---|---|---|
Minute | 0〜59 | 分 |
Hour | 0〜23 | 時(24時間制) |
Day | 1〜31 | 日(月の中の日付) |
Weekday | 0〜7 | 曜日。0 と 7 が日曜、1=月曜 … 6=土曜 |
Month | 1〜12 | 月 |
複数の時刻に実行したいときは、StartCalendarInterval を辞書ではなく辞書の配列にして並べる。
単純に「一定間隔ごと」でよければ StartInterval(秒数)の方が手軽。
<!-- 毎日 AM 9:30 -->
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key><integer>9</integer>
<key>Minute</key><integer>30</integer>
</dict>
<!-- 毎週月曜 AM 10:00(Weekday は 0/7=日曜, 1=月曜 … 6=土曜) -->
<key>StartCalendarInterval</key>
<dict>
<key>Weekday</key><integer>1</integer>
<key>Hour</key><integer>10</integer>
<key>Minute</key><integer>0</integer>
</dict>
<!-- 毎月1日 AM 0:00 -->
<key>StartCalendarInterval</key>
<dict>
<key>Day</key><integer>1</integer>
<key>Hour</key><integer>0</integer>
<key>Minute</key><integer>0</integer>
</dict>
<!-- 1日に複数回(配列で並べる。9時と18時) -->
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Hour</key><integer>9</integer>
<key>Minute</key><integer>0</integer>
</dict>
<dict>
<key>Hour</key><integer>18</integer>
<key>Minute</key><integer>0</integer>
</dict>
</array>
<!-- 一定間隔で繰り返す(StartInterval は秒数。3600=1時間ごと) -->
<key>StartInterval</key>
<integer>3600</integer> 注意:Mac がスリープしている時刻に予定があった場合、その回は飛ばされる(cron と同じ)。ただし launchd は復帰後に「逃した実行」を 1 回だけまとめて走らせる挙動があるので、起きていなかった分が 丸ごと消えるとは限らない。
KeepAlive で常駐させる
「スクリプトを動かし続けたい」「落ちても自動で立ち上げ直したい」という常駐用途では
KeepAlive を使う。true にすると、プロセスが終了するたびに launchd が
再起動する。条件を絞りたいときは辞書で書く。
<!-- プロセスが落ちたら自動で再起動する(常駐用) -->
<key>KeepAlive</key>
<true/>
<!-- 条件つきの再起動(異常終了したときだけ再起動する) -->
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict> 辞書で指定できる主な条件キーは次のとおり。
| サブキー | 型 | 意味 |
|---|---|---|
SuccessfulExit | 真偽値 | true なら正常終了後も再起動、false なら異常終了したときだけ再起動。 |
Crashed | 真偽値 | true でクラッシュ時に再起動する。 |
NetworkState | 真偽値 | true でネットワークが有効なときだけ起動する。 |
PathState | 辞書 | 指定したパスが存在する/しない状態に応じて起動を制御する。 |
plist の配置場所
plist をどのディレクトリに置くかで、それが LaunchAgent になるか LaunchDaemon になるか、
誰の権限で・いつから動くかが決まる。個人利用なら ~/Library/LaunchAgents/ を使うのが基本。
| ディレクトリ | 種類 | 対象 | 実行ユーザー |
|---|---|---|---|
~/Library/LaunchAgents/ | Agent | そのユーザーがログイン中 | ログインユーザー |
/Library/LaunchAgents/ | Agent | 全ユーザー(要管理者権限) | ログインユーザー |
/Library/LaunchDaemons/ | Daemon | システム全体(起動時から) | root |
/System/Library/... | — | macOS 組み込み(触らない) | — |
/System/Library/ 以下は OS 標準のジョブが入っている領域なので、自分のジョブを置いたり
中身を書き換えたりしない。自作のものは ~/Library/LaunchAgents/ か、root で動かしたいときだけ
/Library/LaunchDaemons/ に置く。
はじめてのジョブを動かす手順
まずは確実に動く最小構成で一度通してみるのが理解の近道。次の plist を
~/Library/LaunchAgents/com.example.hello.plist として保存する。
ロードした瞬間に /bin/echo を 1 回実行し、結果を /tmp/hello.log に書き出すだけのジョブ。
plist ファイルは、フォルダが無ければ mkdir -p ~/Library/LaunchAgents で作ってから、
nano ~/Library/LaunchAgents/com.example.hello.plist(または VS Code などの好きなエディタ)で
新規作成し、次の内容を貼り付けて保存する。特別なツールは要らず、ただのテキストファイルとして書けばよい。
<?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.hello</string>
<key>ProgramArguments</key>
<array>
<string>/bin/echo</string>
<string>hello launchd</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/hello.log</string>
</dict>
</plist> 保存したら、次の流れで動作を確認する。
# 1) フォルダが無ければ作る
mkdir -p ~/Library/LaunchAgents
# 2) 上の最小plistを com.example.hello.plist として保存
# (Label と同じ名前にするのが慣習)
# 3) 文法チェック(OK が出れば書式は正しい)
plutil -lint ~/Library/LaunchAgents/com.example.hello.plist
# 4) ロード(RunAtLoad が true なのでロード時に1回実行される)
launchctl load ~/Library/LaunchAgents/com.example.hello.plist
# 5) 結果を確認
cat /tmp/hello.log
# 6) 後片付け(登録を解除する)
launchctl unload ~/Library/LaunchAgents/com.example.hello.plist launchctl load は成功しても何も表示しない(無言で完了する)。エラーが出ていなければ
登録できているので、/tmp/hello.log に行が追記されているか確認する。書き込まれていれば成功。
ここまで通せれば、あとは
ProgramArguments を自分のスクリプトに、RunAtLoad を
StartCalendarInterval に置き換えていくだけで定期実行に発展させられる。
基本コマンド(load / unload / list)
plist を配置したら、launchctl で launchd に登録(ロード)する。登録を外すのがアンロード。
状態確認や手動実行もこのコマンドから行う。
# ロード(plistをlaunchdに登録する)
launchctl load ~/Library/LaunchAgents/com.example.myjob.plist
# アンロード(登録を解除する)
launchctl unload ~/Library/LaunchAgents/com.example.myjob.plist
# 登録済みジョブの一覧を見る
launchctl list
# Label でしぼり込む
launchctl list | grep com.example
# スケジュールを待たずに今すぐ実行する
launchctl start com.example.myjob
# 実行中のジョブを止める
launchctl stop com.example.myjob
plist を編集したときは、一度 unload してから load し直さないと変更が反映されない
点に注意。load し直さずに中身だけ書き換えても、launchd は古い設定で動き続ける。
bootstrap / bootout とドメインの考え方
macOS 10.10 Yosemite 以降では、load / unload に代わる新しいコマンドとして
bootstrap / bootout が用意されている。新しい macOS ではこちらが推奨で、
後述の SSH 経由の操作でも必要になる。
新コマンドの肝は「ドメイン」という考え方。launchd はジョブを、それが属する場所 (セッション)ごとに区切って管理しており、操作するときにどのドメインかを明示する。よく使うのは次の 2 つ。
gui/<uid>… GUI でログイン中のユーザーセッション。LaunchAgent はここ。<uid>はid -uで得られる数値。system… システム全体。LaunchDaemon はここ。
# bootstrap(load に相当・ドメインを明示する)
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.myjob.plist
# bootout(unload に相当)
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.example.myjob.plist
# ジョブの詳細な状態を見る(終了コード・実行状況など)
launchctl print gui/$(id -u)/com.example.myjob
# 今すぐ実行する(再起動つき)
launchctl kickstart -k gui/$(id -u)/com.example.myjob kickstart -k はジョブを今すぐ起動するコマンドで、-k を付けると実行中の場合は
一度止めてから起動し直す。動作確認に便利。
⚠️ Desktop 配下のスクリプトが実行できない
実際に動かしたときにハマった点。まず、plist の ProgramArguments に
~/Desktop 配下のスクリプトを指定すると、ロードはエラーなく成功するのに、いざ実行される
時刻になっても何も起きないことがある。エラーも出ないので原因が分かりにくい。
<!-- ~/Desktop のスクリプトを呼び出す例 -->
<!-- ロードは成功するのに、実行されない/エラーも出ない -->
<key>ProgramArguments</key>
<array>
<string>/Users/username/Desktop/myscript.sh</string>
</array>
原因は macOS Catalina(10.15)以降の TCC(Transparency, Consent, and Control)によるサンドボックス保護。
~/Desktop・~/Documents・~/Downloads はアクセスに明示的な許可が
必要な保護対象フォルダになっており、GUI セッションの外で動く launchd はそのままではここを読めない。
結果として Operation not permitted になるか、何も表示されず静かに失敗する。
「システム設定 → プライバシーとセキュリティ → フルディスクアクセス」に launchctl や
launchd を足しても解決しない。確実なのは、plist が参照するスクリプト本体を保護対象外の
ディレクトリ(~/bin など)へ移すこと。
# 避けるべき場所(TCC の保護対象)
~/Desktop/myscript.sh
~/Documents/myscript.sh
~/Downloads/myscript.sh
# スクリプトを保護対象外へ移す
mkdir -p ~/bin
mv ~/Desktop/myscript.sh ~/bin/myscript.sh
chmod +x ~/bin/myscript.sh
# plist 側のパスも書き換える
<key>ProgramArguments</key>
<array>
<string>/Users/username/bin/myscript.sh</string>
</array> 「実行ファイル」と「処理対象データ」を分けて考える
ここがいちばん分かりにくいところ。「Desktop が絡むと何もかも動かない」と思いがちだが、実際にダメなのは
launchd が直接起動する実行ファイル(plist の ProgramArguments が指すスクリプト本体)
が保護フォルダにある場合だけ。スクリプトが 処理対象として読み書きするデータファイル が
~/Desktop 配下にあること自体は問題にならない。
理由は、止まる場所が違うから。前者は launchd がスクリプトを起動する「入口」で保護に阻まれ、プロセスが 起動すらできない。後者はスクリプト本体が保護外にあるので起動は成功し、起動したプロセスが (LaunchAgent ならユーザーのログインセッション内・ユーザー権限で動くため)Desktop のファイルを 読み書きするのは通る。
| 構成 | 結果 | 理由 |
|---|---|---|
plist が指す実行ファイル本体が ~/Desktop 配下 | ✗ 起動できない | launchd が保護フォルダの実行ファイルを起動する入口で阻まれる(無言で失敗) |
実行ファイルは保護外(~/bin 等)・処理対象データが ~/Desktop 配下 | ✓ 動く | 起動は保護外なので成功し、その後のデータ読み書きはユーザー権限(LaunchAgent)で通る |
# OK な構成:実行ファイルは保護外、処理対象データは Desktop 配下でもよい
~/bin/backup.sh # ← launchd が起動するスクリプト本体(保護外に置く)
# backup.sh の中身(処理対象のデータは ~/Desktop でも動く)
#!/bin/bash
ls ~/Desktop/*.csv >> /tmp/backup.log # Desktop のファイルを読む処理は通る
# plist が指すのは「スクリプト本体」。ここが保護外なら起動できる
<key>ProgramArguments</key>
<array>
<string>/Users/username/bin/backup.sh</string>
</array>
補足:後者も TCC が厳しい環境やフルディスクアクセス未付与の場合は、データ側のアクセスが
Operation not permitted で拒否されることがある。その場合は「システム設定 →
プライバシーとセキュリティ → フルディスクアクセス」に、スクリプトを動かす実体
(ターミナルや /bin/bash など)を許可すると通るようになる。なお LaunchDaemon
(root 実行)はユーザーの Desktop に対する権限が無いため、この構成でもデータ側で弾かれやすい。
⚠️ SSH からの操作で挙動が変わる
こちらも実際にハマった点、 SSH 経由の操作。SSH でリモートログインした状態で
launchctl load を実行すると、手元のターミナルとは違う結果になることがある。
$ launchctl load ~/Library/LaunchAgents/com.example.myjob.plist
# → "Could not find domain for this service"
# → "Bootstrap failed: 5: Input/output error"
# → 何も起きない(無言で失敗)
これは前述のドメインが関係している。GUI ログインのセッションは
gui/<uid>、SSH のセッションは user/<uid> という別ドメインで、
LaunchAgent は GUI セッション側に属する。SSH のドメインからは、GUI 側のジョブをそのまま
操作できないため、対象が見つからずエラーになる。
SSH から操作する場合は、bootstrap / bootout / print に
gui/<uid> を明示的に渡す。手元のターミナルで使っていた load を
そのまま SSH 越しに打つとハマるので、SSH のときは新コマンド+ドメイン指定に切り替えると覚えておく。
# 自分の UID を確認する
id -u # → 例: 501
# bootstrap / bootout は gui/<uid> ドメインを明示する
launchctl bootstrap gui/501 ~/Library/LaunchAgents/com.example.myjob.plist
launchctl bootout gui/501 ~/Library/LaunchAgents/com.example.myjob.plist
# 状態確認
launchctl print gui/501/com.example.myjob
# 今すぐ実行
launchctl kickstart -k gui/501/com.example.myjob
# それでも動かないとき:GUI セッションが存在するか確認する
who # ログイン中のセッション一覧(console 行があれば GUI ログイン中)
それでも動かない場合は、そもそも GUI セッションが存在しない(誰も画面にログインしていない)
ケースが考えられる。その場合は素直に LaunchDaemon(/Library/LaunchDaemons/ に置いて
sudo launchctl bootstrap system ...)として root 権限で動かす方が確実。
デバッグ・ログ確認
想定どおり動かないときの調べ方。まず plutil -lint で plist の文法を確認し、次に
launchctl list の終了コードを見る。さらに追うなら launchctl print で
詳細を、log コマンドで launchd 周りのログを見る。
# plist の文法チェック(まずこれ)
plutil -lint ~/Library/LaunchAgents/com.example.myjob.plist
# → OK: /Users/.../com.example.myjob.plist: OK
# 登録状態と終了コードを見る
launchctl list | grep com.example
# → - 78 com.example.myjob ← 78 = エラーあり
# → - 0 com.example.myjob ← 0 = 正常終了
# → 501 - com.example.myjob ← 数字 = 実行中の PID
# ジョブの詳細(最後の終了コード・パスなど)
launchctl print gui/$(id -u)/com.example.myjob
# launchd 関連のログをリアルタイムで見る
log stream --predicate 'subsystem == "com.apple.launchd"' --level debug
# 過去ログをさかのぼる(スクリプト名で絞り込み)
log show --predicate 'processImagePath contains "myscript"' --last 1h launchctl list の出力は左から「PID・最後の終了コード・Label」の順。終了コードの主なものは次のとおり。
| 終了コード | 意味 |
|---|---|
0 | 正常終了 |
1 | 一般的なエラー |
78 | 設定エラー(plist の記述ミスが多い) |
127 | コマンド・スクリプトが見つからない(パスの誤り) |
Operation not permitted | 権限エラー(TCC・Desktop 問題など) |
つまずきやすいところ
- plist を書き換えただけでは反映されない。
unload→load(またはbootout→bootstrap)でロードし直す。 - 「ロード成功」と出ても、スクリプト本体が保護フォルダ(Desktop など)にあると静かに失敗する。エラーが出ないぶん気づきにくい。
- plist 内のパスでは
~が展開されない。~/bin/x.shではなく/Users/ユーザー名/bin/x.shのように絶対パスで書く。 - コマンドやスクリプトは絶対パスで書く。launchd 経由では
PATHが最小限なので、pythonのような名前だけの指定は見つからないことがある。 - SSH からは
gui/<uid>を明示しないと操作できない。ローカルで動いたコマンドをそのまま持ち込むとハマる。 StartCalendarIntervalはフィールドを書かないと「毎回」扱いになる。Minuteを書き忘れると毎分実行になってしまう点に注意。
参考(一次情報)
各キーの正式な仕様や、ここで触れていないオプションを確認したいときは、macOS に標準で入っている man ページが一次情報として確実。ターミナルで次を実行すれば、その環境の launchd に対応した 正確な内容を参照できる。
man launchctl # launchctl コマンドのリファレンス
man launchd.plist # plist で指定できるキーの正式な仕様(型・取りうる値)
man launchd # launchd 本体の説明
特に man launchd.plist は、本記事の「plist で使えるキー一覧」で
挙げた各キーの型・取りうる値・デフォルト挙動が正式に定義されている。記事と挙動が食い違う場合は、
手元の環境の man ページを優先してほしい。