0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

🔄 cronが死んだ日に曞いた䞀括launchd移行スクリプト

0
Posted at

解雇から半幎、月商0から120䞇円たで持ち盎した自埋環境がある朝8時に黙っお止たっおいた話をしたす。原因はmacOSのアップデヌトによるcron daemonの無効化。察策ずしお曞いたのが、crontabを1行ず぀解析しおlaunchd plistを自動生成する97行のシェルスクリプトです。

なぜこの仕組みが効くのか

「動いおいるはず」は最も危険な確信

副業で月60䞇を皌いでいた時期、自動化の恩恵はほが党額がcronゞョブに乗っおいたした。noteの公開タむミング調敎、SNS投皿のスケゞュヌリング、日次のデヌタ集蚈——これらがすべおcrontab -lに䞊んでいた。解雇されお0になり、Claude Codeで環境を再構築するずきも、crontabをそのたた匕き継ぐのが最速だず刀断したした。

macOS Sequoia15系に䞊げた盎埌は䜕も倉わっおいないように芋えたした。crontab -lを叩けば゚ントリは党郚ある。しかしdaemonは起動しおいない。AppleはmacOS Ventura以降、段階的にcron daemonをナヌザヌセッションから切り離しおきおおり、Sequoia/Tahoeでは/usr/sbin/cronは存圚するのにlaunchctl list | grep cronでは䜕も返っおこない環境が普通にありたす。

気づくのが遅れたのは、自動化が止たっおも゚ラヌが出ないからです。cronが動いおいないなら/var/mail/ナヌザヌ名に゚ラヌメヌルが来る——ずいう想定が厩れおいた。Sequoideではデフォルトでポストオフィスに届かない。朝8時のデむリヌブリヌフが来ない、11時のSNS投皿が流れない、それでようやく気づく。この「静かな死」が怖い。

launchdが正解である理由

macOSにおいおプロセスの起動・管理を担うのはlaunchdPID 1です。cronは歎史的な互換ずしお残っおいるだけで、Appleが本来掚奚するのはlaunchd経由のゞョブ管理です。launchdはdaemonがクラッシュしたずきの自動再起動、マシンスリヌプ䞭にスケゞュヌルされたゞョブのスリヌプ解陀埌自動実行、暙準出力・゚ラヌ出力のファむルぞの盎接リダむレクト、環境倉数の明瀺的な泚入、ずいった機胜をplist 1ファむルで宣蚀的に扱えたす。

cronが*/5 * * * * cmdず1行で曞けるのに察し、launchdのplistは20〜30行のXMLになる——この冗長さがlaunchd移行の最倧の心理的障壁です。手䜜業で10本を曞き盎すのは珟実的でない。だからスクリプトで自動生成したす。

実際に皌働しおいるplistを1本芋るず、構造が腑に萜ちたす。~/Library/LaunchAgents/com.shun.daily-brief.plistは以䞋の構成です実ファむルより抜粋、パスは~衚蚘に倉換

<key>Label</key>
<string>com.shun.daily-brief</string>

<key>EnvironmentVariables</key>
<dict>
  <key>PATH</key>
  <string>~/.nvm/versions/node/v24.13.0/bin:/opt/homebrew/bin:/opt/homebrew/sbin:
          /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:~/.local/bin</string>
</dict>

<key>StartCalendarInterval</key>
<array>
  <dict>
    <key>Hour</key><integer>8</integer>
    <key>Minute</key><integer>0</integer>
  </dict>
  <dict>
    <key>Hour</key><integer>10</integer>
    <key>Minute</key><integer>30</integer>
  </dict>
</array>

<key>ProgramArguments</key>
<array>
  <string>~/.claude/scripts/claude-quota-guard.py</string>
  <string>--job</string>
  <string>com.shun.daily-brief</string>
  <string>--</string>
  <string>/bin/bash</string>
  <string>~/.claude/scripts/daily-brief.sh</string>
</array>

<key>LowPriorityIO</key><true/>
<key>Nice</key><integer>10</integer>
<key>RunAtLoad</key><true/>
<key>StandardOutPath</key>
<string>~/.claude/logs/com.shun.daily-brief.log</string>
<key>StandardErrorPath</key>
<string>~/.claude/logs/com.shun.daily-brief.log</string>

泚目点が3぀ありたす。

EnvironmentVariablesの明瀺。launchdはナヌザヌのシェル蚭定.zshrc・.bashrcを読みたせん。nvm経由でむンストヌルしたnodeを䜿うスクリプトは、launchd管理䞋ではPATHが通っおおらずnode: command not foundで死ぬ。cronから移行したゞョブが突然動かなくなる原因の9割がこれです。plistにPATHを明瀺するこずで、シェルが䜕であれ同じバむナリを確実に呌べたす。

StartCalendarIntervalの配列圢匏。1日に耇数回実行したい堎合、<array>の䞭に<dict>を䞊べたす。daily-briefは8:00ず10:30の2回。cronなら0 8,10 * * *ず曞けたすが、launchdは時刻ごずに蟞曞を䜜る必芁がある。この蚘述差を自動生成スクリプトがどこたでカバヌするか、埌述の萜ずし穎に関わりたす。

LowPriorityIOずNice。バックグラりンドゞョブにI/O優先床を䞋げ、CPUスケゞュヌラのnice倀を10に蚭定しおいたす。フォアグラりンドの䜜業゚ディタ・ブラりザぞの圱響を最小化するための蚭定で、自埋環境の「存圚感を消す」哲孊に沿っおいたす。

䜜業でなく環境に投資する意味

月商120䞇の内蚳は、自分が手を動かしおいる郚分がほがない。note連茉・SNS曎新・デヌタ集蚈の倚くが自動化されおいたす。この環境の維持コストは、cronを正しく動く基盀に移し替えるこずに尜きたす。1床曞いた定期タスクが3幎埌も動いおいるのがlaunchd管理䞋のゎヌルです。AppleのlaunchdはmacOS 10.42005幎から倉わらない安定したAPIで、cronのように「気づかず死ぬ」こずがない。launchctl list com.shun.daily-briefで即座にLastExitStatusず次回実行予定が確認できたす。

環境を敎える90分は、毎朝「ちゃんず動いおいるか確認する」5分×365日=30時間を買い戻す投資です。

党䜓の流れ

凊理の地図

crontab -l
  │  grep -vE '^\s*#' | grep -v '^$'  ← コメント行・空行を陀倖
  ↓
[1行ごずにルヌプ]
  │  awk '{print $1...$5}' で schedule フィヌルド抜出
  │  cut -d' ' -f6-           で cmd 郚分を切り出し
  │  basename からラベル生成  → com.shun.<script-name>
  ↓
StartCalendarInterval XML 組み立お
  │  ※ */N 圢匏は非察応固定倀のみ← ここが萜ずし穎
  ↓
plist ファむル曞き出し
  → [dry]   ~/.claude/scripts/launchd-proposed/*.plist
  → [apply] ~/Library/LaunchAgents/*.plist
               + launchctl unload → launchctl load
  ↓
⚠  譊告: crontab から手動削陀しないず二重起動

スクリプト解剖党97行

スクリプトは~/.claude/scripts/cron-to-launchd.shに眮いおあり、䜿い方は2通りです。

# 差分確認ファむルを曞くだけ、loadしない
~/.claude/scripts/cron-to-launchd.sh dry

# 本番反映LaunchAgentsにコピヌしおlaunchctl load
~/.claude/scripts/cron-to-launchd.sh apply

匕数なしで呌べばdryがデフォルトMODE="${1:-dry}"。いきなりapplyせず、たずdryで生成物を目芖確認するのが鉄則です。

フェヌズ1crontabの読み蟌みずパヌス20〜28行目

CRON_LINES=()
while IFS= read -r line; do
  [ -n "$line" ] && CRON_LINES+=("$line")
done < <(crontab -l 2>/dev/null | grep -vE '^\s*#' | grep -v '^$')

bash 3.2 互換ずコメントにある通り、macOS暙準のbashはバヌゞョン3.2GPLv2の郜合でAppleが曎新しおいないです。mapfileやreadarrayは3.2では䜿えないため、while IFS= read -rのルヌプで配列を構築しおいたす。crontab -l 2>/dev/nullでcrontabが空の堎合の゚ラヌを握り぀ぶし、grep -vE '^\s*#'でコメント行、grep -v '^$'で空行を陀去したす。

フェヌズ2各行から schedule ず cmd を分解28〜38行目

minute=$(echo "$line" | awk '{print $1}')
hour=$(echo "$line" | awk '{print $2}')
dom=$(echo "$line"   | awk '{print $3}')
mon=$(echo "$line"   | awk '{print $4}')
dow=$(echo "$line"   | awk '{print $5}')
cmd=$(echo "$line"   | cut -d' ' -f6-)

cronの曞匏min hour dom mon dow cmd...をawkで1フィヌルドず぀取り出したす。cmdは6番目以降をすべおcut -d' ' -f6-で取るため、コマンドに匕数がいく぀あっおも正しく拟えたす。

ラベルの生成ロゞック38〜40行目

script=$(echo "$cmd" | grep -oE '~/.claude/scripts/[^ ]+' | head -1 | xargs basename 2>/dev/null)
if [ -z "$script" ]; then
  script="$(echo "$cmd" | awk '{print $1}' | xargs basename 2>/dev/null)-${minute}${hour}"
fi
label="com.shun.$(echo "$script" | sed -E 's/\.[a-z]+$//' | tr '_' '-')"

~/.claude/scripts/配䞋のスクリプトはbasenameから拡匵子を陀いおラベル化したす。たずえばdaily-brief.shならcom.shun.daily-brief。それ以倖の汎甚コマンドfind・backup-rotateなどは、コマンド名分時でナニヌク性を確保したす。アンダヌスコアはハむフンに倉換launchのLabel芏玄。

フェヌズ3StartCalendarInterval XMLの組み立お44〜52行目

cal_xml="  <key>StartCalendarInterval</key>\n  <dict>\n"
# */N 呚期は launchd では耇数゚ントリで再珟する必芁 — ここでは固定倀だけ察応
if [ "$minute" != "*" ]; then cal_xml+="    <key>Minute</key><integer>${minute}</integer>\n"; fi
if [ "$hour"   != "*" ]; then cal_xml+="    <key>Hour</key><integer>${hour}</integer>\n";   fi
if [ "$dom"    != "*" ]; then cal_xml+="    <key>Day</key><integer>${dom}</integer>\n";      fi
if [ "$mon"    != "*" ]; then cal_xml+="    <key>Month</key><integer>${mon}</integer>\n";    fi
if [ "$dow"    != "*" ]; then cal_xml+="    <key>Weekday</key><integer>${dow}</integer>\n";  fi
cal_xml+="  </dict>"

フィヌルドが*ワむルドカヌドなら察応するキヌをXMLに含めない——これがlaunchdのStartCalendarIntervalの意味論です。たずえば0 8 * * *毎日8時0分はHour=8, Minute=0のみ曞けばよく、Day/Month/Weekdayは省略するこずで「毎日」になりたす。

フェヌズ4plist本䜓の曞き出し54〜76行目

cat > "$plist" <<XMLEOF
<?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>${label}</string>
  <key>ProgramArguments</key>
  <array>
    <string>/bin/zsh</string>
    <string>-c</string>
    <string>${cmd//&/&amp;}</string>
  </array>
$(echo -e "${cal_xml}")
  <key>StandardOutPath</key>
  <string>${log}</string>
  <key>StandardErrorPath</key>
  <string>${log}</string>
  <key>ProcessType</key>
  <string>Background</string>
</dict>
</plist>
XMLEOF

コマンドは/bin/zsh -c "cmd"ずしおラップしたす。cronで走っおいたコマンドはシェルの展開~展開・グロブに䟝存しおいるこずが倚く、ProgramArgumentsに盎接枡すず動かないケヌスがある。zsh経由にするこずでその差異を吞収しおいたす。${cmd//&/&amp;}はXML゚スケヌプ——コマンドに&が含たれるず䞍正なXMLになるため、ここで眮換したす。ログは~/.claude/logs/${label}.logに暙準出力・゚ラヌ出力をたずめお吐きたす。

フェヌズ5applyモヌドの配備84〜96行目

if [ "$MODE" = "apply" ]; then
  for f in "$PROPOSED"/*.plist; do
    cp "$f" "$TARGET_DIR/"
    launchctl unload "$TARGET_DIR/$(basename $f)" 2>/dev/null
    launchctl load   "$TARGET_DIR/$(basename $f)"
    echo "  loaded: $(basename $f)"
  done
  echo ""
  echo "🚚 cron 行は **手動で削陀しおください**:  crontab -e"
  echo "(誀っお cron+launchd 䞡方走るのを避けるため)"
fi

launchctl unloadを先に呌ぶのは冪等性のためです。すでにloadされおいるplistに再床loadしようずするず゚ラヌになる。事前にunloadしおおくこずで、applyを䜕床実行しおも同じ結果になりたす。ただし泚意点が1぀——スクリプトはapply埌に「crontabから手動削陀しおください」ず譊告を出すだけで、削陀は自動化しおいたせん。cron行を残したたた攟眮するず、macOSが将来的にcron daemonを埩掻させたタむミングでcron+launchdの二重起動が発生したす。

実装の詳现

set -uo pipefail —— -e を倖した理由

スクリプト1行目の宣蚀は set -uo pipefail です実ファむル9行目。-e゚ラヌ即終了が入っおいないこずに気づいた人もいるかもしれたせん。これは意図的な蚭蚈です。

applyモヌドのルヌプを芋おください84〜96行目。

launchctl unload "$TARGET_DIR/$(basename $f)" 2>/dev/null
launchctl load   "$TARGET_DIR/$(basename $f)"

launchctl unloadは、察象のplistがただloadされおいなければ終了コヌド非れロを返したす。-eが有効なら、1本目の初回unloadでスクリプトが死ぬ。2>/dev/nullで゚ラヌ出力は消しおいたすが、終了コヌドは残りたす。-eなしにするこずで「unloadが倱敗しおもルヌプを続ける」ずいう冪等な挙動を実珟しおいたす。

同じ理由で、crontab -l 2>/dev/null22行目も安党です。crontabが空のナヌザヌ環境ではcrontab -lがcrontab: no crontab for ナヌザヌ名ず非れロで終わりたすが、2>/dev/nullで握り぀ぶしおルヌプに進めたす。-eがあればここで萜ちおいた。

-u未定矩倉数゚ラヌず-o pipefailパむプ倱敗の䌝播は残す。 これは必芁なガヌドです。倉数名のタむポや、パむプ途䞭での倱敗を拟うためです。-eだけが足を匕っ匵る状況——この刀断はシェルスクリプトの゚ラヌハンドリングで頻出するパタヌンです。

ラベル生成の正芏衚珟ず絶察パス䟝存

35行目のラベル生成ロゞックを正確に読むず、1぀重芁な仕様が芋えたす。

script=$(echo "$cmd" | grep -oE '~/.claude/scripts/[^ ]+' | head -1 | xargs basename 2>/dev/null)

正芏衚珟が~/.claude/scripts/ではなく絶察パスでマッチしおいるこずに泚目しおください。crontabの゚ントリが~/.claude/scripts/daily-brief.shず曞かれおいた堎合、この正芏衚珟にはマッチしたせん。~はシェルが展開する前の文字列ずしお蚘録されおいるからです。マッチしなければscript倉数は空になり、フォヌルバックに萜ちたす。

if [ -z "$script" ]; then
  script="$(echo "$cmd" | awk '{print $1}' | xargs basename 2>/dev/null)-${minute}${hour}"
fi

フォヌルバックは「コマンド名のbasename分時」です。たずえば~/.claude/scripts/daily-brief.shを0 8 * * *で登録しおいたなら、ラベルはcom.shun.daily-brief-08になりたす。daily-briefではなくdaily-brief-08。このズレが埌でログを远いかけるずきに混乱を生みたす。

crontabに登録するずきは垞に絶察パスで曞く、これがこのスクリプトず共存する唯䞀の正解です。

*/N 圢匏が壊れる仕組み——コヌドで远う

前半で觊れた「*/N非察応」を、実際のコヌドフロヌで確認したす。たずえばcrontabに*/15 * * * * ~/.claude/scripts/health-check.shずいう行があった堎合、䜕が起きるか。

minute=$(echo "*/15 * * * * ~/.claude/scripts/health-check.sh" | awk '{print $1}')
# → "*/15"

次に条件分岐

if [ "$minute" != "*" ]; then
  cal_xml+="    <key>Minute</key><integer>${minute}</integer>\n"
fi

"*/15" != "*"はtrueなので条件を通過し、生成されるXMLは

<key>Minute</key><integer>*/15</integer>

<integer>タグの䞭に*/15ずいう文字列が入りたす。XMLずしおは䞀応パヌスできたすが、launchdがplistをロヌドするずきに「Minuteは0〜59の敎数でなければならない」ずいう怜蚌で蹎られたす。launchctl loadは終了コヌド非れロを返し、loaded:の衚瀺はされたすが、実際にはスケゞュヌリングが有効になっおいたせん。

この「loadは通ったように芋えるが実は動いおいない」状態が厄介で、次の節でも登堎したす。

/bin/zsh -c ラップの意図ず限界

生成されるplistのProgramArgumentsは54〜66行目

<key>ProgramArguments</key>
<array>
  <string>/bin/zsh</string>
  <string>-c</string>
  <string>${cmd}</string>
</array>

zshでコマンドをラップするのは、cron゚ントリに含たれがちな~展開・環境倉数参照・グロブパタヌンを解釈させるためです。ProgramArgumentsにコマンドを盎接枡すず、シェル展開なしでexecvpが呌ばれるため~がそのたた文字列ずしお枡されおファむルが芋぀からない゚ラヌになりたす。

ただし、/bin/zsh -cでもlaunchdはナヌザヌの.zshrcを読みたせん。これはlaunchdの蚭蚈です。zshのinteractiveモヌドではなくnon-loginのscriptモヌドで起動するため、source ~/.zshrcを曞いおいおも読み蟌たれない。結果ずしおnvmで管理しおいるnode・pyenvのpython・Homebrewの各皮コマンドはPATHが通っおいない状態で起動したす。

生成plistのテンプレヌトを芋るず、EnvironmentVariablesキヌがありたせん54〜76行目党䜓を芋おもない。daily-brief.plistが手䜜業でEnvironmentVariablesを远蚘しおあるのはそのためです。スクリプトが自動生成するplistには、このPATH泚入が含たれおいたせん。


私が詰たった話

詰たり1*/15 * * * *がsilent failした

症状。 applyを実行するずloaded: com.shun.health-check.plistず衚瀺された。しかし15分埌も30分埌も~/.claude/logs/com.shun.health-check.logには䜕も曞かれない。

launchctl list com.shun.health-check
# → Could not find service "com.shun.health-check" in domain for port

loadされおいるはずのサヌビスが、launchctlのリストに存圚しない。

原因。 */15がそのたた<integer>*/15</integer>に曞かれ、launchdがplistの怜蚌で内郚的に棄华しおいたした。launchctl loadコマンド自䜓は終了コヌド0を返したためmacOS Sequoiaでの挙動、スクリプトのecho "loaded:"が走っおしたった。゚ラヌが衚瀺されないたた、サヌビスは存圚しない状態になっおいたした。

盎し方。 plutil -lint ~/.claude/scripts/launchd-proposed/com.shun.health-check.plistでplistを怜蚌するず即座に匟かれたす。*/15を含む行は移行前にcrontabから手動で曞き換えが必芁です。15分おきならlaunchdのStartInterval秒単䜍の間隔指定に倉えるか、00,15,30,45ず展開した固定倀を4゚ントリの配列ずしお曞きたす。

<key>StartCalendarInterval</key>
<array>
  <dict><key>Minute</key><integer>0</integer></dict>
  <dict><key>Minute</key><integer>15</integer></dict>
  <dict><key>Minute</key><integer>30</integer></dict>
  <dict><key>Minute</key><integer>45</integer></dict>
</array>

あるいはStartIntervalで秒指定する方がシンプルです

<key>StartInterval</key>
<integer>900</integer>

900秒15分。この圢匏ならスクリプトの自動生成範囲倖ですが、手曞き1箇所で枈みたす。

詰たり2crontabの~パスでラベルが衝突し叀いplistを䞊曞き

症状。 dryモヌドで確認した~/.claude/scripts/launchd-proposed/の䞭に、芋芚えのないラベル名のplistが生たれおいた。com.shun.daily-brief-08.plistずcom.shun.note-publish-308.plistなど、末尟に時刻が付く呜名。

原因。 crontabに~/.claude/scripts/daily-brief.shず~で曞いおいたため、35行目の絶察パスマッチ~/.claude/scripts/[^ ]+にヒットせず、フォヌルバックのコマンド名-分時圢匏になっおいたした。さらに、以前にapplyしたずきに生成したcom.shun.daily-brief.plistは~/Library/LaunchAgents/に残っおいるため、叀いplistず新しいplistが別ラベルで二重に存圚する状態になっおいたした。

launchctl list | grep com.shunを叩くず同じスクリプトを呌ぶ゚ントリが2぀衚瀺されおいたした。

盎し方。 crontabをcrontab -eで開き、~を絶察パスに曞き盎したす。次に~/Library/LaunchAgents/の旧ラベルplistを手動でunloadしお削陀したす。

launchctl unload ~/Library/LaunchAgents/com.shun.daily-brief-08.plist
rm ~/Library/LaunchAgents/com.shun.daily-brief-08.plist

applyの前に必ずdryで生成ラベルを目芖確認し、期埅するcom.shun.<スクリプト名>圢匏になっおいるかを芋る習慣が必芁です。フォヌルバック圢匏末尟に数字が混じっおいたら、crontabの曞き方を疑いたす。

詰たり3node・pythonがcommand not found

症状。 apply埌、~/.claude/logs/com.shun.note-autolike.logに毎回同じ゚ラヌが出続ける。

/bin/zsh: node: command not found

手動でタヌミナルから同じコマンドを実行するず問題なく動く。

原因。 生成plistにEnvironmentVariablesが含たれないためです。/bin/zsh -cで起動しおも.zshrcは読たれず、nvmが远加する~/.nvm/versions/node/v24.13.0/binはPATHに存圚しない。タヌミナルのシェルセッションず、launchd管理䞋のプロセスはたったく別のPATH環境で動きたす。

盎し方。 生成されたplistを手動で線集し、<key>ProgramArguments</key>の前にEnvironmentVariablesを远加したす。daily-brief.plist実ファむルから匕甚が正解のモデルです

<key>EnvironmentVariables</key>
<dict>
  <key>PATH</key>
  <string>~/.nvm/versions/node/v24.13.0/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:~/.local/bin</string>
</dict>

本来はスクリプトの生成テンプレヌトにこのブロックを組み蟌むべきです。ただし「どのnodeバヌゞョンを䜿うか」は環境によっお異なるため、テンプレヌトにハヌドコヌドするず環境が倉わったずきに党plistの曞き盎しが必芁になる。珟圚のスクリプトが意図的に省いおいるのは、この「バヌゞョン固定の危うさ」を避けるためかもしれたせん——少なくずも自分でそう解釈しお折り合いを぀けおいたす。

実際の運甚では、nodeを䜿うゞョブのplistには必ず手動でEnvironmentVariablesを曞き足しおいたす。スクリプト生成は「雛圢の9割を䜜るこず」、残り1割のPATH泚入は手動、ずいう分業です。

詰たり4cron+launchdが同䞀スクリプトを二重起動しおいた

症状。 note自動投皿が1日2回走るはずが、ログを芋るず投皿APIが1日4回呌ばれおいた。レヌト制限に匕っかかり、゚ラヌレスポンスが出始めお発芚したした。

原因。 apply埌にcrontab -eでcron行を削陀するのを忘れおいたした。スクリプト末尟94〜95行目の譊告を芋萜ずしおいたのです。

🚚 cron 行は **手動で削陀しおください**:  crontab -e
(誀っお cron+launchd 䞡方走るのを避けるため)

macOS Sequoida環境ではcron daemonが起動しおいないため「cron行があっおも安党」ず思い蟌んでいたした。実際にはSequoideでもcron daemonが再起動するタむミング䞻にOSアップデヌト埌があり、そのずき䞡方が動き始めたす。今回はmacOSのマむナヌアップデヌトがそのタむミングでした。

盎し方。 crontab -lでlaunchd移行枈みの行を確認し、党削陀するか移行枈み行をコメントアりトしたす。䞀番安党なのはcrontab -r党削陀ですが、移行挏れがあるず取り返しが぀かないため、1行ず぀確認しながらcrontab -eで察凊したした。

この倱敗以来、applyの完了確認ずしお次の2コマンドをセットで走らせおいたす。

# launchd偎の皌働確認
launchctl list | grep com.shun

# cron偎の残骞確認0行ならOK
crontab -l 2>/dev/null | grep -vE '^\s*#' | grep -v '^$' | wc -l

2぀目のコマンドが0を返せば、有効なcron行は存圚しない。これが移行完了の刀断基準です。

詰たり5dryで確認したplistずapplyで配備されたplistが別物だった

症状。 dryで生成物を目芖確認→問題なし→applyを実行→なぜかスケゞュヌルが倉わっおいる。

原因。 PROPOSEDディレクトリ~/.claude/scripts/launchd-proposed/に前回dryした叀いplistが残っおいたした。今回dryでは別のcron行からの生成が走ったため、曎新されたplistず叀いplistが混圚しおいる状態になっおいたした。applyはPROPOSED/*.plistを党郚デプロむするため、意図しおいない叀い䞖代のplistも~/Library/LaunchAgents/に䞊曞きコピヌされたした。

for f in "$PROPOSED"/*.plist; do
  cp "$f" "$TARGET_DIR/"

この党コピヌが問題の起点です。

盎し方。 dryの前にPROPOSEDディレクトリを䞀床クリアする習慣を぀けたす。

rm -f ~/.claude/scripts/launchd-proposed/*.plist
~/.claude/scripts/cron-to-launchd.sh dry

あるいはapplyの盎前にdiffでPROPOSEDずLaunchAgentsの差分を確認する。どちらも手間ですが、スクリプト偎にクリヌン凊理が入っおいないため、珟状は手動の芏埋で補うしかありたせん。


ここたでの詰たりをたずめるず、スクリプトが自動化しおくれるのは「固定スケゞュヌル・絶察パス・PATH䞍芁」な行だけです。残り——*/N圢匏・~パス・nvm/pyenv䟝存——は手動で前凊理たたは埌凊理が必芁です。この境界を最初に把握しおいれば、4぀の倱敗のうち3぀は防げたした。97行のスクリプトは「cronの移行を党自動にする」ものではなく、「手䜜業の8割をスキップしお残り2割を浮き圫りにする」ツヌルずしお読むのが正確です。

぀たずきポむント

前段の「詰たった話」では5本の゚ピ゜ヌドを取り䞊げたした。ここでは同じ倱敗を繰り返さないために、萜ずし穎を䜓系的に敎理したす。たず「スクリプトが自動化できる範囲」を確認しおから、芋萜ずしやすい眠を䞀気に䞊べたす。

自動化できる範囲・できない範囲

cron-to-launchd.sh97行が正しく機胜するのは、次の条件をすべお満たすcron行だけです。

  • cron匏が固定倀のみ——*/N圢匏を含たない
  • コマンドが絶察パスで蚘述されおいる——~展開ではない
  • コマンド文字列に&・<・>が含たれない
  • 同䞀スクリプトが1時刻にのみ登録されおいる

この4条件を倖れた行は、自動生成が壊れるか、生成埌の手動修正が必須です。「crontabの党行を党自動で移行する」ものではなく、「4条件を満たす行の雛圢を8割䜜り、残り2割の手䜜業を浮き圫りにするツヌル」ずしお䜿うのが正確です。

萜ずし穎リスト実コヌド付き

XML゚スケヌプが&のみ——<ず>を含む行でplistが砎損する

スクリプト65行目を芋おください。

<string>${cmd//&/&amp;}</string>

&を&amp;に倉換しおいたすが、<→&lt;、>→&gt;の倉換はありたせん。crontabにcmd > /dev/null 2>&1のようなリダむレクト付きの行がある堎合、生成されるplistの<string>タグ内に>が混入し、XMLパヌサヌがplistを読めなくなりたす。launchctl loadぱラヌを返したすが、applyのルヌプ内では次のplistに進むため、1本の砎損を芋萜ずしやすい構造です。>や<を含む行は移行前にリダむレクトをスクリプト内郚に移すか、plistを手曞きしおください。

生成plistにRunAtLoadがない——apply盎埌の動䜜確認ができない

自動生成テンプレヌト54〜76行目党䜓にRunAtLoadキヌは存圚したせん。䞀方、手動で仕䞊げたcom.shun.daily-brief.plistの実ファむル28〜29行目には<key>RunAtLoad</key><true/>が入っおいたす。

RunAtLoadがないplistは、次のスケゞュヌル時刻たで実行されたせん。apply盎埌にログを確認しおも䜕も曞かれおいない状態は動䜜䞍良ではなく蚭蚈通りです。ただし「本圓に動くのか」をその堎でテストできないずいう問題がありたす。確認したい堎合はlaunchctl kickstartを䜿いたす

launchctl kickstart -k gui/$(id -u)/com.shun.xxx
tail -f ~/.claude/logs/com.shun.xxx.log

StartCalendarIntervalが<dict>単䜓——耇数時刻は手動で<array>化が必芁

生成スクリプトの46〜52行目のcal_xmlは<dict>1぀で完結したす。com.shun.daily-brief.plist実ファむル33〜47行目のように「8:00ず10:30の2回」を衚珟するには配列が必芁ですが、スクリプトは配列を生成したせん。

<!-- 自動生成物単䞀時刻しか衚珟できない -->
<key>StartCalendarInterval</key>
<dict>
  <key>Hour</key><integer>8</integer>
  <key>Minute</key><integer>0</integer>
</dict>

耇数時刻を同䞀スクリプトに割り圓おたい堎合は、生成埌にplistを手動で配列圢匏に曞き盎したす。

同名スクリプトを耇数時刻でcrontabに登録するず埌者のplistが前者を䞊曞き

daily-brief.shを8時ず10時半に動かしたくおcrontabに2行曞いたずしたす。

0  8  * * * /path/to/.claude/scripts/daily-brief.sh
30 10 * * * /path/to/.claude/scripts/daily-brief.sh

ラベル生成40行目はスクリプト名から拡匵子を陀くため、䞡行ずもcom.shun.daily-briefになりたす。plistファむル名もcom.shun.daily-brief.plistで同䞀。埌から凊理された行10:30の行が前の行8:00を䞊曞きし、8時の蚭定が消えたす。スクリプト偎に衝突怜知はありたせん。dryで生成物を目芖確認するしかありたせん。

*/N圢匏がsilent failする——launchctl loadは成功に芋える

p2で詳述した゚ピ゜ヌドの栞心を䞀行でたずめたす。*/15が<integer>*/15</integer>ずしお曞き出され、launchdが内郚怜蚌でplistを棄华したす。launchctl loadのコマンドは終了コヌド0を返すため成功に芋えたすが、launchctl list com.shun.xxxでサヌビスが芋぀からなければ棄华されおいたす。移行前に*/Nを含む行を手動で倉換するこずが唯䞀の解決策です。

~パスはラベル生成のフォヌルバックに萜ちる

35行目の正芏衚珟は絶察パスのみマッチしたす。~/.claude/scripts/note-autolike.shず曞いおいれば、フォヌルバック36〜39行目が動いおラベルがcom.shun.note-autolike-308のような末尟数字付きになりたす。以前に絶察パスで生成したcom.shun.note-autolike.plistが~/Library/LaunchAgents/に残っおいれば、2぀の別ラベルが同䞀スクリプトを呌ぶ二重起動状態が䜜られたす。crontabには必ず絶察パスで曞いおください。

PROPOSEDをクリアしないたたapplyするず叀いplistが混入する

87行目のfor f in "$PROPOSED"/*.plistはPROPOSEDの党ファむルを無差別にコピヌしたす。前回のdryで生成したが今回削陀したcron行のplistが残っおいるず、削陀したはずのゞョブがapplyで埩掻したす。dryを実行する前にrm -f ~/.claude/scripts/launchd-proposed/*.plistでクリアするのを習慣にしたす。

生成plistにEnvironmentVariablesがない——nvm・pyenv・Homebrewのコマンドが死ぬ

生成テンプレヌト54〜76行目にEnvironmentVariablesキヌは含たれおいたせん。launchdはナヌザヌの.zshrcを読たないため、nvmで管理するnodeを呌ぶスクリプトは起動盎埌にnode: command not foundで萜ちたす。タヌミナルから手動実行するず問題なく動くのに、launchd経由では死ぬ——この非察称が蚺断を難しくしたす。com.shun.daily-brief.plistの6〜9行目のPATH文字列を手本に、nodeやpythonを䜿う党plistに远蚘しおください。

LowPriorityIOずNiceが生成plistにない——自動化がフォアグラりンドを劚害する

com.shun.daily-brief.plistの12〜15行目にはLowPriorityIOずNice 10がありたすが、生成テンプレヌトにはありたせん。蚭定がなければバックグラりンドゞョブが通垞のI/O優先床で走りたす。ファむルの倧量読み曞きを䌎うゞョブが゚ディタやブラりザの応答を遅くした経隓がある堎合、このキヌの有無を確認しおください。

cron行の削陀忘れは時限爆匟——次のOSアップデヌトで二重起動する

スクリプトのapply埌94〜95行目に「cron行は手動で削陀しおください」ず譊告が出るだけで、削陀は自動化されおいたせん。Sequoiaではcron daemonが起動しないため「残しおも安党」ず思いがちですが、macOSのマむナヌアップデヌトでcron daemonが埩掻するケヌスが実際にありたす。note自動投皿が1日4回走っおAPIのレヌト制限に匕っかかったのはこの倱敗からです。移行完了の刀定に「cron残骞がれロ」を含めるこずで再発を防いでいたす。


ベストプラクティス

97行のスクリプトず半幎の運甚から蒞留した、移行䜜業・日垞運甚の䞡方で䜿えるルヌル集です。

1. crontabの゚ントリは絶察パスで曞く

~/.claude/scripts/xxx.shではなく、/home/.../.claude/scripts/xxx.shず曞きたす。35行目の正芏衚珟にマッチし、ラベルが意図したcom.shun.xxxになりたす。過去のcron行を曞き盎す手間はかかりたすが、移行埌のラベル衝突・二重起動・フォヌルバック呜名の混乱を3぀同時に防げたす。

2. */N圢匏は移行前に手動倉換する

*/15 * * * *15分おきは次のどちらかに倉換したす。間隔が固定ならStartInterval秒指定が最もシンプルです。

<key>StartInterval</key>
<integer>900</integer>  <!-- 900秒 = 15分 -->

特定分数での実行が必芁な堎合は固定倀を配列で列挙したす0・15・30・45分。倉換挏れはplutil -lintで怜蚌できたす。

3. dryの前にPROPOSEDディレクトリをクリアする

rm -f ~/.claude/scripts/launchd-proposed/*.plist
~/.claude/scripts/cron-to-launchd.sh dry

この2行をセットで実行するこずで、叀い䞖代のplistがapplyに混入する問題を防ぎたす。

4. dry埌にplutil -lintで党plistを怜蚌しおからapplyする

for f in ~/.claude/scripts/launchd-proposed/*.plist; do
  echo "--- $(basename $f)"
  plutil -lint "$f"
done

*/N混入・XML゚スケヌプ挏れ・構文゚ラヌをApple公匏ツヌルで事前に匟きたす。plutil -lintがOKを返さないplistはapplyしたせん。

5. apply盎埌に2コマンドで完了を確認する

# launchd偎の皌働確認
launchctl list | grep com.shun

# cron残骞確認0ならOK
crontab -l 2>/dev/null | grep -vE '^\s*#' | grep -v '^$' | wc -l

2行目が0を返しおlaunchd偎の゚ントリ数が移行察象行数ず䞀臎しおいれば、移行完了ず刀断できたす。

6. nodeを䜿うゞョブのplistにEnvironmentVariablesを手動远蚘する

生成盎埌に<key>ProgramArguments</key>の盎前ぞ挿入したす

<key>EnvironmentVariables</key>
<dict>
  <key>PATH</key>
  <string>~/.nvm/versions/node/v24.13.0/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:~/.local/bin</string>
</dict>

nvmのバヌゞョン番号は実環境に合わせたす。com.shun.daily-brief.plistの6〜9行目がモデルです。

7. 耇数時刻のplistはStartCalendarIntervalを配列に曞き盎す

同䞀スクリプトを2時刻で動かすなら、1぀のplistに配列を䜿いたすcrontabに2行曞いおラベル衝突させない。

<key>StartCalendarInterval</key>
<array>
  <dict><key>Hour</key><integer>8</integer><key>Minute</key><integer>0</integer></dict>
  <dict><key>Hour</key><integer>10</integer><key>Minute</key><integer>30</integer></dict>
</array>

com.shun.daily-brief.plistの33〜47行目の蚘述が実䟋です。

8. バックグラりンドゞョブ党般にLowPriorityIOずNice 10を蚭定する

䜜業の邪魔をしないために、生成plistすべおに远蚘したす

<key>LowPriorityIO</key><true/>
<key>Nice</key><integer>10</integer>

自動化環境が「存圚感を消す」こずで、人間の䜜業領域を䟵食しない蚭蚈にできたす。

9. コマンドに&・<・>が含たれる行は手動でplistを曞く

生成に頌らず、XML゚スケヌプを正確に行いたす

  • & → &amp;
  • < → &lt;
  • > → &gt;

リダむレクトは呌び出すシェルスクリプトの内郚に移すのが最もクリヌンです。plistのXML内でリダむレクトを扱おうずするず、ほが必ずこの゚スケヌプ問題に圓たりたす。

10. launchctl list com.shun.xxxでLastExitStatusを定期確認する

launchctl list com.shun.daily-brief

"LastExitStatus" = 0が正垞。0以倖の堎合はログを確認したす。週次での䞀括確認

launchctl list | grep com.shun | awk '{print $3}' | \
  xargs -I{} sh -c 'launchctl list "{}" 2>/dev/null' | \
  grep -E '"Label"|"LastExitStatus"'

11. launchctl kickstartでオンデマンド実行しおデバッグする

スケゞュヌル時刻を埅たず即時実行したい堎合

launchctl kickstart -k gui/$(id -u)/com.shun.xxx

-kは実行䞭のむンスタンスをkillしお再起動する冪等なオプションです。ログに䜕も出なければPATHかスクリプトのパス問題です。

12. macOSアップデヌト埌は党サヌビスの生死を確認する

マむナヌアップデヌトでlaunchdの動䜜が倉わるこずがありたす。アップデヌト翌朝のデむリヌブリヌフが来なければ、たずlaunchctl list | grep com.shunを叩きたす。サヌビスが消えおいれば再applyで埩掻したす。

13. 移行完了の「完了条件」を3぀定矩しおおく

移行䜜業の「終わり」が曖昧だず確認を省略しがちです。以䞋を完了条件ずしお蚭定しおいたす

  • launchctl list | grep com.shunの゚ントリ数が移行察象のcron行数ず䞀臎する
  • crontab -l 2>/dev/null | grep -vE '^\s*#' | grep -v '^$' | wc -lが0を返す
  • 党サヌビスのLastExitStatusが少なくずも初回実行埌に0になっおいる

この3぀を満たしお初めお「移行完了」ず蚀えたす。

14. 移行䜜業党䜓の所芁時間を事前に芋積もる

cron行の本数・*/N圢匏の含有数・nvm䟝存ゞョブの数を数えおから䜜業を始めたす。10本以䞋であればdry→plutil怜蚌→手動修正→apply→完了確認の䞀連が90分以内に終わりたす。30本以䞊ある堎合は4条件を満たす行を先に自動移行し、残りを翌日以降に手動移行する分割戊略が珟実的です。


たずめ

macOSのcron daemonが静かに止たる問題は、゚ラヌが出ないぶん発芋が遅れたす。crontab -lに゚ントリが䞊んでいるのに8時のデむリヌブリヌフが来ない、11時のSNS投皿が流れない——その違和感に気づくたで数時間かかる。launchdぞの移行は「cronが壊れおから察凊する」のではなく、「macOSのネむティブな仕組みに乗り盎す先行投資」ずしお捉えた方が正確です。

97行のスクリプトcron-to-launchd.shがやるこずは単玔です。crontabを1行ず぀読み、5フィヌルドをXMLに倉換し、plistずしお曞き出す。dry→plutil怜蚌→applyの3ステップで、固定スケゞュヌル・絶察パス・PATH䞍芁な行の雛圢を量産できたす。ただし「党自動の魔法」ではありたせん。*/N圢匏・~パス・nvm/pyenv䟝存・耇数時刻・XML゚スケヌプ察象文字——これらは手動で前凊理たたは埌凊理が必芁です。スクリプトが「9割の雛圢を䜜る」こずで、手動䜜業の察象が明確になる。その構造を理解しお䜿うこずが、移行埌に詰たらないための䞀番の近道です。

launchdに移し終えたゞョブは、launchctl list com.shun.xxxで状態を即座に確認できたす。LastExitStatusが0であるこずが、「動いおいるはず」ではなく「動いおいる」を蚌明したす。自埋環境の信頌性は、「動いおいるず思っおいたら止たっおいた」ずいう発芋を排陀するこずで積み䞊がっおいきたす。


仕組みの党䜓像・月120䞇の内蚳・30日手順は有料noteにたずめおいたす。

📕 Claude Code自埋環境で、実際どう皌ぐか ― 仕組み・実䟋・始め方・サポヌト


Lily@bokuwalily― 個人開発者。Claude Code で自動化基盀を組みながら、iOSアプリやWebサヌビスを量産しおいたす

  • 制䜜物・蚘事は bokuwalily.com にたずめおいたす🖥
  • AIで「寝おおも回る仕組み」を䜜っお月120䞇にした話は noteの有料蚘事 に💰
  • OSS: github.com/bokuwalily 🐙
  • 最新情報・お問い合わせは X @bokuwalily ぞ🌍
  • AI導入・自動化の盞談ず実装テンプレ7本の配垃は 公匏LINE から💬

皆さんの ❀ やシェアが励みになりたす

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?