disk-root-cause
Use when a user asks why a disk is filling, what grew, or how much can be reclaimed. Reclaimable-amount estimates require leaf-level attribution; plain capacity checks use disk-audit.
- License 许可
- MIT
- 最近更新
- 2026-11-14
- Stars
- ★ 6
- Forks
- 0
01 前言 Front Matter
- name
- disk-root-cause
- description
- Use when a user asks why a disk is filling, what grew, or how much can be reclaimed. Reclaimable-amount estimates require leaf-level attribution; plain capacity checks use disk-audit.
03 SKILL.md 正文
disk-root-cause — forensic attribution skill
When a user asks why the disk filled up, what grew over a window, or where a specific bucket came from, this skill is the entry point. A plain capacity check routes to the zero-du triage in ../disk-audit/SKILL.md. For disk attribution and reclaim estimates, use the state repository reported by diskm state status and prefer parallel fan-out when resources permit.
Mandatory 100 MiB directory descent
Every measurement, inventory, report, age decision, and reclaim-candidate scan must resolve directories to individual files and directories smaller than 104857600 bytes (100 MiB). A directory at exactly 100 MiB must be descended into. Files are terminal items at any size; name and size every file at or above 100 MiB. Never treat a fixed class name, depth, Git marker, protected status, or configuration ceiling as a successful measurement stop.
Continue read-only metadata measurement inside Git worktrees, source trees, sessions, and other protected paths. Measurement does not authorize deletion, content reads, or changes: preserve existing owner, lease, cwd, open-file, dirty/unpushed, secret, and retention gates. Never follow a symlink; measure it as a link leaf. Preserve inode and hardlink deduplication.
A real permission, identity, enumeration, or deadline failure is unfinished work. Name the blocked path and reason, preserve its resume point, and exclude its parent and subtree from complete totals, floors, age decisions, and reclaim estimates. Report aggregate directory sizes as navigation hints only; they do not replace leaf itemization or prove reclaimability.
Default first-use workflow
Start with the ground-truth probes defined in ../disk-audit/SKILL.md, including the fleet check, existing per-host ledger, and 7-day growth history. Read those results before drilling down.
For root-cause, floor, growth, or reclaim-estimate requests, run the full diskm audit after these probes unless an existing current report already proves complete 100 MiB leaf coverage for every requested path and period. Directory-aggregate snapshots cannot prove that coverage. A quick capacity-only request may stop after the triage if its answer is sufficient.
The audit is the three-lane diagnostic. Its top-down accounting must resolve requested Data paths into file leaves and directories below 100 MiB. This instruction defines the required result; it does not change the scanner. Check the emitted report's actual resolution and unfinished paths. Until the installed runtime produces complete leaf itemization, treat coarse or unfinished paths as incomplete and any reclaim estimate that depends on them as unknown. The audit launches in parallel:
- top-down whole-disk accounting from physical/APFS capacity through the Data volume to individual files and directories smaller than 100 MiB;
- coverage-validated snapshot deltas, so a coverage change cannot masquerade as physical growth; and
- safe quick wins and obvious outliers, with cleanup commands still behind repository safety gates.
Read the whole report before drilling down. Keep permission/TCC denial, time budget, node budget, cross-device boundaries, APFS sibling allocations, purgeable estimates, and residual bytes as separate fields. The residual is an attribution gap: it is not backup size and not reclaimable without evidence.
The 900-second wall-clock cap is the infinite-process guard; live whole-volume runs on this host vary from about 6.5 minutes to more than 8 minutes under I/O load, so 480 seconds was empirically insufficient. Separately, the 100,000,000-record ceiling is a deliberately remote post-scan parser/ledger sanity guard (or a visited-path guard in the legacy fallback), not a normal coverage budget. GNU du stdout is spooled to a temporary file rather than retained in memory before that validation. The human-readable report must show the backend, records/paths processed, and this ceiling.
The displayed Data equation must use one accepted leaf ledger: measured file leaves + measured directory leaves below 100 MiB + measured tail + purgeable estimate + unresolved residual = Data used. Report APFS container-free space and sibling-volume allocations in a separate container-level reconciliation so they are not mistaken for Data leaves or counted twice. A directory total, synthetic direct-allocation segment, or arbitrary size bucket is not a resolved leaf. Measure every file leaf and descend every directory at or above 100 MiB; name files at or above 100 MiB individually. Smaller file leaves may be compactly displayed as lossless count-and-size totals only after each file was measured and inode deduplication applied. Permission, identity, time, node, enumeration, or unscanned cross-device children stay on the named unfinished frontier and inside residual. Never reuse an earlier parent observation. A hidden measured-total equation does not validate the displayed leaves. Capture Data used before and after the namespace walk; when it changes, report the residual interval and mark the measurement non-atomic.
The top-down scanner prefers one NUL-delimited, multi-root GNU gdu inventory process. Never sum independent per-shard du processes: hardlink deduplication is process-local. The one pass must traverse all measurable directories to the 100 MiB stop floor and preserve hardlink/inode deduplication. Do not replace file rows with synthetic direct-allocation chunks. Localized permission/TCC errors taint their subtree and every ancestor; unreadable or unfinished subtrees remain explicitly incomplete. Unknown diagnostics, malformed output, or timeout fail closed to the bounded frontier fallback (gdu, then dua, then macOS du) and preserve the resume point. Fast backends must not be wrapped in taskpolicy -b, which can starve them under host I/O pressure. Symlink leaves never pull their targets into the ledger.
Pair with disk-magician skill (measurement + safe cleanup) and the CLAUDE.md / AGENTS.md in this repo (cross-repo authorization + never-delete list).
When to use
Trigger when the user asks any of:
- "Why is my disk filling up"
- "What grew over the last day/week/month"
- "Find the minimum disk used in the last month" (floor)
- "Root cause the disk growth"
- "Show me the delta between then and now"
- "Which bucket is worth reclaiming, and why" or "how much can I free" — both require full leaf-level attribution. A quick available-space question with no reclaim estimate goes to
../disk-audit/SKILL.md. This skill reports findings and typed recommendations; cleanup commands are separate.
For a one-file size query, report that file's size directly. A directory below 100 MiB may be reported as one measured unit; for any directory at or above 100 MiB, descend into files and smaller directories before reporting age, attribution, or reclaimability.
Hard rules (skill-wide)
- Action scope and cleanup gates: Perform cleanup only when the live request or standing authorization covers the same action, targets, and result. Authorization persists across the steps needed to complete that task; do not ask again for those steps. Route each deletion through its canonical
diskmcommand and satisfy its current target-specific gates. Ask before adding new actions or targets not already covered; never userm -rfdirectly. - Never-delete list (hard stop for removal by every route):
~/.codex/sessions*,~/.codex/state*.sqlite,~/.codex/log, and~/.claude/projects. Continue read-only metadata descent through directories at or above 100 MiB; never delete, alter, or read protected file contents. - Symlink gotcha — always realpath-verify: agent-home aliases (for example backup or prod copies of
${HERMES_HOME:-$HOME/.hermes}) are often symlinks to the live home;/tmp,/var,/etcare volume-root symlinks to/private/*. Naiveduover args triples-counts them. Usedu -P(no-follow) and dedup by realpath when comparing totals. - No VACATE-without-name. When a user says "vacate / pause / stop the CI runners", verify the runners' actual supervisor first — self-hosted GitHub Actions runners are often plain processes under their install directory, not a service job. Stopping the wrong thing (e.g. an unrelated service) is harmless but accomplishes nothing; stopping the GH Actions runners pauses live CI. Always inspect the service list (
launchctl liston macOS,systemctl --user list-unitson Linux) andps aux | grep <hint>before any destructive action, and report the candidate names back to the user verbatim. - Read-first, deduce-later. Do not assert "X grew 5 GB" from one
du; cross-check against snapshot history (a committed git repo on disk) and the livedf -koutput. If two sources disagree, attribute honestly. - Stay within authorized scope. Do not use an auto-remediator outside the canonical
diskmcommands. Continue cleanup that the current request authorizes and that the canonical per-target gates permit; ask only before a new action or target the request does not cover. - Honest attribution in final report. If disk improved mid-analysis for reasons outside this session (concurrent sweepers, OS reclaims), say so explicitly. Never claim a delta whose source you didn't observe.
Phase 0 — environment ground truth (always run)
# Resolve the snapshot from the machine-specific state repository reported by the CLI.
df -h
STATE_REPO="$(diskm state status | sed -n 's/^\[state_repo\] state repo: //p')"
if [[ -z "$STATE_REPO" || ! -d "$STATE_REPO" ]]; then
echo "state repo unavailable; snapshot history is incomplete" >&2
exit 1
fi
SNAP="$STATE_REPO/snapshots/disk_snapshot.json"
git -C "$STATE_REPO" log --oneline -1 -- 'backup/*/disk_snapshot.json' snapshots/disk_snapshot.json 2>/dev/null
if [[ -r "$SNAP" ]]; then
python3 -c 'import json,sys; from pathlib import Path; d=json.loads(Path(sys.argv[1]).read_text()); print("schema_version:", d.get("schema_version"), "coverage:", d.get("snapshot_coverage_pct"))' "$SNAP"
else
echo "snapshot unavailable at resolved state-repo path: $SNAP" >&2
fi
If schema_version != 2, the snapshot is pre-v2 (coverage inflates by parent/child double-count). Use coverage_pct_raw_v1 from snapshot_metadata for trend continuity and note the inflation.
Phase 1 — floor + deltas (snapshot history)
The Git root for current state is the repository reported by diskm state status. Use resolver-backed history commands for archived samples; do not assume frozen backup/<host>/disk_snapshot.json files live under the current state repo. A frozen archive may contribute only a historical sample whose embedded capture time is in the requested window and whose coverage is at least 70%. Never use an archived file as the current snapshot or count duplicate captures twice.
Three deltas, always reported:
| Window | Find | Compare to |
|---|---|---|
| Last week (7 d) | Minimum disk_used_gb in window from snapshots with snapshot_coverage_pct >= 70% |
live df -k |
| Last month (30 d) | Minimum disk_used_gb in window from snapshots with snapshot_coverage_pct >= 70% |
live df -k |
| All time floor | Minimum covered disk_used_gb over reachable + reflog-recovered commits |
live df -k |
Pull both disk_used_gb (denominator, in GiB — verified to use 1024*1024 divisor in disk_snapshot.sh:200-201) and disk_free_gb. Compute:
delta_used = used_now − used_min
delta_free = free_min − free_now
Per-directory snapshot deltas locate growers but remain directory aggregates. They do not establish file-level age, full attribution, or reclaimability. For a current attribution or reclaim estimate, descend every directory at or above 100 MiB to file and small-directory leaves. For null keys under disk pressure (the timeout_keys field), continue bounded itemization or report the exact unfinished key; a live du -sk parent total is not a substitute.
Coverage gate: exclude snapshots with missing, null, or snapshot_coverage_pct < 70% from floor selection and exact per-directory delta comparisons; keep them as low-coverage context only. The topdown_coverage field embeds the frontier scanner's ~/.disk_magician_state/frontier_last.json summary if <36h old; use frontier_unfinished_count and residual_kb to name unmeasured tails. If no qualifying snapshot remains, report the snapshot floor unavailable and use the df-only daily host_disk.used_kb history in the configured disk observer log and its rotations. Report observed days and earliest/latest dates; do not infer all-time floors or per-path deltas from that fallback.
Phase 2 — parallel-subagent fan-out (when scope demands it)
For a point size query on an individual file, report that file's size. A parent du total locates a directory; it never replaces descent at or above 100 MiB. When the attribution crosses the boundary into "find everything growing in category X", fan out via /swarm:
| Lane | Fan-out scope | Sample prompt shape |
|---|---|---|
| lane-audit | Reclaim-candidate inventory across dev caches, worktrees, AO sessions, /tmp, and Library caches | "Descend each directory at or above 100 MiB; measure file-level size/activity. Use cleanup previews and preserve their delete gates. Report unfinished paths and skip reasons." |
| lane-history | Snapshot-history growth attribution | "From the state repository reported by diskm state status, identify directory aggregates that grew between <floor-sha> and HEAD; treat these as navigation hints and itemize any current directory at or above 100 MiB before attributing or estimating reclaim. Cite commit SHAs." |
| lane-tmp | /private/tmp live holders vs orphan candidates | "Use directory sizes to locate candidates; descend each directory at or above 100 MiB to files and smaller directories. List each terminal item's size, age, open-handle/cwd owner, and cleanup gate." |
| lane-colima | Colima sparse-disk inflation + recovery sequence | "Measure the VM image file and its allocation; do not treat the enclosing ~/.colima directory total as itemized. Run the authorized fstrim sequence and measure its result. NEVER docker prune -af." |
| lane-frontier | Configure disk_frontier_scan.py --granularity-gib 0.09765625 --output-default after the scanner passes the inclusive-boundary and file-leaf checks |
Do not claim complete leaves from this flag alone; verify that equality descends and that no synthetic chunks or skipped children remain. Record unfinished paths. |
| lane-critic | Adversarial reality-check on top claims | "Try to refute each finding by recomputing from raw data. Cite file:line." |
Fan-out rule: single-writer per file, grep -n "agent(" <swarm-script> cost-routed (haiku for mechanical, sonnet for analysis), staggered starts (/swarm, "Long runs"), explicit --model: on every call. Adversarial verify ≥3 lenses before any actionable claim.
Phase 3 — readout format (always)
⚠ Limits: coverage < X% / timeouts / pinned-subtrees HELD
↘ FLOOR (last month): used=YGB / free=YGB @ <iso> (commit <sha>)
↘ FLOOR (last week): used=YGB / free=YGB @ <iso> (commit <sha>)
↘ NOW (live df): used=YGB / free=YGB (GiB, GiB; units confirmed)
↘ DELTA (30 d): used_now − used_min = ±Y GB; free_min − free_now = ±Y GB
↘ Per-bucket attribution (non-overlapping sums of measured file/small-directory leaves; directory aggregates are navigation hints only):
- colima: +X (sum of measured leaves; sawtooth Y; what range over time)
- /tmp: +X (sum of measured leaves)
- AO sessions: +X (sum of measured leaves; post-.bak-drop, mostly live)
- ~/projects: +X (sum of measured leaves)
- residual (unaccounted): X (frontalier scanner names these within 36h of first nightly run)
↘ Safe-cleanup recommendations (require explicit OK):
- run A: cleanup_tmp.sh --clean (~X GB, low risk)
- run B: VACATE_CI_RUNNERS_APPROVED — needs user to confirm GitHub Actions runner target
- run C: ...
↘ Evidence / proofs (commit SHAs, snapshot JSON keys, df outputs, du numbers)
Cleanup-command catalogue (read-only — never auto-runs)
| Cmd | Use | Requires |
|---|---|---|
./scripts/cleanup_tmp.sh --clean |
>240min /tmp + agent scratch | Nothing (script dry-runs when defaults don't apply) |
./scripts/cleanup_dev_caches.sh --clean (via disk_audit.sh --clean) |
uv/pre-commit/cursor-agent/claude-cli caches | DISK_MAGICIAN_AUTO_CLEAN=1 |
user_scope/scripts/cleanup-ao-sessions.sh --drop-bak --days N |
.bak chains (post-6poe fix) | N chosen by caller |
./scripts/cleanup_colima.sh --clean |
Docker prune + fstrim (compresses host sparse disk) | Nothing (preserves running containers via docker prune semantics) |
./scripts/cleanup_worktrees.sh --clean |
Antigravity worktree GC | pre-WORKTREE-APPROVED if plan targets stale |
./scripts/cleanup_apfs_snapshots.sh --clean |
OS update snapshots >24h old | sudo (script silently fails without) |
cleanup-ao-sessions.sh --days 0 |
Force ALL AO backups drop (aggressive) | User OK |
tmutil thinlocalsnapshots |
APFS local TM snapshots reclaim | sudo + user OK (impacts Time Machine reversibility) |
Known traps (read this before answering)
- Snapshot coverage collapses under disk pressure — directories can be
nullor unfinished after a timeout. A live parentdu -sktotal only locates the path; it does not restore file-leaf coverage. Continue itemization within the available budget or report the exact unfinished paths. discovertimes out at 120s with zero output — do not treat parent sizes as a completed inventory; use the bounded frontier and preserve every unfinished path.cleanup_tmp.shdefaults to DRY-RUN despite header claiming otherwise — header was fixed in v0.2.0 (commit5341a6f); callers still must pass--clean.- Colima wedges with I/O errors when host disk hits 100% — fstrim can't run; must
colima stop && colima startfirst. 49.6→6.8 GB recovery verified live. - Confirm the runner supervisor —
launchctl list | grep -i runner(macOS) orsystemctl --user list-units | grep -i runner(Linux), plusps aux | grep -i runner, before any VACATE-style directive. - Legacy snapshot path:
backup/<host>/disk_snapshot.jsonis frozen historical data, not a current measurement. Consider it only as an in-window historical sample with a valid embedded capture timestamp and coverage of at least 70%. Current measurement usessnapshots/disk_snapshot.json;diskm state statusidentifies the per-host state repository.
Anti-patterns to flag in code review
- Re-implementing du/dedup in shell when
disk_snapshot.shalready dedups with a realpath containment trie (schema v2). Live measure if a one-off is needed. - Source-level
grepfor keys with substrings (e.g.assert 'measured' not in json.dumps(td)false-fail when the wanted key ismeasured_total_kb). Test contract: assert on dict keys, not substring matching. - Heredoc stdin race:
python3 - <<'PY' … | something— the heredoc wins the fd, so the pipe isn't read. Write to a temp file and pass it as argv. - Symlink dedup at parent (
/var → /private/var) for paths given todu. Add-Palways.
What this skill is NOT
- Not a cleaner (use
disk-magicianfor cleanup; this skill explains why something grew). - Not a real-time monitor (left to
disk-root-cause+pressure_sweep.sh). - Not authorized to push commits or merge PRs (read-only forensics + typed recommendations only).
Exit criteria for the skill
A complete root-cause answer for this machine has:
- [ ] Default
diskm auditthree-lane report completed or its named limits were reported - [ ] Every resolved directory leaf is below 100 MiB; every directory at or above 100 MiB was descended, and every file is individually itemized with hardlinks deduplicated and symlinks not followed
- [ ] Every traversal failure is named as unfinished and excluded from complete leaf totals, floors, age decisions, and reclaim estimates
- [ ] Displayed Data equation reconciles measured file leaves + measured directories below 100 MiB + measured tail + purgeable estimate + unresolved residual = Data used, with APFS siblings reported separately
- [ ] Data-used before/after bracket is shown; any drift is marked non-atomic with a residual interval
- [ ] Residual is explicitly not called backup or reclaimable without evidence
- [ ] Phase 0 ground-truth probe done
- [ ] Phase 1 floor deltas computed (last week + last month + all-time min)
- [ ] Phase 2 attribution buckets are non-overlapping sums of measured leaves; directory aggregates remain navigation hints
- [ ] Phase 3 readout in the format above, with explicit limits listed
- [ ] No destructive command proposed without an explicit human-OK gate
- [ ] Honest attribution: where any bucket delta is sourced from a concurrent agent or external sweep, say so
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.