launchd-auto-cleanup
Create macOS launchd cleanup agents with dry-run verification. Writes cleanup script + plist installer, verifies with --dry-run, then installs and runs live.
- License 许可
- MIT
- 最近更新
- 2026-11-14
- Stars
- ★ 6
- Forks
- 0
01 前言 Front Matter
- name
- launchd-auto-cleanup
- context
- inline
- triggers
- [ "automate cleanup", "launchd cleanup job", "schedule cleanup", "cleanup job", "auto clean", "recurring cleanup", "create launchd agent", "install launchd plist" ]
- description
- Create macOS launchd cleanup agents with dry-run verification. Writes cleanup script + plist installer, verifies with --dry-run, then installs and runs live.
- when_to_use
- When user wants to automate recurring cleanup of a directory, cache, or temp files on macOS via launchd
- allowed-tools
- Read, Write, Edit, Bash
02 资源文件 5
- 参考文档manifest.json86 B
- 参考文档references/migration.md5.9 KB
- 参考文档references/patterns.md8.6 KB
- 脚本tests/test_validate.sh5.9 KB
- 脚本validate-cleanup-script.sh2.7 KB
03 SKILL.md 正文
launchd-auto-cleanup
Contract
This skill guarantees:
- Every generated cleanup script has a
--dry-runflag and runs a dry-run before any live deletion - The generated script passes
validate-cleanup-script.sh(no hardcoded paths, uses system tools only, has safety guards) - The plist installer uses
StartCalendarInterval(notStartInterval) for human-readable schedules - The install sequence runs
launchctl bootout(with|| true) beforebootstrapto handle reinstalls validate-cleanup-script.shis always run before reporting done
10-Item Checklist Status
| # | 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.
Phases
Phase 1: Audit Target
Before writing any files, gather:
- Target path: directory, file pattern, or cache location to clean (e.g.
~/.cache/opencode/bin/,/tmp/*.log) - Retention rule: age threshold in days (e.g. "older than 30 days") or size threshold
- Schedule: human schedule (e.g. "every Sunday at 3am", "daily at 2am")
- Label: reverse-DNS launchd label (e.g.
com.user.cleanup-opencode-bin) - Script path: where to install the cleanup script (e.g.
~/.local/bin/cleanup-opencode-bin.sh)
Ask the user for any missing items before writing files.
Phase 2: Write Cleanup Script
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:
- Use
$HOMEnot~in variable assignments (both work in bash but$HOMEis explicit and safe in all contexts) - Use absolute paths for all external commands:
/usr/bin/find,/bin/rm,/usr/bin/wc, etc. -maxdepth 1 -mindepth 1to avoid deleting the target dir itself- Always check
[[ -d "$TARGET_DIR" ]]before operating - Log every action to stdout (plist's
StandardOutPathcaptures it)
Phase 3: Write Install Script
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"
Phase 4: Dry-Run Verify
Run the cleanup script with --dry-run and review output:
bash <script_path> --dry-run
Expected output:
- Lists candidate files/dirs it would delete
- Ends with "DRY RUN — no deletions performed"
- Zero exit code
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.
Phase 5: Install and Run Live
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
Output Format
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>)
Dry-Run Gate Protocol
Every cleanup script must be verified with --dry-run before any live run. This is a hard gate — not a suggestion.
What a compliant dry-run looks like
[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:
- Every intended target appears in the output
- No unexpected targets (especially parent dirs, binaries, or active data dirs)
- "DRY-RUN complete" or "DRY RUN — no deletions performed" at the end
- Exit code 0
Red flags that require stopping and fixing before live run
| 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 |
Dry-run enforcement in validate-cleanup-script.sh
The 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.
Safety Guard Checklist
Every generated cleanup script MUST satisfy all of the following. validate-cleanup-script.sh checks the mechanical ones:
- [ ]
set -euo pipefailat top of script - [ ]
--dry-runflag handled and respected (no deletions when set) - [ ] All paths use
$HOMEnot~in variable assignments - [ ] All external commands use absolute paths:
/usr/bin/find,/bin/rm,/usr/bin/wc, etc. - [ ] No homebrew tools (
brew,nvm,npm,node,python3via homebrew) — launchd PATH is/usr/bin:/bin:/usr/sbin:/sbinonly - [ ] Existence check before
rm -rf(check[[ -e "$item" ]]in the deletion loop) - [ ] Directory existence check before operating on target
- [ ] Never delete
~/.codex/sessions,~/.claude/projects, or any path modified in last 14 days withoutAPPROVEDenv 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 - [ ] Log every deletion to stdout (caller redirects to log file via plist)
- [ ]
-mindepth 1in find to avoid deleting the target directory itself
Common launchd Failure Patterns
1. StartInterval vs StartCalendarInterval
StartIntervaltakes seconds (integer) — fires every N seconds from bootStartCalendarIntervaltakes a dict (Hour, Minute, Weekday, Day, Month) — cron-style- Use
StartCalendarIntervalfor human schedules ("every Sunday at 3am") - Common mistake: using
StartInterval 604800(weekly) looks right but fires from boot time, not at a fixed clock time
2. RunAtLoad trap
RunAtLoad: truefires the script on every login AND on the schedule- Omit
RunAtLoadunless you explicitly want the job to run at login/bootstrap - If present accidentally, it causes unexpected runs on every reboot
3. PATH is minimal
- launchd agent PATH:
/usr/bin:/bin:/usr/sbin:/sbinonly brew,nvm,npm,node,python3(homebrew),rtk— all unavailable- Fix: use absolute paths for every external command in the script
4. Script not executable
chmod +xis not sufficient for cross-machine installs- Use
/usr/bin/install -m 0755 src dstin the install script
5. Log dir must exist
StandardOutPathandStandardErrorPathdirs must exist before the agent runs~/Library/Logs/always exists on macOS — safe default
6. bootout before bootstrap for reinstalls
- If agent is already loaded,
launchctl bootstraperrors - Pattern:
launchctl bootout "gui/$(id -u)/<label>" 2>/dev/null || truebefore bootstrap
7. GUI vs system agents
- User agents:
~/Library/LaunchAgents/bootstrapped undergui/<uid> - System agents:
/Library/LaunchDaemons/bootstrapped undersystem - Never use
launchctl load/unload(deprecated in macOS 10.11+)
8. The opencode bin/ pattern (real incident)
- Target was
~/.cache/opencode/bin/(directory contents, not the dir itself) - Script used
find $TARGET -mtime +30with no-mindepth 1 - Dry-run showed
~/.cache/opencode/binitself as a candidate (find with depth 0 includes the root) - Fix: always add
-mindepth 1to exclude the target directory from results
04 同仓库更多 Skills
instinct
Use when the user invokes /instinct or /in, or asks to "message Instinct", "ask Instinct", "tell Instinct", "check Instinct", or read replies from the configured peer assistant. iMessage first, WhatsApp Web as fallback.
hr
Compatibility alias for history-resume; produce the same fresh-state, provenance-aware resume briefing.
history-resume
Resume briefing for the current repository branch or worktree using fresh state and admitted history sources.
grok
Alias for the canonical grokbot browser conversation workflow; /grok talks to the Grok bot the same way /dot talks to Dot.
codexa-bench
Compare `codexa` and normal `codex` on a fair split of comparable work by default; use `--strict` for duplicate runs of the exact same task.
frontend-design
Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.