disk-space-saviour
v0.25.0
Published
Safely reclaims disk space: caches and build outputs of 20+ ecosystems, forgotten projects, and Docker (running, stopped and nested containers), with an emergency mode
Maintainers
Readme
disk-space-saviour
dss finds and safely deletes regenerable disk usage: package manager
caches, build outputs, forgotten dependency folders, old toolchain versions,
agent and browser caches, system logs, and Docker data. That includes caches
inside running containers and in nested Docker-in-Docker daemons. It is a
command line tool and a JavaScript library (scan() → report,
clean(report) → audit log).
Why
Build machines and AI agent hosts fill their disks with data that can be
rebuilt: node_modules, target/, ~/.cache, Docker layers and caches left
inside long-lived containers. Deleting this data by hand is risky, because
the same folder may belong to a build that is running right now or to a
container holding unpushed work. dss only touches regenerable data. It
checks liveness and Git state again right before each deletion, and it never
deletes anything unless you ask it to.
Quick Start
npm install -g disk-space-saviour # or: npx disk-space-saviour scan
dss scan # report only, never deletes
dss clean # plan for the safe tier (dry run)
dss clean --yes # delete the safe tier
dss clean --tier moderate --yes
dss emergency --free 20G --yes # escalate tiers until 20 GiB is free
dss docker scan --recursive # containers and nested daemonsExample (dss scan ~/work on two projects):
disk-space-saviour scan of host (build-01) in 3.7s (report only, nothing was deleted)
/: 75.8 GiB free of 193 GiB (61% used)
SAFE 2 items, 7.6 MiB
4.8 MiB cargo-target /home/me/work/api/target
2.9 MiB node-modules /home/me/work/web/node_modules
Reclaimable (cumulative): safe 7.6 MiB · moderate 7.6 MiB · aggressive 7.6 MiB; blocked 0 B
Audit log: /home/me/.local/state/disk-space-saviour/audit/dss-scan-2026-09-26T00-31-05-432Z-232874.json
Run `dss clean --tier safe` to see the plan, add --yes to delete.dss clean without --yes asks on a terminal. When nobody can answer
(CI, agents, pipes), it prints the plan, deletes nothing, and says
Dry run: pass --yes to delete non-interactively. Add --json to any
command to get the report or audit log as JSON (dss scan --json).
Commands
dss scan [paths...] [--docker] [--depth N] [--json] # report only
dss clean [paths...] --tier safe|moderate [--yes] [--older-than 1h]
dss emergency --free 20G | --until 80% [--path /] [--yes]
dss docker scan|clean [--container ID] [--recursive]
dss usage [paths...] [--min-size 1G] [--json] # where space goes| Exit code | Meaning |
| --------- | ---------------------------------------- |
| 0 | success (a dry run counts as success) |
| 1 | error, or at least one deletion failed |
| 2 | usage error |
| 3 | dss emergency could not reach its goal |
Run dss --help for every option. The most useful ones:
--only NAME|ID|PATHlimits a run to an ecosystem, rule or kind (--only rust,--only npm-cache,--only docker-stopped-container), a single item by its JSONid, or the items inside a path or matching a glob (--only ~/Library/Caches/Codex,--only '**/Crashpad/*'). A path never widens to the item holding it. When every entry names a cache or toolchain version rule or one of its items, only those rules are scanned, sodss clean --only openai-app-caches --yestakes seconds.--exclude PATH|GLOBprunes matching paths and descendants before reading them. Patterns containing separators match full paths and their ancestors; other patterns also match ancestor basenames.--scanner NAMErestricts scanning toprojects,global,versions,agentsorsystem.--report FILElimits cleaning to items in a saveddss scan --jsonreport.--from FILEalso accepts an audit JSON containing the full report. Recent reports authenticated by this installation are reused; older, unsigned or edited reports require discovery again. Changing discovery or action options also rebuilds the plan. Every deletion still gets fresh checks.dss usageshows where the space of a whole volume goes, including data no rule covers. One read-onlydu -axkpass walks the macOS Data volume (/Users,/Applications,/private,/opt, ...) or/on Linux, or the given paths, staying on their filesystem. It prints a tree of the largest directories (1% of the walk and up, or--min-size), the largest files, and for each directory what the latest dss scan or clean found inside it ([dss: 1.2 GiB freeable, 3.4 GiB blocked]). On macOS it adds the APFS volumes a walk cannot see (system, VM swap, Preboot, Recovery), local snapshots and the part of the Data volume left unread. A directory filled by many small entries names what fills it by pattern (T/ms-* (68523 entries, each under 64 MiB)). It needsdu, so it runs on macOS and Linux; a full walk takes minutes.- APFS clones that share every block with an installed app, such as Chrome's
code-sign clones, free nothing when deleted, so they are never offered. The
scan lists them under
SHARED WITH INSTALLED APPSanddss usagemarks them, becauseduand Finder count them at full size. node_modulescounts as regenerable only while it holds what package managers install: packages,@scopefolders,.bin, lockfile copies and tool caches such as.cacheor.vite. Any other data kept there (verification runs, captured evidence) blocks the whole directory, and the check runs again right before deleting.- Git worktree folders (
.worktrees,worktrees) restart the project search depth, so dependencies of nested worktrees are found below--max-depth. cleanautomatically reuses a matching scan from the last 30 minutes.--freshbypasses this plan and the discovery/size index;--cache-dir DIRchooses the state location (default$XDG_STATE_HOME/disk-space-saviour, or~/.local/state/disk-space-saviour).
Tiers
Tiers are cumulative: moderate includes safe. Regenerable data is never
ranked by how recently it was used: a cache, build output or dependency
directory is safe and is held back only while something uses it (see
Safety).
| Tier | What it removes |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| safe | Everything regenerable: download, runtime and application caches, idle package-runner entries, old browser revisions, node_modules, vendor, target/, .venv, build/ and similar folders of any project, dangling Docker images and build cache. |
| moderate | Data that is disposable but not regenerated by a tool: old agent scratch and Telegram downloaded media (secret-chat media may be irreplaceable), unused toolchain versions no process runs from (the item names the global packages it removes; never one holding virtual environments), tagged images and stopped containers (both require explicit consent). |
| aggressive | Accepted by dss emergency for compatibility; currently the same as moderate. |
Covered ecosystems: JavaScript/TypeScript (npm, yarn, pnpm, bun, deno,
framework build caches), Python, Rust, JVM (Gradle, Maven, Kotlin, Android),
Go, C/C++ (CMake, ccache, vcpkg, Conan), .NET, PHP, Ruby, Swift/Xcode, Dart,
Haskell, Scala, Elixir, OCaml, Lean, Julia, R, Zig, and scripting languages.
Also covered: version managers (nvm, pyenv, rbenv, SDKMAN!, rustup, elan,
ghcup, opam, swiftly, VS Code Server, Claude Code, the Codex daemon and Copilot CLI), Playwright, playwright-go, Puppeteer
and Cypress browsers, HTTP caches of Chrome, Chromium, Edge, Brave, Opera,
Opera GX, Vivaldi, Arc, Yandex Browser, Naver Whale, 360, QQ Browser, Firefox,
LibreWolf, Waterfox, Zen and Floorp, IDE caches (JetBrains, VS Code, Cursor,
Windsurf, Qoder), AI agent caches and logs, the Discord cache, macOS app
updates staged by Sparkle and Squirrel, package manager archives, trash, crash
reports, core dumps and journald logs. Application downloads are covered too:
Chrome bundle clones left by exited browsers (kept while any browser runs the
clone's hard-linked executable, and counted as free when they still share
their blocks with the installed app), Chromium on-device AI models
(downloaded again when a page uses built-in AI), Google Updater installers, ChatGPT Atlas and Codex app caches,
Electron and electron-builder archives, the Bun transpiler cache, Docker
Desktop and Warp staged updates (kept while the app runs), Zed language
servers, tree-sitter toolchains, Codex plugin staging, the Clang module cache
Crashpad reports (processed ones, and unsent ones after 30 days), rotated app
logs in ~/Library/Logs (warp.log.old.2) and the GitHub CLI run-log cache.
Telegram pure caches, Safari/WebKit,
Chromium code/GPU/Dawn/service-worker caches and marker-validated Electron
caches are included. Old Playwright browser revisions are kept while a
Playwright package recorded in the registry's .links still launches them or
a browser runs from them; Playwright installs and downloads left in temporary
directories are removed once untouched. Telegram caches are removed only
while Telegram is not running. Telegram databases, browser profiles and
Playwright MCP profiles are preserved. macOS access failures explain when Full Disk Access
is needed. Agent scratch cleanup requires inactivity and Git safety checks.
Anything inside a Claude Code session scratchpad
({TMP}/claude-<uid>/<project>/<session>) is kept until the session and its
transcript in ~/.claude/projects stay untouched for the inactive window
(30 days), checked again right before deletion.
Safety
- Dry run by default.
scannever deletes.cleanandemergencydelete only with--yesor an interactive yes, and never with--dry-run. - Regenerable data only. Every rule describes data that a tool recreates: a cache, a build output or a dependency install.
- Liveness checks, repeated right before each path is deleted. Paths
that a process holds open (
/proc/*/fd,cwd,exe, orlsofon macOS) are skipped. So are projects with a runningcargo,gradle,npm,nodeor similar tool, and anything modified within--older-than(1h). How long ago a project was last used does not matter. A tool running elsewhere does not block a runtime cache it is not using: a script of an installed project keeps no lock on the Bun install cache, whilebun install,bunxor an auto-installing script does. A directory on thePATHof a running process is kept too: the process may still start programs from it. - Per-app macOS folders stay while their app runs. Caches, containers
and other folders under
~/Library/<kind>/<bundle-id>belong to the app with that bundle identifier, read from theInfo.plistof every bundle a running process starts from. Such a folder is blocked while that app, a helper or an updater of it runs (for example the WebKit cache of Music, Telegram's group container), even when no rule names the app's process. A path held open names its holder:in use: <path> (pid N, App). - Blocked space is grouped by what frees it. A
WARNING:line and theFREEABLE ON CONDITIONSsection total blocked space by the condition that clears every blocker of an item: quit an app, a process or Claude Code session ending, files closing, the activity window passing, a container stopping or a later scan finishing what this one ran out of time for. Items kept for Git state, protected installs or unverifiable records are counted apart.cleanrepeats the summary after the plan. Rerun dss once a condition is met; it rechecks everything before deleting.cleanscans again instead of reusing a saved scan when a process named by its blockers has exited, and warns when the scan behind its plan stopped early (for example at--scan-budget), since items it never reached are not planned. - Toolchain versions are kept while anything may need them. The
manager's default and aliases, versions pinned by a project (
.nvmrc,rust-toolchain.toml, ...) and the newest install stay. A partial nvm pin such as20keeps only the newest matching 20.x, as nvm resolves it. A version holding globally installed npm packages or pyenv virtual environments is blocked; bundlednpm/corepackand use-m caches (<name>-v-<version>) are reinstalled on demand and do not count. A Claude Code version stays while a running session uses it: its executable is open, or its~/.local/state/claude/locks/<version>.locknames a live process or cannot be read. All of this is checked again right before each version is deleted. Container paths are checked individually before each bounded deletion batch. - Rust builds are pruned in two parts.
cargo-supersededremoves older hashes of a unit (.rlib,.rmeta,.so/.dylib/.a,build/*,.fingerprint) that a newer build replaced, but never whilecargoorrustcruns in the project: a running build may have judged them fresh.cargo-superseded-leafremoves incremental session directories idle for 3 hours and test/example binaries older than 3 hours that a newer build replaced. Nothing links against them, so this part stays eligible while cargo runs; each file a process runs or holds open is skipped. - Running tasks must survive. Before its first deletion in an
environment (the host or a container),
cleanrecords PID 1, agents (claude,codex,solve, ...) and build tools (cargo,gradle, ...) with their start times. It checks them again after every removed path and item. If one is gone or its PID was reused, cleaning in that environment stops at once, and the audit log and summary say which process was lost. A build that simply finishes during the cleanup also stops it: an exit cannot be told apart from a crash. Host cleanup watches processes in its own mount namespace; each container watches its own tasks. Processes with unknown namespace membership remain watched. - Git awareness. Whole projects and containers are protected while they
have unsaved changes, unpushed commits or stashes. Ignored dependency and
build outputs can be cleaned despite unrelated source changes; tracked or
non-ignored files inside an artifact remain protected. Stopped-container scans show
Git state before cleanup. Unstaged and untracked files can be recognized
as preserved when their bytes and modes all match one cached remote ref,
such as
origin/recovery/<branch>. No remote fetch is performed; missing refs, staged work and unreadable files remain blockers. Detached upstream checkouts are accepted only when their commit is reachable from a remote ref. Nested Docker layer copies and temporary Codex plugin checkouts are excluded from this inspection. Unsaved container work requires an exact--allow-dirty-container IDoverride. - Audit log on every run. Each run, dry runs included, writes
dss-<command>-<time>-<pid>.jsonwith every decision, the bytes freed and left per environment, and disk usage before and after. Scan audits include every candidate and its blockers; blocked candidates do not contribute to reclaimable tier totals. If a read-only scan cannot save its audit, it warns and still prints the report. When the disk is full,cleanandemergencydelete anyway and write their audit as soon as deletions free room; if it never fits, the summary saysAudit log: not written (ENOSPC ...). Other audit failures, such as an unusable--audit-dir, still stop them before deleting. Root scans with another user'sHOMEuse a private temporary audit directory. Set--audit-dirto choose a persistent writable location.
Long-lived npm exec/npx servers allow idle npm download and node-gyp
header caches to be cleaned. Active npx hashes remain protected through
their child command paths, working directories and open files; other
hashes are separate candidates subject to the age window. Install commands
still block relevant caches. Bunx, pnpm dlx and Yarn dlx entries use the same
path protection, and dss always protects its own installation. Interrupts
finish the partial audit and report the abort. Unreadable processes retain
their readable identity; uncertain activity blocks relevant paths and owners
and produces one environment hint. Old isolation logs and sanitized upload staging
are moderate-tier candidates. Opam download cleanup removes its cache
directory directly and does not invoke opam clean or migrate the root.
Scans have a ten-minute budget per filesystem environment (two minutes for
dss emergency, which must act quickly on a full disk), configurable with
--scan-budget 30m (library: scanBudget). Progress goes to stderr, leaving
JSON stdout clean. SIGINT and SIGTERM stop new work, cancel subprocesses and
print completed findings marked aborted; incomplete findings remain blocked.
The CLI exits 130 or 143 respectively. A second signal exits immediately.
Library callers can pass an AbortSignal and an onScanProgress callback.
Global caches run first, prioritizing rules that can use native sizing without
recursive activity timestamps, and emit findings as each rule finishes. Non-interactive
clean plans or deletes completed safe host caches while the remaining discovery
continues. emergency does the same, checks actual free space after each deletion,
and skips the remaining scan once the goal is met. Interactive runs show a plan
before asking for deletion consent. A later phase hitting its budget blocks its
partial findings while completed independent findings stay usable.
Project discovery uses eight directory workers, prunes matched generated trees,
Git objects, home media/Photos libraries, macOS Library and mounted volumes.
Source activity is checked separately without sizing entire project trees again.
Spotlight (mdfind) or plocate/locate supplies verified early hints when
available; the walk also runs because an index can miss files. Version-pin discovery
also prunes dependency/build trees.
The persistent index/ stores directory listings and candidate measurements.
Each descendant directory's own mtime/ctime and identity is checked before reusing
its listing, so a nested addition is visible even when an ancestor did not change.
Unchanged directories avoid another listing; discovery still checks their metadata.
Sizing uses bounded workers and du -skx when no recursive activity timestamp is
needed. Sizes cached for up to an hour are labeled estimated, because changing
an existing descendant need not update the candidate's directory mtime. Cleanup
always bypasses that cache and rechecks processes, open files, Git, path boundaries
and sizes. Safe cleanup also rechecks source activity. Explicit moderate cleanup
preserves the ability to remove ignored dependencies when only source changed.
Library callers opt into state with cacheDir.
On macOS, reports include sysctl vm.swapusage and the largest memory consumers
from top (memory includes compressed and swapped pages). These are memory
diagnostics, not verified per-process swap ownership. On Linux, reports include
/proc/meminfo swap and the processes with the most swapped memory (VmSwap).
Each scan records swap usage and its largest holders in the state directory, so
the next scan shows how much both grew during the last week. A WARNING: line
appears when swap grew by 1 GiB or more, is at least 75% full, or exceeds the free
disk space it can grow into, naming the processes holding the memory and their
growth. The dss run itself is listed as (this dss run) and never named as a
process to quit. Long-running AI agent sessions keep their history in memory: quitting and
resuming them (codex resume <session-id>, claude --resume) releases it. When
Docker Desktop consumes memory, saving work and restarting it or lowering its VM
memory limit can help. dss never deletes swap files.
Git clones in temporary directories ({TMP}/* and {TMP}/*/*) are safe to remove
once untouched for the activity window when every worktree sharing the repository
is clean, none lives outside the clone, nothing is stashed, and every commit
reachable from HEAD, a branch or a worktree is advertised by an https or ssh
remote. That last check runs git ls-remote against the remote rather than
trusting remote-tracking refs, outside any repository, with only the https and
ssh transports and no prompts. Clones of local paths and clones whose .git is a
symlink are left alone, and cleanup repeats every check right before deleting.
Cache discovery honors tool configuration before default paths, including
GOMODCACHE, GOPATH, GOCACHE, CARGO_HOME, RUSTUP_HOME,
GRADLE_USER_HOME, DENO_DIR, BUN_INSTALL_CACHE_DIR and XDG_CACHE_HOME.
Go, npm, pip, Yarn and pnpm cache queries are bounded and cached per environment.
Safe rules also cover perlbrew build/download trees, SDKMAN tmp/archive contents,
nvm downloads, idle target/semver-checks, and downloaded Ruby .gem files.
SDKMAN parent directories and installed gems are preserved.
Docker
With --docker, or automatically when a daemon is reachable, dss handles
three kinds of Docker data:
- Daemon data:
docker system df, dangling images and build cache.--include-volumeslists unattached volumes for manual inspection; it does not delete them. - Unused tagged images are listed with their size, creation date and
the container that used them last (from the
docker inspectbackups of containers dss removed), when known. No tier removes them, emergency mode included: approve each one with--remove-image REF(repo:tag,repofor:latest, or an id prefix of at least 12 characters) or an interactive yes for that image. The summary line counts them, and stopped containers that need consent, asneeds explicit consent, so the tier totals match whatclean --tier … --yesremoves. - Running containers are never stopped, restarted or removed. They are
scanned and cleaned from the inside through
docker exec, with the same rules and liveness checks as the host. An unreadable/procprobe is retried with a read-only rootdocker exec --privilegedinspection. If this is denied or incomplete, cleanup stays blocked and names the unreadable processes. This also applies when dss runs inside a container with access to its Docker daemon. Root probes also retry unreadable processes as their owning UID/GID withsetprivwhen available. Readable evidence is retained after failed retries; unresolved inspection produces one environment hint. Blocked totals show measured bytes and separately identify visible bytes whose freeable size is unknown. - Stopped containers are listed with their size, image, command, owner
session (
HIVE_MIND_PARENT_SESSION_IDand other session labels or environment variables, else a session id in the container name), task URL, exit code and reason, OOM flag and end time, and the Git state of repositories in their writable layer. The layer is copied out withdocker cpand checked with Git. A container is removed only with--remove-stopped-containersor after an interactive yes per container, and only when its Git state is clean. Beforedocker rm, its logs anddocker inspectoutput are saved to--backup-dir. - Containers kept for investigation (a non-zero exit code or OOM
killed) are never removed by a tier or by
--remove-stopped-containers, in emergency mode too. Name each one with--remove-container ID|NAME(a full name, or an id prefix of at least 12 characters) to remove it. Alternatively,--investigation-max-age 7dlifts holds for containers that ended at least seven days ago; removal still requires consent and Git checks. The default keeps investigation holds indefinitely.
Container cache sizes count changed writable-layer file blocks and mounted
data. Immutable image data is reported separately as imageBytes and is
excluded from reclaimable totals, including the host scan when dss runs
inside an identifiable container. Image-only caches stay visible and blocked;
rebuild the image to remove that data. These estimates exclude directory
metadata overhead. A small stopped container pinning a large image is shown
even below --min-size; its potential image bytes require a separate image
removal after the container is removed.
--no-docker makes no Docker calls, including for storage accounting and
saved-report cleanup. An overlay root without authoritative upperdir evidence
has unknown freeable bytes and contributes zero to reclaimable totals.
Measurements prefer accessible upperdirs; the diff fallback shares one
serialized snapshot and cached stats per container/run, checks host and cgroup
memory headroom, and skips excessive candidate counts. Fresh Git checks before
stopped-container removal still inspect new work. Verbose mode reports storage
measurement progress. Host scans prune runtime, nested overlay, read-only and
unselected mount points; explicitly selected ordinary mount roots are allowed.
For containerd-backed running containers, the daemon-side init process's
/proc/<pid>/mountinfo supplies the overlay upperdir when GraphDriver is absent.
JSON environments and the text summary show the storage source. Paths are
measured on the daemon host and removed inside the container so overlay
whiteouts are respected; cleanup reports the measured change in allocated
upperdir blocks when available. Remote filesystem operations use bounded
POSIX-shell batches and do not require Node inside the target container.
Docker discovery uses a size-free container listing and measures each
container independently. Failed size queries are retried once; objects
remain visible with sizeUnknown when measurement fails. Scans continue
through other objects and nested daemons, report partial errors, and return
a nonzero CLI exit status when discovery is incomplete.
--recursive follows Docker-in-Docker: a container that runs its own daemon
is scanned as a new environment, down to --depth levels (default 3). The
CI job Docker-in-Docker Integration builds a host → l1 → l2 chain and runs
tests/integration/dind.mjs against it (npm run test:dind).
Scan roots
Project discovery walks a list of roots per environment; the traversal policy (which mount points are crossed) uses the same requested roots, so both always agree:
- host: the positional
paths(rootsin the API), otherwise the homes and temporary directories; - every scanned container, at every Docker depth:
--container-root PATH(containerRoots, repeatable) replaces the defaults; without it, the container's homes, temporary directories, existing common workspace paths (/workspace,/workspaces,/app,/src,/code,/project,/usr/src/app) and every host root that also exists inside the container. Host and container paths are different namespaces, so no path is remapped.
Each JSON environment carries scope: its roots with source (roots,
container-roots, home, tmp, container-default) and status
(searched, partial, absent, inaccessible, pruned, excluded,
not-searched), plus maxDepth, exclude and prunedMounts. The text
report names requested roots that were not searched. scanStatus: complete
means complete within that scope; exclusions, mount boundaries and liveness
checks still apply.
Hard links
Sizes are physical reclaimable bytes. A file's blocks count toward an item
only when every hard link to it lies inside the deletion set: a package
cache or node_modules hard-linked to a shared store (pnpm, Nix, uv) reports
those blocks as sharedBytes, next to allocatedBytes (what du prints).
Each dev:inode is counted once across the whole selection, so files linked
between two selected paths are reclaimable once. In the audit log,
sharedBytes lists blocks that stayed allocated; freedBytes is a measured
before/after value for Docker writable layers (counting only blocks whose
every hard link is inside the deletion set, marked
freedAccounting: 'hardlink-aware-measurement') and otherwise the link-aware
estimate, marked with freedEstimated and freedAccounting.
Emergency mode
dss emergency --free 20G (at least 20 GiB available) or
dss emergency --until 80% (at most 80% used) works through the tiers in
order. Within a tier it removes caches first, then Docker objects, and
stopped containers last, only when approved. It re-reads the disk after
every deletion and stops as soon as the goal is met. It exits with 3 when
even the aggressive tier cannot reach the goal. --path chooses the volume
(default /); items on other host volumes are skipped.
Library
import { scan, clean, emergency, formatReport } from 'disk-space-saviour';
const report = await scan({ roots: ['/work'], docker: false });
console.log(formatReport(report));
// Library calls delete unless dryRun is set.
const audit = await clean(report, { tier: 'safe', dryRun: true });
console.log(audit.plannedBytes, audit.file);
await emergency({ free: '20G', removeStoppedContainers: false });Types ship in src/index.d.ts. See examples/basic-usage.js for a runnable
example and examples/universal-app for a report viewer.
Configuration
| Option / variable | Default | Purpose |
| -------------------------------- | ------------------------------------------ | ------------------------------------------- |
| --audit-dir, DSS_AUDIT_DIR | $XDG_STATE_HOME/disk-space-saviour/audit | Where audit logs go |
| --backup-dir, DSS_BACKUP_DIR | …/disk-space-saviour/backups | Container log backups before docker rm |
| --older-than | 1h | Activity window; newer files are kept |
| --inactive | 30d | Idle time before agent scratch is abandoned |
| --min-size | 1M | Smaller items are not reported |
| --depth | 3 | Docker nesting depth |
| --journal-keep | 512M | journald size to keep |
| --verbose, DSS_DEBUG=1 | off | List everything, trace commands and timings |
$XDG_STATE_HOME falls back to ~/.local/state. Linux and macOS are
supported (liveness uses /proc on Linux and lsof on macOS).
Repository settings
Protected-Branch Release Pull Requests
If main requires pull requests and the Pipeline Status check, configure a
repository secret named RELEASE_PR_TOKEN. It must be a fine-grained PAT for
an automation actor other than the workflow's built-in GITHUB_TOKEN, scoped
to this repository with Contents and Pull requests write access and Checks read
access. The release and generated-preview fallbacks use it to open the PR, wait
for the PR's own checks, and merge only after they pass. The manual
changeset-PR mode uses it for the same reason. Teams that generate short-lived
GitHub App installation tokens can wire that action output to the same workflow
inputs instead of storing a PAT.
Repositories that allow the release workflow to push directly to main do not
exercise the fallback, but manual changeset PR creation still requires this
secret.
Optional Docker Hub Publishing
Docker publishing is disabled by default. To enable it for a project that ships
a Docker image, add a Dockerfile and configure these GitHub Actions settings:
| Setting | Type | Description |
| -------------------- | ------------------ | ------------------------------------------------------------------------------------- |
| DOCKERHUB_IMAGE | Variable | Docker Hub image name, for example namespace/image. This enables Docker publishing. |
| DOCKERHUB_USERNAME | Variable | Docker Hub username used by docker/login-action. |
| DOCKERHUB_TOKEN | Secret | Docker Hub access token used for registry authentication. |
| DOCKER_CONTEXT | Variable, optional | Docker build context. Defaults to .. |
| DOCKERFILE | Variable, optional | Dockerfile path. Defaults to ./Dockerfile. |
When enabled, the release workflow waits until the exact published npm version
is visible in the npm registry, then publishes Docker Hub tags for latest and
that same version. The Docker build also receives NPM_PACKAGE_VERSION as a
build argument so Dockerfiles can install the matching published package.
ESLint Rules
Customize ESLint in eslint.config.js. Current configuration:
- ES Modules support
- Prettier integration
- No console restrictions (common in CLI tools)
- Strict equality enforcement
- Async/await best practices
- Strict unused variables rule: No exceptions - all unused variables, arguments, and caught errors must be removed (no
_prefix exceptions)
Prettier Options
Configured in .prettierrc:
- Single quotes
- Semicolons
- 2-space indentation
- 80-character line width
- ES5 trailing commas
- LF line endings
Development
npm install
npm test # Node.js, 30s per test
bun test --timeout 30000 # Bun
deno test -A --parallel # Deno (fixtures need -A)
npm run check # lint + format + duplication
npm run test:dind # Docker-in-Docker integration (needs --privileged)
node bin/dss.js scan --verboseProject Structure
.
├── .changeset/ # Changeset configuration
├── .github/workflows/ # GitHub Actions CI/CD
├── .husky/ # Git hooks (pre-commit)
├── examples/ # Usage examples
│ └── universal-app/ # React + GitHub Pages + Electron + Capacitor app
├── scripts/ # Build and release scripts
├── src/ # Source code
│ ├── index.js # Main entry point
│ └── index.d.ts # TypeScript definitions
├── tests/ # Test files
├── .eslintrc.js # ESLint configuration
├── .prettierrc # Prettier configuration
├── bunfig.toml # Bun configuration
├── deno.json # Deno configuration
└── package.json # Node.js package manifestMulti-Runtime Support
This template is designed to work seamlessly with all major JavaScript runtimes:
- Bun: Primary runtime with highest performance, uses native test support (
bun test) - Node.js: Alternative runtime, uses built-in test runner (
node --test) - Deno: Secure runtime with built-in TypeScript support (
deno test)
The test-anywhere framework provides a unified testing API that works identically across all runtimes.
Package Manager Agnostic
While package.json is the source of truth for dependencies, the template supports:
- bun: Primary choice, uses
bun.lockb - npm: Uses
package-lock.json - yarn: Uses
yarn.lock - pnpm: Uses
pnpm-lock.yaml - deno: Uses
deno.jsonfor configuration
Note: package-lock.json is not committed by default to allow any package manager.
Universal App Example
The template includes examples/universal-app, a Vite React app that imports
add and multiply from src/index.js and renders a visual calculator UI.
The same static build is used by:
- GitHub Pages (
npm run example:web:build) - Electron desktop packaging (
npm run example:desktop:package) - Capacitor Android/iOS sync (
npm run example:mobile:sync)
The example app has its own package.json and lockfile so template users can
opt into the frontend stack without adding React, Electron, or Capacitor to the
library package itself.
See examples/universal-app/README.md for local web, desktop, Android, and iOS testing instructions.
Code Quality
- ESLint: Configured with recommended rules + Prettier integration
- Prettier: Consistent code formatting
- Husky + lint-staged: Pre-commit hooks ensure code quality
- File size limit: Files must stay under 1500 lines for maintainability (enforced via ESLint and CI)
Release Workflow
The release workflow uses Changesets for version management:
- Creating a changeset: Run
bun run changesetto document changes - PR validation: CI checks for valid changeset in each PR
- Automated versioning: Merging to
maintriggers version bump - npm publishing: Automated via OIDC trusted publishing (no tokens needed)
- Optional Docker Hub publishing: When configured, waits for the exact npm version and tags the Docker image with that version
- GitHub releases: Auto-created with formatted release notes
First release of a brand-new package: npm publishes only through trusted publishing, and a trusted publisher can only be attached to a package that already exists (the first publish from CI fails with
E404). Publish the first version once with package-registry-manager:npx package-registry-manager setup --registry npm --execute. It signs in through the browser, publishes, attaches this workflow as the trusted publisher and signs out again. No npm token is ever stored.
Manual Releases
Two manual release modes are available via GitHub Actions:
- Instant release: Immediately bump version and publish
- Changeset PR: Create a PR with changeset for review
CI/CD Pipeline
The GitHub Actions workflow (.github/workflows/release.yml) implements a fast-fail pipeline:
Fast checks (~7-30s each, run first for fastest feedback):
- Test compilation: Syntax-checks all
.mjsfiles withnode --check - Lint, format & secrets scan: ESLint, Prettier, jscpd, and secretlint for credential leak detection
- File line limits: Enforces the 1500-line limit on JavaScript (
.js,.mjs,.cjs) and Markdown (.md) files plusrelease.yml - Changeset check: Validates PR has exactly one changeset (added by that PR)
- Version check: Blocks manual version changes in
package.json - Documentation validation: Checks required doc files (doc line limits are enforced by the file line limits check)
Slow checks (only run after all fast checks pass):
- Test matrix: 3 runtimes × 3 OS = 9 test combinations
- Broken link checks: Validates all links in Markdown/HTML files (separate workflow)
Release (on merge to main):
- Changeset merge: Combines multiple pending changesets at release time
- Release: Automated versioning and npm publishing
- Optional Docker publish: Publishes Docker Hub
latestand npm-version tags after the npm package is visible
Reasonable Timeouts
Every CI job declares an explicit timeout-minutes so hung steps fail
in minutes instead of reaching the GitHub Actions default of six hours.
Fast checks use 5-10 minute caps, release jobs use 50 minutes, and the
link checker uses 10 minutes for external network variance.
That cap is a backstop, never the deadline: GitHub reports a job it
kills as cancelled, not failed. Long steps therefore own an
explicit budget via scripts/run-with-budget-warning.sh, which warns at
70% of the budget and fails the step with exit code 124 when it expires.
See CI-TIMEOUT-BUDGETS.md.
Individual tests are also capped inside supported runners:
npm test runs node --test --test-timeout=30000, and the CI Bun
runner uses bun test --timeout 30000. Both bound a single test, not
the suite, which is why the suite budget above exists. Deno does not
provide a single global per-test timeout flag, so Deno tests are
protected by their step budget and the matrix job backstop.
See BEST-PRACTICES.md for detailed explanations of each practice.
Robust Changeset Handling
The CI/CD pipeline is designed to handle concurrent PRs gracefully:
PR Validation: Only validates changesets added by the current PR, not pre-existing ones from other merged PRs. This prevents false failures when multiple PRs merge before a release cycle completes.
Release-time Merging: If multiple changesets exist when releasing, they are automatically merged into a single changeset with:
- The highest version bump type (major > minor > patch)
- All descriptions preserved in chronological order
This design decouples PR validation from the need to pull changes from the default branch, reducing conflicts and ensuring that even if CI/CD fails, all unpublished changesets will still get published when the error is resolved.
Deploying the example app
The example-app.yml workflow deploys the universal example app to GitHub
Pages on every push to main. Before the first run on main in a new
repository created from this template, open Settings → Pages and set
Source = GitHub Actions. This is a one-time manual step and cannot be
configured from a workflow because the Pages source defaults to
Deploy from a branch. Without it, the pages-deploy job fails on
actions/deploy-pages with Get Pages site failed /
Failed to create deployment. After flipping the source, the workflow
provisions the Pages site on its first run.
Auto-regenerated preview screenshots
The same example-app.yml workflow contains a preview-regen job that boots
the built example app in a headless Chromium via
browser-commander +
Playwright and writes fresh screenshots to
docs/screenshots/example-app/example-app-{locale}-{theme}.png on every
push to main (and on workflow_dispatch). Any drift is committed back to
main so README/site images never go stale between releases. Screenshot-only
pushes do not match this workflow's path filter, while a protected-branch
fallback PR remains eligible for its required checks. The job runs in the
official Playwright container with the browser already installed, avoiding CI
stalls from live Chromium downloads.
The same script is available locally:
npm install --prefix examples/universal-app
npm run example:web:preview-images
# Verbose probe of <html data-theme>, <html lang>, and PNG signatures:
PREVIEW_VERBOSE=1 npm run example:web:preview-imagesThe matrix defaults to {en, ru} × {light, dark}. The shipped example app
has no localization or theme toggle yet, so every cell currently renders
the same UI — when a fork adds either, the matrix produces real per-cell
variants without script edits.
Broken Link Checker
The link checker workflow (.github/workflows/links.yml) validates all links in Markdown and HTML files:
- Detection: Uses lychee to walk the checkout for
.mdand.htmlfiles (respecting.gitignore, including dot directories such as.changeset) - Transient re-check: Links that failed with a transport error or an HTTP 429/5xx answer are retried with a capped wait before they count as broken; GitHub file links fall back to the contents API. Set
RECHECK_VERBOSE=trueto log every probe - Web Archive fallback: For any broken links found, automatically checks the Wayback Machine for archived versions
- Actionable suggestions: Reports one of three outcomes for each broken link:
- Archived: Suggests the Web Archive URL as a replacement
- Not archived: Clearly reports the link is unrecoverable
- Scheduled checks: Runs weekly to catch links that break over time (even if no files changed)
- Issue creation: On scheduled runs, creates a GitHub Issue with the full broken links report
Add regex patterns to .lycheeignore to exclude URLs from checks (e.g., local dev URLs, example.com, known rate-limited sites).
Scripts Reference
| Script | Description |
| ------------------------------------ | ----------------------------------------------------- |
| bun test --timeout 30000 | Run tests with Bun and a 30s per-test cap |
| npm test | Run tests with Node.js and a 30s per-test cap |
| bun run lint | Check code with ESLint |
| bun run lint:fix | Fix ESLint issues automatically |
| bun run format | Format code with Prettier |
| bun run format:check | Check formatting without changing files |
| bun run check | Run all checks (lint + format) |
| npm run example:web:dev | Start the universal app Vite dev server |
| npm run example:web:build | Build the universal app static web bundle |
| npm run example:web:preview-images | Regenerate preview screenshots via browser-commander |
| npm run example:desktop:package | Package the Electron desktop app locally |
| npm run example:mobile:sync | Build and sync the app bundle into Capacitor projects |
| bun run changeset | Create a new changeset |
Best Practices
The repository implements CI/CD best practices for AI-driven development. See BEST-PRACTICES.md for details on:
- File size limits for AI readability
- Automated formatting and linting
- Multi-runtime and cross-platform testing
- Changeset-based versioning
- Concurrency control for CI/CD pipelines
Contributing
See CONTRIBUTING.md for detailed contribution guidelines.
Quick steps:
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Make your changes
- Create a changeset:
bun run changeset - Commit your changes (pre-commit hooks will run automatically)
- Push and create a Pull Request
License
Unlicense - Public Domain
Explicit macOS app removal and leftovers
dss apps remove "/Users/me/Applications/Example.app" previews removal;
--yes moves only that bundle to the macOS Trash. It never empties Trash or
removes support data. App identity and process activity are checked before
removal; symlinked paths, nested app components and App Store receipts are
refused. Use a vendor uninstaller first when one exists.
Chrome web apps should first be uninstalled using chrome://apps.
--launcher-only explicitly allows moving their launcher bundles to Trash;
it preserves Chrome's app registration, cookies, profiles and website data,
so Chrome may recreate the launcher. The command is separate from automatic
cache cleanup. Removed URL handlers may be recreated by their owning tool.
dss apps leftovers reports possible leftover Library files, including logs.
An absent bundle in standard application folders is only a clue: apps can live
elsewhere, and helpers or other apps can share data. Caches and logs are marked
disposable-after-activity-check; settings, application support, containers,
and saved state require a user decision. Every candidate remains read-only,
including disposable candidates. No leftover is safe to delete unconditionally;
logs may be needed for investigation. Inventory errors appear in the report.
Platform-specific competitor comparison
See the feature comparison tables for dss and 30 competitors across Linux, macOS, FreeBSD, OpenBSD, NetBSD, illumos and AIX. Capability, interface and safety-backend tables distinguish implemented features, vendor documentation, partial coverage and unknowns. The comparison is a sourced inventory, not a performance or superiority claim.
