9 min read
AI assisted

Disk Utilities Through Two Surfaces — zsh Aliases and a Claude Skill

The same disk-management logic exposed as zsh aliases (dh/dl/dol) and as a Claude Skill, with stale-while-revalidate caching in the shell

A small set of disk utilities exposed through two interfaces at once — zsh aliases (dh, dl, dol, ...) for human use, and a Claude Skill for agent use. The same shell functions answer both surfaces.


Two interfaces, one logic

The canonical version of the disk management code lives in ~/.oh-my-zsh/custom/disk_utils.zsh. It defines about a dozen functions — disk_home, disk_github, disk_ollama, disk_ml_cache, and so on — and then binds short aliases to each one: dh, dgh, dol, dml. That is the human surface.

The second surface is ~/.claude/skills/disk-manager/SKILL.md. It registers the same alias vocabulary with Claude Code so the agent can invoke dh or ddiff in response to natural-language requests like "show me what is taking up space in my GitHub folder" or "how much disk did I free this week." The agent does not reimplement the logic — it calls the same shell commands the human would call.

The split exists because the two callers have different needs. A human running dh from the terminal wants output immediately, can tolerate a one-line stale notice, and will press dref if the cache looks wrong. An agent calling ddiff inside a longer troubleshooting session needs the same data but triggers it through text — there is no shell prompt to read, no menu to navigate. The Skill layer translates natural language into the right alias and handles the output interpretation. The zsh layer stays unchanged.


The zsh side

disk_utils.zsh organizes the aliases into three groups: read-only queries, history commands, and cleanup commands.

Read-only queries:

disks    # full interactive dashboard (numeric menu)
dh       # home directory top 15 items
dgh      # GitHub projects by size
dicl     # iCloud Drive
dcol     # Colima VM — Docker images with project references
dol      # Ollama installed models
das      # Library > Application Support
dlc      # Library > Caches
dct      # ~/.cache (CLI/dev tools)
ddt      # dev tools: npm / Bun / nvm / pyenv / Cursor / Gradle / GHCup
dml      # ML caches: HuggingFace / uv / PyTorch / Keras / TensorFlow / pip

History commands (SQLite-backed):

dsnap    # take an immediate snapshot (launchd runs this every 6 hours automatically)
ddiff    # usage delta vs. N hours ago (default 24h — ddiff 48 works too)
dhist    # total usage trend chart; dhist colima 30 for a specific category

dhist categories: colima, ollama, uv_cache, hf_cache, github, icloud, lib_caches, lib_appsupport.

Cleanup commands:

dcache   # prompt-confirmed wipe of ~/.cache/*
dnpm     # prompt-confirmed npm cache clean --force
dref     # invalidate disk_cache files and re-run disk_home

The interactive disks dashboard is useful for an overview session. The individual aliases are faster for one-off lookups or for scripting. The history commands (ddiff, dhist) require a snapshot record in ~/.disk_cache/disk_history.db — run dsnap at least once, or let launchd accumulate a few runs first.

The cache directory is ~/.disk_cache/. Each cached query writes to a named file there (home_usage, github_usage, icloud_usage, and so on). TTL is 21600 seconds — six hours. The _cache_valid function compares the file's mtime against the current epoch:

_cache_valid() {
  local cache_file="$1"
  [[ ! -f "$cache_file" ]] && return 1
  local age=$(($(date +%s) - $(stat -f %m "$cache_file" 2>/dev/null || echo 0)))
  [[ $age -lt $CACHE_TTL ]]
}

The Claude Skill side

The Skill is registered at ~/.claude/skills/disk-manager/SKILL.md. Its description field (the text Claude Code matches against incoming requests) reads:

"디스크 사용량을 모니터링하고 정리합니다. 대용량 파일 찾기, 폴더별 크기 분석, 캐시 정리, ML 프레임워크 모니터링, Docker 이미지 분석 등에 사용합니다. dust/fd 기반 고속 스캔, stale-while-revalidate 캐싱, launchd 주기적 인덱싱."

When the agent matches a request against that description, it has the full alias list and the disk-utils.sh script available. It does not need to reconstruct what dml does — the Skill tells it: dml = ML framework caches (HuggingFace / uv / PyTorch). The agent runs the alias in a Bash tool call and reads the output.

Beyond the simple alias calls, the Skill includes a structured investigation protocol — the order in which to narrow down a reported disk spike. It starts with df -h | grep /System/Volumes/Data, drills into dust -d 1 -n 15 ~/Library, then walks through Library/Containers for Docker Desktop or UTM disks. The agent follows that sequence rather than jumping to cleanup commands prematurely.

The Skill also carries caveats the agent needs to surface: docker images's CREATED field reflects build time, not pull time; Docker image deletion on macOS requires colima ssh -- sudo fstrim -av before the filesystem reclaims space; dref shows stale data during the TTL window and should not be trusted for post-cleanup verification. These are things the human running dh would know from experience but the agent would not infer from shell output alone.


Shared logic

disk_utils.zsh is the single source of truth. disk-utils.sh — the companion script in the Skill directory — is an earlier version of the same functions, now superseded. The current production file is the zsh one. Both the human alias path and the agent invocation path point to the same zsh source.

The _get_or_update_cache function is where both surfaces converge at runtime:

_get_or_update_cache() {
  local cache_file="$1"
  local cmd="$2"

  if [[ -f "$cache_file" ]]; then
    cat "$cache_file"
    if ! _cache_valid "$cache_file"; then
      (eval "$cmd" > "${cache_file}.tmp" && mv "${cache_file}.tmp" "$cache_file") &!
    fi
  else
    eval "$cmd" | tee "$cache_file"
  fi
}

When a human runs dgh, this function runs. When the agent calls dgh through a Bash tool, this function runs. The cache file is the same file. There is no separate cache for agent calls.

The underlying scan command for most queries is dust — a Rust-based du replacement that is faster on large directory trees and produces sortable, human-readable output. dust -d 1 -n 30 $GITHUB_DIR scans one level deep and returns the top 30 entries. The Colima section calls docker images and docker system df directly when Colima is running, falling back to a grep across docker-compose files when it is not.


Stale-while-revalidate in a shell

The cache pattern used here is stale-while-revalidate: if a cache file exists, return it immediately regardless of age, then check whether it has expired. If it has, start a background refresh.

if [[ -f "$cache_file" ]]; then
  cat "$cache_file"                          # return stale data now
  if ! _cache_valid "$cache_file"; then
    (eval "$cmd" > "${cache_file}.tmp" \
      && mv "${cache_file}.tmp" "$cache_file") &!   # refresh in background
  fi
else
  eval "$cmd" | tee "$cache_file"            # no cache: block and populate
fi

The &! suffix in zsh disowns the background job so it survives the terminal session without being tracked in the jobs table. The tmp-then-mv pattern prevents a reader from seeing a partially written cache file.

This means dh returns in under a second even when the underlying dust scan of the home directory would take several seconds. The tradeoff is that the numbers shown may be up to six hours old. For the common use case — checking whether disk usage is in the expected range — stale data is fine. For post-cleanup verification, dref discards the cache files and forces a synchronous rescan.

The exception is _check_space_change, which runs at the start of disk_home (and thus dh). It calls df /System/Volumes/Data — a cheap syscall, not a directory scan — and compares the result against the last recorded value stored in ~/.disk_cache/.last_used_space. If the delta exceeds 5120 MB (5 GB), it logs a warning, calls _invalidate_cache to delete all the named cache files, and returns a non-zero exit code. On the next call, those files do not exist, so the code falls through to the synchronous scan branch instead of returning stale data.

The six-hour TTL and five-GB threshold are rough calibrations for a machine that is actively in use but not continuously churning disk. The launchd agent (~/Library/LaunchAgents/com.jaesolshin.disk-index.plist) runs a refresh script every six hours regardless of whether any alias has been called, so the cache tends to be warm by the time a query arrives.


Operational notes

Permissions. dust requires read access to the directories it scans. Most home-directory paths work without elevation. ~/Library/Containers and ~/Library/Application Support have some subdirectories that require Full Disk Access granted in System Settings. Commands that hit those paths silently skip inaccessible entries — the size totals will be understated without a visible error.

Cache staleness and agent calls. When the agent calls dh or dml, it receives whatever is in the cache at that moment. If the cache was last updated four hours ago and a large model download happened in the meantime, the agent will see the pre-download numbers. The Skill notes this explicitly: for post-cleanup verification, run dref first or use df directly. The agent knows not to treat dh output as real-time.

Registering the Skill. The Skill lives at ~/.claude/skills/disk-manager/SKILL.md. Claude Code picks it up automatically on next start if the skills directory is on the configured path. No additional registration step is required beyond placing the file.

Concurrent calls from both surfaces. If a human runs dgh in a terminal at the same moment the agent calls dgh in a Bash tool, both reads hit the same cache file. Both will get the cached output. If the cache is stale, both will attempt a background refresh — two concurrent dust processes scanning the same directory, writing to github_usage.tmp, and racing to mv the result. In practice this is harmless: both processes produce the same output, and whichever mv completes second overwrites the first with an identical file. The scan output is deterministic for a given directory state. The risk would be a half-written .tmp file if both processes were interrupted simultaneously, but the tmp-then-mv pattern makes that non-destructive — the next call would miss the cache file and do a fresh blocking scan.

SQLite history. ddiff and dhist query ~/.disk_cache/disk_history.db. The database retains 90 days of snapshots. The schema has two tables: snapshots (category, size_mb, captured_at) and disk_free (total_mb, free_mb, captured_at). ddiff compares the latest row against the row closest to N hours ago; dhist draws an ASCII bar chart from daily-latest values using a Python inline script. Both commands check for the database file first and print a prompt to run dsnap if it does not exist yet.