From claude-commands
Creates macOS launchd cleanup agents with dry-run verification. Writes cleanup scripts and plist installers, verifies with --dry-run, then installs via launchctl. Useful for automating recurring directory, cache, or temp file cleanup on macOS.
How 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:
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 resultsnpx claudepluginhub jleechanorg/claude-commands --plugin claude-commandsAutomates macOS disk cleanup and memory monitoring with Mole-based safety guards and LaunchAgent alerting. Responds to low disk space, kernel panics, and vm-compressor shortages on Apple Silicon.
Analyzes local Mac disk usage, validates snapshot completeness, and identifies cleanup candidates. Defaults to dry-run preview and requires explicit approval before any deletion. Useful when disk usage is high or user wants to understand disk growth.
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).