Help us improve
Share bugs, ideas, or general feedback.
From claude-commands
Creates macOS launchd cleanup agents with dry-run verification. Writes cleanup script and plist installer, verifies with --dry-run, then installs and runs live.
npx claudepluginhub jleechanorg/claude-commands --plugin claude-commandsHow this skill is triggered — by the user, by Claude, or both
Slash command
/claude-commands:launchd-auto-cleanupWhen to use
When user wants to automate recurring cleanup of a directory, cache, or temp files on macOS via launchd
This skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill guarantees:
Playbook for macOS disk cleanup, dev-machine optimization, and proactive health alerting. Covers kernel panics, vm-compressor shortages, Jetsam events, full disks, and slow Macs.
Manages scheduled Claude Code tasks: add recurring/one-off skills/prompts/scripts, list/pause/resume/remove, view results/logs, test execution with safety controls and notifications. Cross-platform (macOS/Linux/Windows).
Diagnoses macOS issues: kernel panics, failing drives, slow boot, TCC permission denials, APFS snapshot bloat, wake failures, launchd audits, and startup item triage via logs, diskutil, and sysdiagnose.
Share bugs, ideas, or general feedback.
This skill guarantees:
--dry-run flag and runs a dry-run before any live deletionvalidate-cleanup-script.sh (no hardcoded paths, uses system tools only, has safety guards)StartCalendarInterval (not StartInterval) for human-readable scheduleslaunchctl bootout (with || true) before bootstrap to handle reinstallsvalidate-cleanup-script.sh is always run before reporting done| # | Item | Status |
|---|---|---|
| 1 | SKILL.md | ✓ present |
| 2 | Code (validate-cleanup-script.sh) | ✓ present |
| 3 | Unit tests (test_validate.sh) | ✓ present |
| 4 | Integration tests | N/A — scripts are deterministic, LLM generates them; no live endpoints |
| 5 | LLM evals | N/A — cleanup targets vary by user; contract is structural not semantic |
| 6 | Resolver trigger | pending — no RESOLVER.md found at ~/.claude/skills/RESOLVER.md |
| 7 | Resolver trigger eval | pending (depends on #6) |
| 8 | check-resolvable | pending (depends on #6) |
| 9 | E2E test | pending — requires macOS launchd context |
| 10 | Brain filing | N/A — skill does not write to brain |
Items 4–5 are intentionally N/A: the skill's output is structurally deterministic (a bash script); quality is verified by validate-cleanup-script.sh and dry-run, not LLM evals. Items 6–9 are pending RESOLVER.md infrastructure.
Before writing any files, gather:
~/.cache/opencode/bin/, /tmp/*.log)com.user.cleanup-opencode-bin)~/.local/bin/cleanup-opencode-bin.sh)Ask the user for any missing items before writing files.
Write <script_path> with:
#!/usr/bin/env bash
# cleanup-<target>.sh — Auto-generated by launchd-auto-cleanup skill
# Usage: cleanup-<target>.sh [--dry-run]
set -euo pipefail
DRY_RUN=false
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true
TARGET_DIR="$HOME/<relative/path>"
RETENTION_DAYS=<N>
LOG_PREFIX="[cleanup-<target>]"
if [[ ! -d "$TARGET_DIR" ]]; then
echo "$LOG_PREFIX Target dir does not exist: $TARGET_DIR — skipping"
exit 0
fi
echo "$LOG_PREFIX Starting. DRY_RUN=$DRY_RUN"
echo "$LOG_PREFIX Target: $TARGET_DIR (older than ${RETENTION_DAYS} days)"
# Find candidates
mapfile -t CANDIDATES < <(
/usr/bin/find "$TARGET_DIR" -maxdepth 1 -mindepth 1 \
-mtime +"$RETENTION_DAYS" 2>/dev/null
)
if [[ ${#CANDIDATES[@]} -eq 0 ]]; then
echo "$LOG_PREFIX No candidates found — nothing to do"
exit 0
fi
echo "$LOG_PREFIX Found ${#CANDIDATES[@]} candidate(s):"
for item in "${CANDIDATES[@]}"; do
echo " $item"
done
if $DRY_RUN; then
echo "$LOG_PREFIX DRY RUN — no deletions performed"
exit 0
fi
for item in "${CANDIDATES[@]}"; do
if [[ -e "$item" ]]; then
/bin/rm -rf "$item"
echo "$LOG_PREFIX Deleted: $item"
fi
done
echo "$LOG_PREFIX Done"
Key rules for the script body:
$HOME not ~ in variable assignments (both work in bash but $HOME is explicit and safe in all contexts)/usr/bin/find, /bin/rm, /usr/bin/wc, etc.-maxdepth 1 -mindepth 1 to avoid deleting the target dir itself[[ -d "$TARGET_DIR" ]] before operatingStandardOutPath captures it)Write install-<label>.sh:
#!/usr/bin/env bash
# install-<label>.sh — Installs launchd agent for cleanup-<target>
set -euo pipefail
LABEL="<com.user.label>"
SCRIPT_SRC="$(cd "$(dirname "$0")" && pwd)/cleanup-<target>.sh"
SCRIPT_DST="$HOME/.local/bin/cleanup-<target>.sh"
PLIST="$HOME/Library/LaunchAgents/${LABEL}.plist"
LOG_DIR="$HOME/Library/Logs"
# Install script
/bin/mkdir -p "$HOME/.local/bin"
/usr/bin/install -m 0755 "$SCRIPT_SRC" "$SCRIPT_DST"
# Write plist
/bin/cat > "$PLIST" << PLIST_EOF
<?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/bash</string>
<string>${SCRIPT_DST}</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Weekday</key>
<integer>0</integer>
<key>Hour</key>
<integer>3</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>${LOG_DIR}/${LABEL}.log</string>
<key>StandardErrorPath</key>
<string>${LOG_DIR}/${LABEL}.error.log</string>
</dict>
</plist>
PLIST_EOF
echo "Installed plist: $PLIST"
echo "Installed script: $SCRIPT_DST"
# Load agent (bootout first to handle reinstall)
/bin/launchctl bootout "gui/$(/usr/bin/id -u)/${LABEL}" 2>/dev/null || true
/bin/launchctl bootstrap "gui/$(/usr/bin/id -u)" "$PLIST"
echo "Agent loaded: $LABEL"
Run the cleanup script with --dry-run and review output:
bash <script_path> --dry-run
Expected output:
If dry-run output shows unexpected targets (e.g. the opencode bin/ itself, not just its contents), fix find flags (-mindepth 1) before proceeding.
Also run the validator:
~/.claude/skills/launchd-auto-cleanup/validate-cleanup-script.sh <script_path>
Do not proceed to Phase 5 until both pass.
bash <install_script_path>
Verify agent is loaded:
/bin/launchctl list | grep <label>
Run live once to confirm no errors:
bash <script_path>
Check log:
tail -20 ~/Library/Logs/<label>.log
Report at completion:
launchd-auto-cleanup complete
Script: ~/.local/bin/<name>.sh
Plist: ~/Library/LaunchAgents/<label>.plist
Schedule: <human schedule>
Dry-run: PASS (<N> candidates listed)
Validate: PASS
Live run: PASS (deleted N items) | PASS (nothing to delete)
Agent: loaded (launchctl list shows <label>)
Every cleanup script must be verified with --dry-run before any live run. This is a hard gate — not a suggestion.
[2026-05-22 04:05:01] === Section 1: ~/.gemini/tmp/ ===
[2026-05-22 04:05:01] gemini tmp: before 3.0G ($HOME/.gemini/tmp)
[2026-05-22 04:05:01] gemini tmp: [dry-run] would delete contents of $HOME/.gemini/tmp
...
[2026-05-22 04:05:02] === DRY-RUN complete — no files deleted ===
Key signals to check in dry-run output:
| Red flag | Root cause | Fix |
|---|---|---|
| Target dir itself listed as candidate | Missing -mindepth 1 in find | Add -mindepth 1 to exclude the root |
Binary or bin/ subdir listed | Over-broad find scope | Use explicit SAFE_SUBDIRS allowlist |
| Protected path appears | No protection check | Add guard: [[ "$item" == *"/.codex/sessions"* ]] && continue |
| No output at all | Wrong path, wrong mtime, dir empty | Verify with ls $TARGET manually |
| Non-zero exit | Script syntax/logic error | Fix before proceeding |
validate-cleanup-script.shThe validator (~/.claude/skills/launchd-auto-cleanup/validate-cleanup-script.sh) checks that --dry-run is present in the script source. Run it before installing any script:
~/.claude/skills/launchd-auto-cleanup/validate-cleanup-script.sh <script_path>
If the validator passes but dry-run output looks wrong, the validator cannot catch runtime behavior — review the dry-run output manually.
Every generated cleanup script MUST satisfy all of the following. validate-cleanup-script.sh checks the mechanical ones:
set -euo pipefail at top of script--dry-run flag handled and respected (no deletions when set)$HOME not ~ in variable assignments/usr/bin/find, /bin/rm, /usr/bin/wc, etc.brew, nvm, npm, node, python3 via homebrew) — launchd PATH is /usr/bin:/bin:/usr/sbin:/sbin onlyrm -rf (check [[ -e "$item" ]] in the deletion loop)~/.codex/sessions, ~/.claude/projects, or any path modified in last 14 days without APPROVED env var set.git/ guard: only delete a directory if you intend to delete a git clone; add [[ -d "$item/.git" ]] guard for git-clone targets-mindepth 1 in find to avoid deleting the target directory itself1. StartInterval vs StartCalendarInterval
StartInterval takes seconds (integer) — fires every N seconds from bootStartCalendarInterval takes a dict (Hour, Minute, Weekday, Day, Month) — cron-styleStartCalendarInterval for human schedules ("every Sunday at 3am")StartInterval 604800 (weekly) looks right but fires from boot time, not at a fixed clock time2. RunAtLoad trap
RunAtLoad: true fires the script on every login AND on the scheduleRunAtLoad unless you explicitly want the job to run at login/bootstrap3. PATH is minimal
/usr/bin:/bin:/usr/sbin:/sbin onlybrew, nvm, npm, node, python3 (homebrew), rtk — all unavailable4. Script not executable
chmod +x is not sufficient for cross-machine installs/usr/bin/install -m 0755 src dst in the install script5. Log dir must exist
StandardOutPath and StandardErrorPath dirs must exist before the agent runs~/Library/Logs/ always exists on macOS — safe default6. bootout before bootstrap for reinstalls
launchctl bootstrap errorslaunchctl bootout "gui/$(id -u)/<label>" 2>/dev/null || true before bootstrap7. GUI vs system agents
~/Library/LaunchAgents/ bootstrapped under gui/<uid>/Library/LaunchDaemons/ bootstrapped under systemlaunchctl load/unload (deprecated in macOS 10.11+)8. The opencode bin/ pattern (real incident)
~/.cache/opencode/bin/ (directory contents, not the dir itself)find $TARGET -mtime +30 with no -mindepth 1~/.cache/opencode/bin itself as a candidate (find with depth 0 includes the root)-mindepth 1 to exclude the target directory from results