Claude Codeをlaunchdで定期実行するplist — 毎朝の登録と動作確認
macOSのlaunchdでclaude -pを毎朝動かすplistの書き方を、StartCalendarInterval・PATH・ログの出し先・launchctlでの登録と試走まで一通りまとめます。
Macでclaude -pを毎朝7時30分に動かすなら、スケジューラーはlaunchdが第一候補です。Appleのドキュメントも、時刻指定のジョブはlaunchdを推奨し、cronは非推奨としています。plistを1枚置いてlaunchctlで登録すれば、スリープ明けの取りこぼしもある程度は拾ってくれます。
ただ、plistを書いただけでは動かない場面が多くあります。ターミナルと環境が違う、ログがどこにも出ない、認証が通らない、の3つです。この記事は、この3つをつぶしながら、登録から試走までを順に進めます。
毎朝7時30分にclaude -pを動かすplistの全体像
先に完成形を示します。~/Library/LaunchAgents/com.example.claude-morning.plistとして保存する想定です。
<?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.claude-morning</string>
<key>ProgramArguments</key>
<array>
<string>/Users/you/bin/claude-morning.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>7</integer>
<key>Minute</key>
<integer>30</integer>
</dict>
<key>StandardOutPath</key>
<string>/Users/you/Library/Logs/claude-morning/out.log</string>
<key>StandardErrorPath</key>
<string>/Users/you/Library/Logs/claude-morning/err.log</string>
</dict>
</plist>要点は3つです。
Labelは必須で、ジョブを一意に識別します。ファイル名と同じにしておくと管理しやすくなりますProgramArgumentsにはclaudeを直接書かず、自作のラッパースクリプトを置きます。理由は環境変数の節で説明しますRunAtLoadは書きません。launchctlでロードした瞬間にも実行されてしまうので、試走は別の手段で行います
ユーザー名やパスは自分の環境に置き換えてください。
plistを動かすまでの流れ
- 1
ラッパースクリプトを書く
claude -pを呼ぶシェルスクリプトを用意し、実行権限を付けます。 - 2
plistを書いて構文を確かめる
~/Library/LaunchAgents/に置き、plutil -lintで形式の誤りを先に潰します。 - 3
launchctlで登録する
launchctl bootstrapでユーザーのドメインに読み込ませます。 - 4
待たずに試走する
launchctl kickstartで時刻を待たずに1回動かし、ログと終了ステータスを見ます。
StartCalendarInterval — 時刻・曜日の指定と取りこぼしの扱い
StartCalendarIntervalは、時刻を辞書で指定するキーです。Minute(0〜59)、Hour(0〜23)、Day(1〜31)、Weekday、Month(1〜12)が使えます。書かなかったキーはワイルドカード扱いなので、HourとMinuteだけなら毎日その時刻に動きます。cronの書式に近い意味づけです。
平日だけ動かしたいときは、辞書を配列にして曜日ごとに並べます。
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Weekday</key><integer>1</integer>
<key>Hour</key><integer>7</integer>
<key>Minute</key><integer>30</integer>
</dict>
<dict>
<key>Weekday</key><integer>2</integer>
<key>Hour</key><integer>7</integer>
<key>Minute</key><integer>30</integer>
</dict>
<!-- 3〜5(水〜金)も同じ形で並べる -->
</array>Weekdayは0と7がどちらも日曜です。ここで1つ落とし穴があります。DayとWeekdayを両方指定すると、AND条件ではなく、どちらか一方が合えば起動します。「毎月1日、かつ月曜」のつもりで書くと、毎月1日と毎週月曜の両方で動いてしまいます。
月に1回だけ動かすなら、DayとHourとMinuteだけを書きます。次の例は毎月1日の朝9時です。Weekdayを書かないので、曜日は問われません。
<key>StartCalendarInterval</key>
<dict>
<key>Day</key><integer>1</integer>
<key>Hour</key><integer>9</integer>
<key>Minute</key><integer>0</integer>
</dict>1日に2回動かしたいときも、辞書を2つ並べた配列にします。朝7時30分と夕方18時なら、HourとMinuteの組を2つ書くだけです。StartInterval(秒間隔)を併用すると、man launchd.plistによれば2つのキーは互いを意識せず別々に評価されます。時刻指定のジョブに間隔指定を足すと、思わぬ時刻にも動きます。どちらか一方にそろえるほうが、後から読み返しやすくなります。
スリープとの関係は、運用で効いてきます。手元のmacOS 26.3.1のman launchd.plistには次の挙動が書かれています。
- cronと違い、スリープ中に予定時刻を過ぎたジョブは、次に復帰したときに起動する
- 復帰までに複数回ぶんの予定が過ぎていても、1回に集約される
一方、Appleのガイドは電源が切れていた場合を別に扱っています。電源オフの間に過ぎた予定は実行されず、次の指定時刻まで待ちます。毎晩Macの電源を落とす人が「午前1時」を指定すると、そのジョブは一度も動きません。毎朝7時30分のジョブでも、出社前に電源が入っていなければ、その日は飛びます。
PATHと認証 — ターミナルで動くのにlaunchdで動かない理由
launchdから起動したジョブは、ログインシェルを通りません。~/.zshrcや~/.zprofileで設定したPATH、nvmやHomebrewの初期化、exportした変数は引き継がれないものとして書くのが安全です。ProgramArgumentsの先頭が相対パスのとき、launchdは_PATH_STDPATHで解決するとman launchd.plistに書かれていますが、これは標準的な場所だけを探す仕組みです。~/.local/binのような置き場所は含まれません。
対策は、plistでは絶対パスのラッパーだけを呼び、PATHと環境変数はラッパーの中で整えることです。plistのEnvironmentVariablesでも指定できますが、値は文字列のみで、文字列以外は無視されます。
#!/bin/sh
# /Users/you/bin/claude-morning.sh
set -u
export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
export CLAUDE_CODE_OAUTH_TOKEN="$(cat "$HOME/.config/claude-morning/token")"
cd "$HOME/work/my-repo" || exit 1
date '+start %F %T'
claude -p "昨日のコミットを3行で要約して" \
--allowedTools "Read,Bash(git log:*)" \
--permission-prompts none </dev/null
status=$?
date '+end %F %T'
echo "exit=$status"
exit "$status"このスクリプトには、無人実行のための工夫が4つ入っています。
- 作業ディレクトリを
cdで固定する。plistでWorkingDirectoryを指定しても同じ効果が得られる --allowedToolsで使うツールを事前に許可する--permission-prompts noneを付け、確認が必要な操作は答えを待たずに拒否させる。このフラグにはClaude Code v2.1.259以降が必要で、古い版は未知のオプションとしてエラーになる</dev/nullで標準入力を空にする
権限まわりでは、Claude Code v2.1.302以降のclaude -pは、権限モードの指定がなければManualモードで始まります。承認が要る呼び出しは、承認する仕組みがなければ拒否されます。無人のジョブで「何も起きなかった」ときは、拒否された操作がないかを疑ってください。
認証はOAuthトークンかAPIキーで渡す
launchdのジョブでは、ブラウザーでのログインを開けません。ドキュメントの案内は、claude setup-tokenで1年間有効なOAuthトークンを作る方法です。
claude setup-tokenトークンは画面に表示されるだけで、どこにも保存されません。CLAUDE_CODE_OAUTH_TOKENとして渡す先を自分で用意します。上のラッパーでは、権限を絞ったファイルから読み込んでいます。plistのEnvironmentVariablesに直接書くこともできますが、その場合はトークンがplistの平文に残ります。ファイルをchmod 600にして読み込むほうが、誤ってリポジトリや共有フォルダーに入れる事故を減らせます。
APIキーを使う場合はANTHROPIC_API_KEYを渡します。なお、起動を速くする--bareは、OAuthの認証情報もシステムのキーチェーンも読みません。サブスクリプションのログインで動かしているジョブに--bareだけを足すと、認証で失敗します。
ログの出し先 — StandardOutPathとStandardErrorPath
launchdのジョブは、何も指定しなければ出力の行き先がありません。StandardOutPathが標準出力、StandardErrorPathが標準エラーの書き込み先になります。指定したファイルがなければ作成されますが、親ディレクトリは作られません。先にディレクトリを作っておきます。
mkdir -p ~/Library/Logs/claude-morningclaude -pの出力は、ログを読む側に少し癖があります。ドキュメントによると、成功すれば終了コード0、失敗なら非ゼロで終わります。ただし、実行中に起きた失敗、たとえば認証がないときは、失敗内容が結果として標準出力に出ます。err.logだけを見ていると、失敗に気づけません。
そのため、ラッパーでは開始・終了時刻と終了コードを標準出力に残しています。out.logの末尾がexit=0なら正常、それ以外なら本文のすぐ上に理由があります。ログは追記され続けるので、半年ほど動かすならnewsyslogなどでのローテーションも考えてください。失敗に気づく仕組みの作り方はClaude Codeのcron失敗を調査する手順で詳しく扱っています。
launchctlで登録して、時刻を待たずに試走する
plistを置いたら、登録の前に構文を確かめます。
chmod +x ~/bin/claude-morning.sh
plutil -lint ~/Library/LaunchAgents/com.example.claude-morning.plist形式に問題がなければOKと出ます。次に、ユーザーのGUIドメインへ登録します。
launchctl bootstrap gui/$(id -u) \
~/Library/LaunchAgents/com.example.claude-morning.plist
launchctl print gui/$(id -u)/com.example.claude-morningbootstrapはドメインにサービスを読み込み、対応するbootoutで取り除きます。printはサービスの状態と、最後の終了ステータスを表示します。ただしman launchctlには、この出力はAPIではないと明記されています。フォーマットは変わりうるので、スクリプトで解析する用途には向きません。人が目で見る確認用です。
時刻を待たずに動かすにはkickstartを使います。launchの条件にかかわらず、サービスをすぐ起動します。
launchctl kickstart -p gui/$(id -u)/com.example.claude-morning
tail -f ~/Library/Logs/claude-morning/out.log-pは、起動に成功したらプロセスIDを表示するオプションです。すでに実行中のジョブを止めてから起動し直したいときは-kを付けます。
plistを書き換えたら、いったんbootoutしてbootstrapをやり直します。
launchctl bootout gui/$(id -u)/com.example.claude-morning
launchctl bootstrap gui/$(id -u) \
~/Library/LaunchAgents/com.example.claude-morning.plist止めたまま置いておきたいなら、launchctl disableでサービスを無効にできます。この状態は再起動後も残ります。
試走が成功したときのout.logは、ラッパーが書いた行で挟まれた形になります。次は形を示す例で、時刻と本文は環境ごとに変わります。
start 2026-10-28 07:30:01
(claude -p の応答本文)
end 2026-10-28 07:30:19
exit=0開始と終了の行があれば、所要時間も読み取れます。予定の時刻にstartの行がない日は、ジョブが起動していません。その日の電源やスリープの状態を疑うところから始められます。
動かないときの切り分け
試走で失敗したときは、症状から原因を絞ります。
| 症状 | まず疑うこと | 見る場所 |
|---|---|---|
bootstrapがエラーになる | まず疑うことplistの形式、Labelの重複、すでに読み込み済み | 見る場所plutil -lint、先にbootout |
手動のkickstartでは動くが朝は動かない | まず疑うこと電源オフ、スリープ前の時刻指定 | 見る場所Macの電源状態、HourとMinute |
command not found: claude | まず疑うことPATHがラッパーで整っていない | 見る場所err.log、ラッパーのexport PATH |
認証エラーがout.logに出る | まず疑うことトークン未設定、--bareの併用 | 見る場所CLAUDE_CODE_OAUTH_TOKENの値 |
| ログが空のまま終わらない | まず疑うことプロセスが固まっている | 見る場所launchctl print、プロセス一覧 |
| 権限まわりで何も変わらない | まず疑うこと承認が必要な操作が拒否された | 見る場所--allowedTools、権限モード |
ログが空のまま何分も進まない場合は、原因が別にあります。zshのシェルスナップショットが関わる報告があり、Claude Codeがlaunchdで固まるときの原因と対処法に回避策をまとめています。
まとめ
launchdでclaude -pを定期実行するときの設計は、「plistは薄く、中身はラッパー」の1点に尽きます。時刻はStartCalendarInterval、出力先はStandardOutPathとStandardErrorPathに任せ、PATH・認証・権限・作業ディレクトリはラッパーに集めます。Macが寝ているだけなら復帰時に拾われますが、電源が落ちていれば、その回は飛びます。毎朝の結果が必須なら、電源設定の側も確かめてください。
OS側のスケジューラーを使わず、Claude Code自体のスケジュール機能で動かす方法もあります。GUIでの設定はClaude Code Desktopの定期実行タスクで扱っています。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。