Zum Hauptinhalt springen
Echtzeit-Radar & Feeds
Alle RSS Feeds ➔
👥 Community & Social
YouTube Security VideosNutanix advances legacy and AI app management with AMD(01.10.2026 um 16:00 Uhr)
•
YouTube Security VideosPC-WELT: 32 TB SSD-Speicher in der HMX 6!(01.10.2026 um 16:15 Uhr)
••••
Videos & KonferenzenPC-WELT: 32 TB SSD-Speicher in der HMX 6!(01.10.2026 um 16:15 Uhr)
••
Sicherheitslücken (CVE)USN-8857-1: KCoreAddons vulnerability(01.10.2026 um 12:48 Uhr)
•••
YouTube Security VideosNutanix advances legacy and AI app management with AMD(01.10.2026 um 16:00 Uhr)
•
YouTube Security VideosPC-WELT: 32 TB SSD-Speicher in der HMX 6!(01.10.2026 um 16:15 Uhr)
••••
Videos & KonferenzenPC-WELT: 32 TB SSD-Speicher in der HMX 6!(01.10.2026 um 16:15 Uhr)
••
Sicherheitslücken (CVE)USN-8857-1: KCoreAddons vulnerability(01.10.2026 um 12:48 Uhr)
•••
Intelligence View
⚡ tsecurity.de Intelligence

Auto-Migrating Claude Code with a Throwaway launchd Job — a Script That Deletes Itself When Its Work Is Done

This continues my "Claude Code environment" series that started with showing rate limits live in the status line. This time it's a one-shot model-migration job…

Beitrag
0
Seite
0
↗ Quelle (dev.to)
Social ReaktionenReagiere als Erste:r — dein Feedback zählt!

This continues my "Claude Code environment" series that started with showing rate limits live in the status line. This time it's a one-shot model-migration job — the "throwaway job" pattern, where the job removes its own plist with launchctl unload once it's finished.



Fable 5 shuts down on 2026-07-07. If your Claude Code settings.json has model: fable-5, it will keep pointing at that stale ID from that day onward. You could just run jq by hand, but missing the shutdown moment, or being mid-task and forgetting, is almost guaranteed to happen. So I handed it off to launchd.






The problem: writing it in cron means it "keeps running after its job is done"



Writing a job that runs jq '.model = "opus"' ~/.claude/settings.json every day is trivial, but the day after the switch is done, a no-op process runs every single day. Having something written down permanently for a task you only do once just feels wrong.



The ideal is a job that "runs exactly once on 07-07 and then disappears."






Design: date guard + jq surgery + notification + self-destruct



It breaks into four steps.




























Step Purpose
Date guard Neutralize catch-up firing and early launches
jq surgery Replace only the model key in settings.json
osascript notification Let a human know the unattended run happened
launchctl unload De-register the job to make it single-use





The script itself



Here is the full text of ~/.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)"









① Date guard






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






launchd will sometimes catch-up-fire scheduled jobs when the Mac boots. There's also the case where the first firing arrives right after you launchctl load. By comparing date +%Y%m%d as an integer against 20260707, no matter when it's launched, it always bails out at zero cost if the date is before 07-07.






② jq surgery that touches exactly one key






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






Going through .tmp is so that if jq fails it won't corrupt the original file. After writing out, it verifies syntax with jq . and then does an atomic mv. Since there's a backup, worst case you can restore with cp "$SETTINGS.bak-model-transition" "$SETTINGS".



The reason for matching case-insensitively with grep -qi 'fable' is to catch any of fable-5, claude-fable-5, or FABLE. Using '.model // empty' makes it return an empty string instead of null when the model key itself is absent (i.e. the default is in use), which prevents an unbound variable error under set -uo pipefail.






③ Self-destruct






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






Whether it switched the model or it was already opus (no-op), whichever path it takes, it always reaches this line at the end. The || true absorbs the error when it's already been unloaded.



:::message

launchctl unload does not delete the plist file. ~/Library/LaunchAgents/com.shun.model-transition-0707.plist stays in place, so you can re-register it anytime with launchctl load. What's "throwaway" is only the job's registration state.

:::





Assembling the plist (three triggers a day)



~/Library/LaunchAgents/com.shun.model-transition-0707.plist (home path shown with ~ notation):




<?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>






Three times a day (06:50 / 12:50 / 20:50) are specified via the StartCalendarInterval array. Even if you miss the morning firing on 07-07, you can catch it at noon or in the evening. Because of the date guard, everything before 07-07 is skipped, and on and after 07-07 the first run does the switch and immediately self-destructs, so no later firings arrive.



Making StartCalendarInterval an array lets you bundle multiple times into a single plist. Be careful not to confuse it with the seconds-interval StartInterval.



:::message

The paths written in ProgramArguments need to be full paths, because launchd does not expand ~. The ~ notation above is for explanation; the actual plist needs to contain the paths with $HOME already expanded.

:::





Registration and verification commands





# 登録
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





If you want to try the actual switch locally, temporarily comment out the date-guard line in the script and run it (a backup is taken in settings.json.bak-model-transition).



The log from a normal firing at 06:50 on 07-07:




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






It's done in two lines, and because the job is gone, the later 12:50 and 20:50 don't fire.






Pitfalls I hit





  • Under set -uo pipefail, a missing model key kills the script with an unbound variable → have '.model // empty' return an empty string. Even if grep -qi gets an empty string, it's false and bails, so that's fine.


  • If jq's .tmp is left behind by a kill, the next launch references the old contents → mv is only reached after the syntax check, so it normally isn't left behind, but if you're worried you could add a guard at the top that deletes .tmp if it exists at launch.


  • Running launchctl unload in an already-unloaded state exits with an error → absorbed with || true.


  • launchd's default PATH can't find jq → explicitly set /opt/homebrew/bin in the plist's EnvironmentVariables.PATH.


  • The osascript notification doesn't show up → when calling it from launchd, use the full path /usr/bin/osascript. >/dev/null 2>&1 || true keeps a failed notification from halting the whole script.






Summary





  • A date guard (integer comparison of date +%Y%m%d) neutralizes catch-up firing and early launches.


  • jq's atomic rewrite via .tmp keeps settings.json from being corrupted.


  • osascript notification makes the unattended completion visible to a human.

  • Placing launchctl unload "$PLIST" on the final line makes it a one-time-only job on either path — success or no-op.



A "time-limited one-shot job" isn't limited to model migration — it's a pattern usable for any work with a fixed end: deprecations, campaign periods, migration schedules, and the like. You can handle the next migration just by changing the plist's label, date, and path.



Next time I plan to write about managing these launchd jobs as they accumulate — how to take stock of a ~/Library/LaunchAgents/ cluttered with scripts.






Written by **Lily* — I ship iOS apps and automate my content stack with Claude Code.

Follow along: Portfolio · X · GitHub*

🔍 CTI & Forensik

Cyber Threat Intelligence & Forensik

Bedrohungsgraph · ATT&CK-Mapping · Exploit-Belege
CTI Threat Relationship Graph
Akteure · Techniken · Beziehungen
4 Knoten · 3 Relationen
CVE / Incident Threat Actor Software MITRE ATT&CK CWE Weakness IoC
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Auto-Migrating Claude Code with a Throwaway launchd Job — a Script That Deletes Itself When Its Work Is Done

Thematisch verwandte Begriffe: AutoMigrating, Claude, Code, with · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

💬 Kommentare werden geladen…
Zum Aktualisieren ziehen
tsecurity.de Icon
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag