devAlice
← AI Agents

AIエージェントをバックグラウンド実行する — cron · launchd · タスクスケジューラ

Claude CodeなどのAIエージェントを定期的に無人実行するスケジューリングパターン。macOS launchd · Linux cron · Windowsタスクスケジューラを同一シナリオで比較。

Claude Codeのようなエージェントは、キーボードの前にいる時だけのものではない。夜間PR レビューIssue トリアージREADME 同期ログ要約 — 無人実行することで価値が積み重なる。以前はエージェントを対話型ツールとしてしか使っていなかった。いまでは定期実行こそがエージェントの真価だと考える。単発の会話ではなく、継続的な自動化にあるからだ。

このガイドでは、3つのOS(macOS launchd、Linux cron、Windows タスクスケジューラ)の標準スケジューリングツールを同一シナリオで比較し、AIエージェント特有の落とし穴(APIキーの安全管理、ログ取得、失敗通知)を解説する。

TL;DR

OSツール定義場所ログ
macOSlaunchd (LaunchAgent)~/Library/LaunchAgents/<label>.plistStandardOutPath/StandardErrorPath
Linuxcroncrontab -e または /etc/cron.d/<name>MTA またはリダイレクト >> log.txt 2>&1
WindowsタスクスケジューラGUI または PowerShell Register-ScheduledTaskイベントビューアまたはファイルリダイレクト

5つのよくある落とし穴:

  1. PATHが空 — コマンドが見つからない
  2. APIキーを plist/crontab にプレーンテキストで保存(禁止)
  3. 同一ジョブの二重起動による競合
  4. サイレントな失敗 — 通知なし
  5. マシンがスリープ中 — ジョブがスキップされる

§1〜§3はOS別のパターン、§4は落とし穴について説明する。

前提条件

  • 実行するAIエージェント(例: Claude Code、Cursor CLI、カスタムスクリプト)
  • 環境変数またはシークレットマネージャーで管理されたAPIキー
  • シェルスクリプト(例: ~/agents/daily-pr-review.sh

1. macOS — launchd LaunchAgent — 10分

macOSにもcronはありますが、Appleはlaunchdを推奨している。スリープ後・再ログイン後のキャッチアップ実行もより確実だ。

1.1 plistを作成する

~/Library/LaunchAgents/dev.local.daily-pr-review.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>dev.local.daily-pr-review</string>
 
  <key>ProgramArguments</key>
  <array>
    <string>/bin/bash</string>
    <string>-lc</string>
    <string>$HOME/agents/daily-pr-review.sh</string>
  </array>
 
  <!-- 環境変数: PATHが空になることがあるため明示的に設定 -->
  <key>EnvironmentVariables</key>
  <dict>
    <key>PATH</key>
    <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
    <!-- ANTHROPIC_API_KEYをここに書かないこと — §4.2参照 -->
  </dict>
 
  <!-- 毎日09:00 -->
  <key>StartCalendarInterval</key>
  <dict>
    <key>Hour</key><integer>9</integer>
    <key>Minute</key><integer>0</integer>
  </dict>
 
  <!-- スリープ後のキャッチアップ -->
  <key>RunAtLoad</key><false/>
  <key>StartOnMount</key><false/>
 
  <!-- ログ -->
  <key>StandardOutPath</key>
  <string>/tmp/daily-pr-review.out.log</string>
  <key>StandardErrorPath</key>
  <string>/tmp/daily-pr-review.err.log</string>
</dict>
</plist>

1.2 登録と実行

# 登録
launchctl load ~/Library/LaunchAgents/dev.local.daily-pr-review.plist
 
# 即時実行(スケジュールをスキップ)
launchctl start dev.local.daily-pr-review
 
# ステータス確認
launchctl list | grep daily-pr-review
 
# 登録解除
launchctl unload ~/Library/LaunchAgents/dev.local.daily-pr-review.plist

1.3 スリープ後のキャッチアップ

cronはスリープ中のジョブをスキップする。launchdはウェイク後に1回実行します(注: StartCalendarInterval のキャッチアップは1回のみです)。

毎時などのキャッチアップには:

<key>StartInterval</key>
<integer>3600</integer>  <!-- 1時間ごと -->

→ cronと違い、ウェイク後に1回実行される。


2. Linux — cron — 5分

2.1 crontabを編集する

crontab -e

例:

# m h dom mon dow  command
0 9 * * *  /home/user/agents/daily-pr-review.sh >> /home/user/logs/pr-review.log 2>&1
フィールド意味
00毎時0分
99午前9時
* * *every毎日

毎日09:00に実行し、stdout/stderrをログファイルに追記する。

2.2 PATHを明示的に設定する

cronのPATHは非常に限られています。スクリプト内またはcrontabの先頭で設定してください:

PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/sbin
SHELL=/bin/bash
 
0 9 * * *  $HOME/agents/daily-pr-review.sh >> $HOME/logs/pr-review.log 2>&1

2.3 システムcron(/etc/cron.d/

サーバーにルートレベルで登録する場合:

sudo tee /etc/cron.d/daily-pr-review > /dev/null <<'EOF'
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
0 9 * * * appuser /home/appuser/agents/daily-pr-review.sh >> /var/log/agent.log 2>&1
EOF

appuserフィールドで実行ユーザーを指定する。セキュリティ上重要だ。

2.4 systemd タイマー(現代的な代替手段)

cronの代替手段。ユニットファイルで再起動・障害処理・ログを一元管理できる。

# /etc/systemd/system/pr-review.service
[Unit]
Description=Daily PR review agent
 
[Service]
Type=oneshot
User=appuser
ExecStart=/home/appuser/agents/daily-pr-review.sh
StandardOutput=journal
StandardError=journal
# /etc/systemd/system/pr-review.timer
[Unit]
Description=Run daily PR review at 09:00
 
[Timer]
OnCalendar=*-*-* 09:00:00
Persistent=true   # ダウンタイム中に時刻が過ぎた場合、起動後に実行
 
[Install]
WantedBy=timers.target
sudo systemctl enable --now pr-review.timer
journalctl -u pr-review.service    # ログ

Persistent=trueはcronに対する大きな利点 — サーバーダウン後の自動キャッチアップが可能だ。


3. Windows — タスクスケジューラ — 10分

GUIでも設定できますが、PowerShellで再現性を持たせましょう。

3.1 PowerShellで登録する

# アクション
$action = New-ScheduledTaskAction `
  -Execute "wsl.exe" `
  -Argument "-d Ubuntu -e bash -lc '~/agents/daily-pr-review.sh'"
 
# トリガー: 毎日09:00
$trigger = New-ScheduledTaskTrigger -Daily -At 9am
 
# 設定: ユーザーがログインしていなくても実行; スリープから復帰して実行
$settings = New-ScheduledTaskSettingsSet `
  -StartWhenAvailable `
  -AllowStartIfOnBatteries `
  -DontStopIfGoingOnBatteries `
  -ExecutionTimeLimit (New-TimeSpan -Hours 1) `
  -RestartCount 2 `
  -RestartInterval (New-TimeSpan -Minutes 10)
 
# 登録
Register-ScheduledTask `
  -TaskName "DailyPRReview" `
  -Action $action `
  -Trigger $trigger `
  -Settings $settings `
  -Description "Run Claude Code daily PR review in WSL"

3.2 GUIで確認する

Win + Rtaskschd.msc → タスクスケジューラライブラリ → 「DailyPRReview」

3.3 即時テスト実行

Start-ScheduledTask -TaskName "DailyPRReview"
Get-ScheduledTaskInfo -TaskName "DailyPRReview" | Select LastRunTime, LastTaskResult

LastTaskResult: 0は成功を意味する。

3.4 WSLへの呼び出し

上記のように wsl.exe -d Ubuntu -e bash -lc '...' を使用する。環境変数とAPIキーはWSLの ~/.bashrc または別の環境ファイルに保存する。

3.5 ログの取得

タスクスケジューラ自体のログは簡素だ。スクリプト内でリダイレクトしてください:

# ~/agents/daily-pr-review.sh
#!/usr/bin/env bash
set -euo pipefail
LOG="$HOME/logs/pr-review-$(date +%Y%m%d).log"
mkdir -p "$(dirname "$LOG")"
exec >> "$LOG" 2>&1
echo "[$(date)] starting..."
# エージェントを実行
claude-code --resume --task pr-review --quiet
echo "[$(date)] done."

4. AIエージェント特有の落とし穴

4.1 PATHが空 — 「動かない」原因の第1位

インタラクティブシェルのPATH ≠ launchd/cronのPATH。スクリプトの先頭に明記してください:

#!/usr/bin/env bash
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"

またはplist/crontabの EnvironmentVariables / PATH= ヘッダーで設定する。

確認方法:

launchctl getenv PATH         # macOS
sudo -u appuser env | grep PATH  # Linux cronユーザー

4.2 APIキーをplist/crontabに絶対書かない

plistの EnvironmentVariables ブロックに ANTHROPIC_API_KEY=sk-ant-... と書いた場合:

  • ls -laで露出する
  • Time Machine / iCloudバックアップにプレーンテキストで保存される
  • 同マシン上の他ユーザーが cat ~/Library/LaunchAgents/*.plist で読み取れる

代替手段:

  • macOS: Keychain + スクリプトから security find-generic-password -s anthropic-api -w で読み取る
  • Linux: 別途 ~/.config/agent/env ファイルを chmod 600 で作成し、スクリプトで source する
  • Windows: Get-Credential または資格情報マネージャー

スクリプト(macOS):

#!/usr/bin/env bash
set -euo pipefail
export ANTHROPIC_API_KEY="$(security find-generic-password -s anthropic-api -w 2>/dev/null)"
[ -z "$ANTHROPIC_API_KEY" ] && { echo "missing API key in Keychain"; exit 1; }
# ... エージェントを実行

キーを1回登録する:

security add-generic-password -s anthropic-api -a "$USER" -w 'sk-ant-...'

4.3 二重起動を防ぐ — flock / シングルインスタンス

二重起動 = 2倍のAPIコスト + 競合。flockでロックする:

#!/usr/bin/env bash
exec 200>/tmp/daily-pr-review.lock
flock -n 200 || { echo "already running"; exit 0; }
# ... エージェントを実行

flock -n = ノンブロッキング; ロック中は即座に終了する。

4.4 サイレントな失敗を防ぐ — 失敗通知

デフォルトでは、stderrはログファイルに書き込まれるだけだ。trapで通知しましょう:

#!/usr/bin/env bash
set -euo pipefail
 
notify_failure() {
  local code=$?
  if [ $code -ne 0 ]; then
    # macOS — terminal-notifier または osascript
    osascript -e "display notification \"PR review failed (exit $code)\" with title \"Agent\""
    # または Slack webhook / メール / Sentry
    # curl -X POST -H 'Content-Type: application/json' \
    #   -d "{\"text\":\"Agent failed: exit $code\"}" "$SLACK_WEBHOOK_URL"
  fi
}
trap notify_failure EXIT
 
# エージェントを実行

4.5 スリープ・起動後のキャッチアップ

  • macOS launchd: スリープからウェイク後に StartCalendarInterval で1回キャッチアップ
  • Linux systemd タイマー: Persistent=true を使用
  • Windows: タスクスケジューラの 「スケジュールされた開始時刻を過ぎた場合、できるだけ早くタスクを開始する」 をONにする

cronだけはスリープ後のキャッチアップが不可能 — それがノートPCでcronを使わない理由だ。


5. 実践シナリオ — Claude Code 毎日PRレビュー

~/agents/daily-pr-review.sh:

#!/usr/bin/env bash
set -euo pipefail
 
# 1. PATH + シークレット
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
export ANTHROPIC_API_KEY="$(security find-generic-password -s anthropic-api -w)"
export GITHUB_TOKEN="$(security find-generic-password -s github-token -w)"
 
# 2. ロック
exec 200>/tmp/daily-pr-review.lock
flock -n 200 || exit 0
 
# 3. ログ
LOG="$HOME/logs/pr-review-$(date +%Y%m%d).log"
mkdir -p "$(dirname "$LOG")"
exec >> "$LOG" 2>&1
echo "[$(date)] starting daily PR review..."
 
# 4. 作業
cd "$HOME/code/my-repo"
git fetch --quiet origin
gh pr list --state open --json number,title,author --jq '.[] | select(.author.login != "me")' | \
  while read -r pr; do
    num=$(echo "$pr" | jq -r .number)
    echo "Reviewing PR #$num..."
    claude-code --task "Review PR #$num — focus on security, performance, missing tests" --quiet
  done
 
echo "[$(date)] done."

LaunchAgentとして登録すれば、毎日09:00に自動実行される。


6. トラブルシューティング

launchd 「Service exited with abnormal code: 127」

PATHが空またはスクリプトに実行権限がありません。chmod +x ~/agents/*.sh + plist内でPATHを設定してください。

cronが実行されずログもない

  • crontabの構文エラー: crontab -l で確認
  • MTA(mailutils)がない場合、stderrが消える → 明示的に 2>&1 >> log
  • journalctl -u cron(またはcronie)で実行履歴を確認

タスクスケジューラ「Last Task Result: 0x1」

スクリプトが終了コード1で終了している。WSL内で直接実行してデバッグしてください。$LASTEXITCODEを確認する。

claude-code コマンドが見つからない

スクリプトのPATHに ~/.npm-global/bin またはnpmグローバルbinが含まれていません。which claude-code のディレクトリをPATHに追加してください。


次のステップ

参考リンク

変更履歴

  • 2026-05-12 — 初版(devAlice M2 シード展開)