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?

使い捨てlaunchdジョブでClaude Codeを自動移行 ― 役目を終えたら自己消滅するスクリプト

0
Posted at

前回のステータスラインにレート制限を表示する話から続く「Claude Code環境」シリーズです。今回はモデル移行のone-shotジョブ ―― 完了したら自分のplistを launchctl unload で外す「使い捨てジョブ」パターンの話です。

Fable 5は2026-07-07に終了します。Claude Codeの settings.jsonmodel: fable-5 を書いていた場合、その日以降も古いIDを参照し続けます。手動でjqを叩けば済む話ですが、起動を逃す・作業中で忘れるはほぼ必ず起きます。だからlaunchdに任せた。

困りごと:cronで書くと「役目を終えたあとも動き続ける」

毎日 jq '.model = "opus"' ~/.claude/settings.json を回すジョブを書くのは簡単ですが、切り替えが終わった翌日からは何もしない処理が毎日走ります。一度しかやらないことをずっと書いてあるのは気持ち悪い。

理想は「07-07に1回だけ動いて、終わったら消える」ジョブです。

設計:日付ガード + jq外科手術 + 通知 + 自己消滅

4つのステップに分けます。

ステップ 目的
日付ガード catch-up発火・早期起動を無害化
jq外科手術 settings.json の model キーだけ置き換える
osascript通知 無人実行を人間に知らせる
launchctl unload ジョブを登録から外して使い捨てにする

スクリプト本体

~/.claude/scripts/model-transition-0707.sh の全文です。

#!/bin/bash
# model-transition-0707.sh — 2026-07-07にFable 5が終了するため、settings.jsonのmodelをopusへ自動切替
# 冪等: 7/7以降かつ model が fable の時だけ書き換える。成功したら自分のplistをunload。
set -uo pipefail
SETTINGS="$HOME/.claude/settings.json"
LOG="$HOME/.claude/logs/model-transition.log"
PLIST="$HOME/Library/LaunchAgents/com.shun.model-transition-0707.plist"

log() { echo "[$(date '+%F %T')] $*" >> "$LOG"; }

# 7/7より前なら何もしない(catch-up発火対策)
if [ "$(date +%Y%m%d)" -lt 20260707 ]; then
  log "skip: before 2026-07-07"; exit 0
fi

current=$(jq -r '.model // empty' "$SETTINGS")
if echo "$current" | grep -qi 'fable'; then
  cp "$SETTINGS" "$SETTINGS.bak-model-transition"
  jq '.model = "opus"' "$SETTINGS" > "$SETTINGS.tmp" && jq . "$SETTINGS.tmp" > /dev/null && mv "$SETTINGS.tmp" "$SETTINGS"
  log "switched model: $current -> opus"
  /usr/bin/osascript -e 'display notification "Fable 5終了に伴いデフォルトモデルをOpusへ切替えました" with title "Claude model transition"' >/dev/null 2>&1 || true
else
  log "no-op: model is already '$current'"
fi

# 役目を終えたらジョブを外す(plistは残す=再登録可能)
launchctl unload "$PLIST" 2>/dev/null || true
log "done (job unloaded)"

① 日付ガード

if [ "$(date +%Y%m%d)" -lt 20260707 ]; then
  log "skip: before 2026-07-07"; exit 0
fi

launchdはmacが起動したときにスケジュール済みジョブをcatch-upで発火させることがあります。また launchctl load した直後に最初の発火が来るケースも。date +%Y%m%d を整数として 20260707 と比較することで、どのタイミングで起動されても07-07より前なら必ずゼロコストで抜けます。

② jqで1キーだけ触る外科手術

cp "$SETTINGS" "$SETTINGS.bak-model-transition"
jq '.model = "opus"' "$SETTINGS" > "$SETTINGS.tmp" && jq . "$SETTINGS.tmp" > /dev/null && mv "$SETTINGS.tmp" "$SETTINGS"

.tmp 経由にするのは、jq がコケても元ファイルを壊さないためです。書き出し後に jq . で構文確認してからatomicに mv します。バックアップがあるので最悪でも cp "$SETTINGS.bak-model-transition" "$SETTINGS" で戻せます。

grep -qi 'fable' でcase-insensitiveにマッチしているのは fable-5claude-fable-5FABLE どれでも通すためです。'.model // empty' にしているのは model キー自体がないとき(デフォルト使用中)にnullを返させず空文字にするためで、set -uo pipefail 環境でunbound variableになるのを防ぎます。

③ 自己消滅

launchctl unload "$PLIST" 2>/dev/null || true
log "done (job unloaded)"

モデルを切り替えた場合も、すでにopusだった場合(no-op)も、どちらのパスを通っても最後に必ずこの行に到達します。|| true はすでにunload済みのときのエラーを吸収します。

launchctl unload はplistファイルを削除しません。~/Library/LaunchAgents/com.shun.model-transition-0707.plist はそのまま残るので launchctl load でいつでも再登録できます。「使い捨て」なのはジョブの登録状態だけです。

plistの組み方(1日3回トリガー)

~/Library/LaunchAgents/com.shun.model-transition-0707.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.shun.model-transition-0707</string>
  <key>ProgramArguments</key><array>
    <string>/bin/bash</string>
    <string>~/.claude/scripts/model-transition-0707.sh</string>
  </array>
  <key>StartCalendarInterval</key><array>
    <dict><key>Hour</key><integer>6</integer><key>Minute</key><integer>50</integer></dict>
    <dict><key>Hour</key><integer>12</integer><key>Minute</key><integer>50</integer></dict>
    <dict><key>Hour</key><integer>20</integer><key>Minute</key><integer>50</integer></dict>
  </array>
  <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>StandardErrorPath</key><string>~/.claude/logs/model-transition.err</string>
</dict></plist>

1日3回(06:50 / 12:50 / 20:50)を StartCalendarInterval の配列で指定しています。07-07当日に朝の発火を逃しても昼・夜で拾えます。日付ガードがあるので07-07より前はすべてskip、07-07以降は1回目で切り替えて即自己消滅するため、以降の発火は来ません。

StartCalendarInterval を配列にすることで複数時刻を1つのplistにまとめられます。秒間隔の StartInterval と混同しやすいので注意してください。

ProgramArguments に書くパスはlaunchdが ~ を展開しないためフルパスが必要です。上記の ~ 表記は説明用で、実際のplistには $HOME の展開後のパスを書く必要があります。

登録・確認コマンド

# 登録
launchctl load ~/Library/LaunchAgents/com.shun.model-transition-0707.plist

# 確認
launchctl list | grep model-transition

# 手動テスト(07-07より前なら "skip: before 2026-07-07" でログに記録されて終わる)
~/.claude/scripts/model-transition-0707.sh
tail -5 ~/.claude/logs/model-transition.log

実際の切り替えをローカルで試したい場合は、スクリプトの日付ガード行を一時コメントアウトして実行します(settings.json.bak-model-transition にバックアップが取られます)。

07-07当日の06:50に正常発火したときのログ:

[2026-07-07 06:50:02] switched model: claude-fable-5 -> opus
[2026-07-07 06:50:02] done (job unloaded)

2行で完了し、以降の12:50・20:50はジョブが消えているので発火しません。

踏んだ落とし穴

  • set -uo pipefailmodel キーがないと unbound variable で死ぬ'.model // empty' で空文字を返させる。grep -qi に空文字が来ても偽で抜けるのでOK
  • jqの .tmp がkillで残ると次回起動時に古い中身を参照するmv は構文確認後にのみ到達するので通常は残らないが、不安なら起動時に .tmp があれば削除するガードを頭に入れてもよい
  • すでにunload済みの状態で launchctl unload するとエラー終了|| true で吸収済み
  • launchdのデフォルトPATHではjqが見つからない → plistの EnvironmentVariables.PATH/opt/homebrew/bin を明示する
  • osascriptの通知が来ない → launchdから叩く場合は /usr/bin/osascript のフルパスを使う。>/dev/null 2>&1 || true で通知失敗がスクリプト全体を止めないようにする

まとめ

  • 日付ガードdate +%Y%m%d 整数比較)でcatch-up発火・早期起動を無害化する
  • jqの .tmp 経由アトミック書き換えでsettings.jsonを壊さない
  • osascript通知で無人完了を人間に見えるようにする
  • launchctl unload "$PLIST" を最終行に置けば、成功・no-opどちらのパスでも一度きりのジョブになる

「期限付きのone-shotジョブ」は、モデル移行に限らず廃止・キャンペーン期間・移行スケジュールなど、終わりが決まっている作業全般に使えるパターンです。plistのラベルと日付とパスを変えるだけで次の移行に対応できます。

次回は、こうしたlaunchdジョブが増えてきたときの管理 ―― スクリプトが散らかった ~/Library/LaunchAgents/ をどう棚卸しするかの話を書く予定です。


Lily@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています

皆さんの ❤️ やシェアが励みになります!

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?