Procedure
- 死活確認:
log show --predicate 'process == "cron"' --last 7dで 0 件なら cron daemon が動いていない。 - plist 自動生成:
~/.claude/scripts/cron-to-launchd.sh dryで~/.claude/scripts/launchd-proposed/に crontab 各行 →com.<user>.<job>.plistを出力。bash 3.2 互換 (while readループ、mapfile不使用)。 - 適用:
cron-to-launchd.sh applyで~/Library/LaunchAgents/に配置 +launchctl bootstrap gui/$(id -u) <plist>。 - 即時動作確認:
launchctl kickstart gui/$(id -u)/com.<user>.<job>で強制実行 → 対応する log file の mtime と PID を確認。 - 並走防止: cron 行は残置 (macOS の cron daemon が動いていないので二重実行リスクなし)。ユーザーに
crontab -eで後日削除を推奨。
Pitfalls
*/N周期 (例:*/5 * * * *) はStartCalendarIntervalに直接マップできない → 配列で N 個の Minute entry に展開するかStartInterval(秒数) を使う。launchctl listは legacy API。modern はlaunchctl print gui/$(id -u)/<label>/launchctl kickstart。Labelはcom.<user>.<name>形式。dot を含まない label は load 拒否される。- plist の
ProgramArgumentsは配列必須 (string 単体は弾かれる)。StandardOutPath/StandardErrorPathは絶対パスで明示しないと/dev/nullに消える。 gui/<uid>domain は GUI セッション必須。ヘッドレス常駐はsystem/domain +/Library/LaunchDaemons/配置 (root 権限必要)。- ⚠️TCC保護領域に書けない: launchd起動プロセスは
~/Desktop~/Documents~/Downloadsへ書くとPermissionError: Operation not permitted(exit 1)。Full Disk Access付与は対象が/bin/bash/python3等になり広すぎ&手動GUI操作要。→ 実体を非保護パス(HOME直下~/foo等)に置き、保護領域には symlink を張るのが定石(プロセスは実体パスに書く=TCC回避、ユーザーはDesktopから symlink で見える)。スクリプト内のパス定数は symlink でなく実体パスを参照させること。 - ⚠️**
env -iでの再現テストは誤検知する**: 素環境でclaude -pを回すと「Not logged in」やKeychainアクセス失敗で落ちるが、本物のlaunchd GUIエージェントはセキュリティセッションを保持してKeychainから認証が通る。検証はenv -iでなくlaunchctl kickstart -k gui/$(id -u)/<label>で本物のエージェントを実走させてログ&last exit codeを見る。 - claude CLI認証(
~/.local/bin/claude -p)はtoken/env不要。PATHに~/.local/binとnode(nvm)を通せば、login keychainの "Claude Code-credentials" から自動で認証される(本物launchd下で実績あり)。
Verification
launchctl list | grep com.<user>で全 plist が load 状態。launchctl kickstart gui/$(id -u)/com.<user>.<name>実行直後に log file の mtime が更新されている。automation-health.shの cron 死活セクションが ALL GREEN。- 1 週間後に各 log file の mtime が想定スケジュール通り進んでいる。