The problem
Agentic work eats disk. Every task gets its own git worktree, and every worktree grows its own node_modules or Rust target/ folder. Agent sessions leave scratch folders behind. On 29 October I found 20 GB of session scratch that nothing could see. The Mac’s disk routinely sat in the nineties, and a full disk doesn’t fail politely: one release build’s output was lost when the disk filled.
I had a disk-cleanup skill that freed space in tiers. On 22 October, deleting the Gradle wrapper and a speech model broke a build. Cleaning was easy. Cleaning without losing work was the actual job.
What I built
Mac the Reaper is a background service, reapd, that reclaims space from the targets Surface’s disk maps find, behind gates. It has its own Mac app, a status icon in Surface’s menu bar, a Reap screen inside Collab, a CLI, and an MCP door so an agent session can see what’s safe to purge.
It ships in dry run. Until I type reaper execute on at the Mac, every run plans, checks every gate, and writes “would reap” to the ledger. No door — HTTP, MCP or Collab — can turn deleting on.
How it works
- Targets come from four places: Surface’s disk map, a curated list of known caches, a scout that walks my dev folder for build output beside its owner file and every registered worktree, and agent session scratch whose session has ended and gone untouched for two days.
- Surface’s disk map watches hotspots on three tiers. Tier 1, build output, caches and anything growing fast, is rescanned every 30 minutes. Tier 2, slower growers, every 3 hours. Tier 3, big and slow-moving, every 12 hours, with a full scan at 03:30 and 15:30. A directory that starts growing 500 MB a day or more is promoted to tier 1 on its own, so an accumulation is spotted within half an hour, not at tomorrow’s full scan. After each full scan a small model re-sorts the tiers. It may move a directory between tiers and nothing else: only fixed rules can call something safe to delete.
- The Reaper only trusts a tier while its scan is fresh, no older than twice that tier’s interval. A stale tier is skipped and named, never guessed at.
- Five gates run at plan time and again right before each delete: safety (a real directory, not a symlink, not on the never list), starred project, git (nothing unpushed, and a worktree root must be clean), active today, and in use (no process or live session inside it). Any gate that can’t answer fails closed.
- Each target gets a verdict:
unattended(lossless caches, and build output in repos idle seven days or more),run(waits for a person to say go — every worktree, always),approval,approved, ornever, shown with the reason. - Worktrees are removed with
git worktree removewithout--force, so git itself refuses one with changes as a last gate. Stashes live in the repo, so reaping a worktree never loses one; each card shows the repo’s stashes with a verdict of “safe to drop” or “rescue”. - It runs nightly at 04:00, after Surface’s 03:30 full scan, and on its own when free space drops under 20 GB. Under 10 GB, the three biggest gated targets become approval requests with one critical headline.
Agents can see their own mess
The sessions that make the mess can see it too. The Reaper has an MCP door, so an agent session can ask what’s taking space, what’s in use and what isn’t, and why each target got its verdict. That way it doesn’t have to guess, and doesn’t have to ask me:
reaper_summary free space, whether deleting is on, pending approvals, recent runs
reaper_targets the current plan: every target, its verdict and the reason
reaper_plan re-plan now
reaper_stashes a repo's stashes, and whether each is safe to drop
reaper_request_approval ask me about something a gate is holding
reaper_run reap what's already cleared to go
reaper_ledger what went, every gate's answer, and how to get it back
So a session that runs low on space in the middle of a build can find its own stale worktrees and build output and reap them right then, without waiting for 04:00. It reaps only what the gates have already cleared. It can’t approve anything a gate is holding, and it can’t turn deleting on. Surface’s disk map has its own MCP door too (disk_tiers, disk_growth, disk_activity), so a session can see where the space is going before it asks.
Decisions worth noting
Deletes are immediate, and the ledger is the undo. There’s no holding folder — a holding folder on a full disk is a second problem. Every ledger row says how to get the thing back: npm install, cargo build, “regenerates on its own”.
An approval can’t override everything. It lapses in 24 hours and overrides only “starred” and “active today”. It never overrides git or in-use, because those protect work that would be lost. An approval given during a dry run is spent by it and never deletes later.
Agents can’t approve and can’t turn deleting on. An agent can plan, ask for approval, and start a run of what may already go. The decision stays mine.
The dev-root rule, bought the hard way. The first night it ran for real, unattended runs took targets that only Surface’s map named, across my whole home folder, and broke installed software: a global npm tool’s node_modules, a VS Code extension’s dist, Playwright’s browsers. A node_modules beside a package.json is build output inside a repo and installed software outside one. Now unattended reaping only touches build output in repos under my dev folder; anything else waits for a person.
Where it fits
Surface measures; Mac the Reaper deletes. It reads Surface’s tier map and the vault’s session ledger, so it knows which sessions are live and which repos were touched today. It sends approval requests through Headlines as blocking cards with Approve and Deny. Collab’s Reap screen mirrors the standalone page and never gets a feature the standalone page lacks.

Outcome
In the week of the screenshot, it freed 49.2 GB. The first-night incident is why the gates look the way they do: every lesson from it, and from the cleanup skill before it, is now a rule in the code — never follow a symlink, fail closed on unpushed work, never the Gradle wrapper, app caches only while the app isn’t running.