Claude Media
Claude Codeをlaunchdで定期実行するplist — 毎朝の登録と動作確認

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. 1

    ラッパースクリプトを書く

    claude -pを呼ぶシェルスクリプトを用意し、実行権限を付けます。

  2. 2

    plistを書いて構文を確かめる

    ~/Library/LaunchAgents/に置き、plutil -lintで形式の誤りを先に潰します。

  3. 3

    launchctlで登録する

    launchctl bootstrapでユーザーのドメインに読み込ませます。

  4. 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-morning

claude -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-morning

bootstrapはドメインにサービスを読み込み、対応する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の定期実行タスクで扱っています。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/headless
  2. code.claude.com/docs/en/authentication
  3. developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemSt…
この記事を共有:XはてブLinkedIn