macOS · launchd · Claude Code

Your disk fills up.
An agent opens by itself and fixes it.

A watcher checks free space every 15 minutes. Below the trigger it opens a workspace, starts Claude Code in it, and hands it one job: get you back above the goal, then close itself. Above the trigger it costs one statfs call.

View on GitHub
$ bun install && bun link
auto-cleanup-agent
activated — every 15 min, cleans up under 15 GB free until 30 GB. 12.4 GB free right now. turn it off with: auto-cleanup-agent --turn-off
tail -1 check.log
2026-08-21T13:27:34Z 12.4 GB free — under 15, starting agent

the premise

Deleting is easy. Deciding is not.

What a script gets wrong

target/ in a repo you build weekly comes back for free. target/ in a repo you archived two years ago is pure garbage. To du they are the same two letters and a slash.

How the work splits

The watcher decides when — cheap, deterministic, a syscall and an if. The agent decides what — contextual, and it stops and asks when it does not know.

the whole api

One command. It answers its own question.

auto-cleanup-agent
activate — or, if already active, say so and how to stop
auto-cleanup-agent --trigger 25 --goal 60
activate with your own thresholds
auto-cleanup-agent --turn-off
deactivate

There is no status subcommand to remember, because “is this thing on?” is answered by the command you already typed. The two thresholds live inside the launchd plist’s own arguments — no config file, nothing to drift out of sync with the thing that actually runs.

the guardrail

It will not delete your files.

The safe list is what a build regenerates: node_modules, target, .venv, build caches. Applied in slices, largest first, re-measuring between each. Outside that list it deletes nothing without asking — an 8 GB .dmg and an 8 GB source video look identical from here.

Always --physical on APFS

An OrbStack disk image reports 8.8 TB logical on a 256 GB SSD. Chase the logical number and you delete the wrong thing.

Uncommitted work is not garbage

node_modules/ comes back with a build. An untracked file comes back with nothing. git status --porcelain first.

what actually broke

Three failure modes, measured.

command not found: herdr

launchd starts with a minimal PATH. The first real trigger died on it. The plist now freezes the activating shell's PATH.

agent_pane_busy

A pane exists before its shell reaches a prompt. Starting the agent now polls instead of sleeping a guessed amount.

the watcher dying of success

A finished workspace that fails to close gates every future check, forever. --check now reaps a done workspace and carries on.