Gigabytes back.
Nothing you can’t rebuild.
dev-prune finds Git repositories you have not touched in a while and deletes what their package managers can rebuild — node_modules, .venv, target, vendor and the rest, across thirty-five managers from npm and pip to Composer, Bundler, Mix, CocoaPods and Terraform. Nothing is deleted until the package manager itself confirms a lockfile can restore it. Verification is not a flag you can turn off.
Pruning is one command of several. The same binary restores what it removed, sizes and clears the caches package managers keep outside your projects, clears what Docker is holding, and shows where the disk went drive by drive — one tool for every dependency directory on the machine, instead of one per ecosystem.
The command is dev-prune. devp is an alias for it — the same executable installed under a shorter name, purely for ease of use. Use either, anywhere, interchangeably; every example below works spelled both ways.
LoginLens/node_modules1.30 GiBpackage-lock.jsonnpm can rebuild itLoginLens/website/node_modules284.97 MiBpackage-lock.jsonnpm can rebuild itVectra-180/.venv273.31 MiBuv.lockuv can rebuild itportfolio-creator/node_modules64.78 MiBpackage-lock.jsonnpm can rebuild it
Copy this prompt and paste it to Claude Code, Cursor, Copilot, Windsurf, or any terminal-capable agent — it installs, verifies and registers your repos for you. Details.
Install and set up `dev-prune` (binary name `devp`), a lockfile-safe workspace cleaner, on this machine. Follow these steps exactly and do not improvise beyond them.
1. Detect the OS and run the matching official installer, nothing else:
- macOS or Linux:
curl -fsSL https://devprune.vkrishna04.me/install.sh | sh
- Windows (PowerShell):
iwr -useb https://devprune.vkrishna04.me/install.ps1 | iex
- If a Rust toolchain is already present and you cannot reach the network, you may
instead run: cargo install dev-prune
Do NOT download binaries from anywhere other than devprune.vkrishna04.me or the
project's GitHub releases, and do NOT edit PATH, the registry, or any OS scheduler by
hand — the installer and `devp setup` do all of that themselves.
2. Open a NEW terminal (so the updated PATH is in effect) and verify:
devp --version
devp doctor
`devp doctor` must exit 0. If it prints warnings, read them out to me; do not try to
"fix" the scheduler or hooks yourself — they are self-installing.
3. Ask me which project directories to keep clean, then register each one:
devp init <path>
If I do not have a list, run `devp init --auto --dry-run`, read me what it found, and
register it with `devp init --auto` once I say yes. Do not register directories I did
not name or approve. `devp init` only records a directory; it never deletes anything
on its own.
4. Show me the result and stop:
devp status
Notes you should rely on, not work around:
- Installation already registered a background pass (every 2 days) and, on Windows, a
windowless task that never flashes a console window. You do not need to configure any
of this.
- Nothing is ever deleted unless a lockfile can rebuild it, the repo has been idle past
the threshold, and (interactively) I confirm. Run `devp run --dry-run` if I want a
preview.
- To undo the whole thing later: `devp uninstall`.Old projects keep their dependencies forever
A repository you last opened eight months ago is still holding a gigabyte of packages that a single command could reinstall. Deleting them by hand is tedious; deleting them with rm -rf and a find pipeline is how you lose a virtual environment nobody wrote a lockfile for.
Register
devp init ~/Code walks your workspace and records every Git repository it finds, or devp init --auto works out where to look on its own. Git hooks then keep the list current as you clone new ones.
Judge idleness
A repository is a candidate only after idle_days (15 by default) with no commit and no source file modified — so uncommitted work in progress protects itself.
Prove it is rebuildable
Each project's own package manager is asked to confirm the lockfile, read-only. A cleanup never rewrites a file Git tracks — it reports the problem and leaves the tree alone.
Delete, then restore on demand
Only verified directories are removed. Coming back to the project later is as simple as devp restore, and every project in the tree comes back with its own manager.
Seven things it will not do
These are invariants in the code, not conventions in a README.
Delete anything unproven
No directory is removed until its package manager confirms a usable lockfile. No flag bypasses this — --ignore-idle included.
Leave the .git boundary
dev-prune only operates inside a directory containing a valid .git root, and behaves identically regardless of where you invoked it from.
Reach into a nested repository
Discovery stops at a nested .git. A submodule is pruned as itself, or not at all — never as part of its parent.
Follow a symlink or junction
A linked bloat directory points at storage the repository does not own, so it is refused outright rather than followed.
Execute anything from your repo
.devprune.json and the committed project.devprune.json hold inert data only: an ignore flag, an idle-day override, a display name, automation opt-outs. Neither can name a command, so a repository you have just cloned cannot run anything on your machine.
Guess when it cannot read your config
A repository config that will not parse skips the repository and reports the syntax error. The unreadable file may have been the one saying "ignore": true.
Phone home
No analytics, no diagnostics, no usage data. One request exists — an unauthenticated release check against GitHub, with no body and no identifier — and devp config set update_check false ends it. When that check finds a newer release, dev-prune installs it after the next pass — verified against its published SHA-256, and never by handing your machine to a package manager. devp config set auto_update false leaves upgrading to you, and devp config set version_lock true pins this copy where it is — nothing dev-prune does replaces the binary after that, not even a re-run of the installer.
Ignore your opt-out slowly
ignore.devprune.json in a repository root is a single file-existence check, made before any config is read or parsed.
Corrupt its own state
The registry is written to a temporary file and swapped into place, so an interrupted write cannot leave a half-written list of your repositories.
Full detail in Safety Invariants .
Thirty-five managers. Any number per repository.
A repository is not assumed to be one project. dev-prune walks the root and, by default, six levels below it — raise or lower that with devp config set scan_depth N — and every directory a package manager recognises is verified, pruned and restored on its own terms.
JavaScript & TypeScript
Four managers, one directory. All of them install into node_modules, so at most one of them owns any given project.
| Manager | Detected by | Deletes | Verified with read-only | Restored with |
|---|---|---|---|---|
| npm | package-lock.json | node_modules | npm ci --dry-run --ignore-scripts | npm ci |
| pnpm | pnpm-lock.yaml | node_modules | pnpm install --lockfile-only --frozen-lockfile | pnpm install --frozen-lockfile |
| Yarn | yarn.lock | node_modules | yarn install --immutable --mode update-lockfile (Berry) | yarn install --immutable |
| Bun | bun.lockb, bun.lock | node_modules | bun install --frozen-lockfile --dry-run --ignore-scripts | bun install --frozen-lockfile |
| Deno | deno.lock | node_modules, vendor | deno.lock complete and not stale | deno install |
When more than one claims the same tree, the owner is decided in this order: the packageManager field in package.json; else whichever manager's bookkeeping files are actually inside the installed node_modules; else the most recently written lockfile. Only that manager verifies, deletes and restores — the others are not consulted. Deno is outside that contest: the other four are alternatives to each other, while a repository holding both a deno.lock and a package-lock.json genuinely uses both tools. It takes vendor/ only when the Deno config asked for one — Go and Composer use that name for something else entirely — and the shared node_modules is still counted and deleted once.
Python
Six managers that can describe the same project, because a uv, Poetry or PDM project is still a directory with a virtual environment in it.
| Manager | Detected by | Deletes | Verified with read-only | Restored with |
|---|---|---|---|---|
| uv | uv.lock, [tool.uv] | .venv | uv lock --locked | uv sync |
| Poetry | poetry.lock, [tool.poetry] | .venv | poetry check --lock | poetry install |
| PDM | pdm.lock, [tool.pdm] | .venv, __pypackages__ | pdm lock --check | pdm install |
| Pipenv | Pipfile | .venv, in-project only | pipenv verify | pipenv install --deploy |
| venv / pip | requirements.txt + pyvenv.cfg | every dir holding pyvenv.cfg | requirements.txt lists ≥ 1 package | python -m venv .venv && pip install -r … |
| pixi | pixi.toml, pixi.lock, [tool.pixi] | .pixi | pixi.lock carries its version: and records at least one conda or PyPI package | pixi install |
uv and Poetry win over plain venv whenever their lockfile or pyproject.toml table is present, because a real lockfile rebuilds the environment exactly and a requirements.txt only approximates it. When uv and Poetry both describe the same project — usually a half-finished migration — the one whose lockfile is actually on disk owns the environment. A project with none of that falls back to venv, and a venv with an empty requirements.txt is refused outright — there would be nothing to reinstall from. Pipenv is claimed only when its environment is inside the repository — the .venv you get from PIPENV_VENV_IN_PROJECT. Its default is a virtualenv in a shared directory under your home, keyed by a hash of the project path, which a repository-scoped prune has no business reaching into.
Rust & Go
One manager each, no ambiguity — and the clearest illustration of why every verification in these tables is read-only.
| Manager | Detected by | Deletes | Verified with read-only | Restored with |
|---|---|---|---|---|
| Cargo — opt-in | Cargo.toml | target | cargo metadata --locked | next cargo build |
| Go | go.mod | vendor | go mod download | go mod vendor |
Read-only, in every ecosystem above. cargo generate-lockfile and go mod tidy make it obvious why: they rewrite Cargo.lock and go.mod/go.sum, tracked files that would turn a cleanup into a diff you did not ask for. The same is true of npm install --package-lock-only and uv lock, so none of them run during a normal pass either. A lockfile that has drifted from its manifest is reported and the directory is left alone — because a pass can be started by the OS scheduler, and a background cleanup must never leave a modified tracked file behind. If you would rather it fixed the lockfile for you, devp config set allow_manifest_rewrite true opts in, and it means the same thing for every adapter with a lockfile. Cargo ships off. target/ is compiler output: the lockfile proves it comes back, but it comes back by recompiling rather than downloading, which on a real workspace is minutes rather than seconds. devp config set enable_cargo true turns it on, under the same build_idle_days gate as the build tools below.
PHP, Ruby, Elixir & Apple
Four managers with one thing in common: only the copy that lives inside the repository is ever claimed.
| Manager | Detected by | Deletes | Verified with read-only | Restored with |
|---|---|---|---|---|
| Composer | composer.json | vendor | composer validate --no-check-publish | composer install |
| Bundler | Gemfile | vendor/bundle | bundle lock --check | bundle install |
| CocoaPods | Podfile | Pods | Podfile.lock complete and not stale | pod install |
| Mix | mix.exs | deps | mix.lock complete and not stale | mix deps.get |
Only what is inside the repository. Bundler defaults to a shared gem home outside it, so vendor/bundle is claimed and nothing else — and .bundle/ is deliberately left alone, because it holds the path configuration that sends the next bundle install back there. Composer declines vendor/ outright when a vendor/bundle is sitting inside it: deleting it would take gems with it under a proof that says nothing about them. Mix takes deps/ and never _build/, which is compiled output. CocoaPods and Mix also share the offline proof with the Dart adapter — none of the three ecosystems has a read-only in-sync check, and the write-side commands fix drift by re-downloading, so what is verified instead is that the lockfile is structurally complete and no older than the manifest it came from.
Infrastructure
One adapter, and the interesting part is everything it refuses to claim.
| Manager | Detected by | Deletes | Verified with read-only | Restored with |
|---|---|---|---|---|
| Terraform | any *.tf / *.tf.json | .terraform/providers | .terraform.lock.hcl records a provider | terraform init -backend=false |
Providers only, never the rest of .terraform/. environment records the workspace you selected, and losing it silently returns you to default — so the next apply somebody runs without looking targets the wrong environment. terraform.tfstate in there is the backend's initialisation record, and rebuilding it needs the backend's credentials. modules/ is fetched from module sources that .terraform.lock.hcl says nothing about, so an unpinned git:: source can come back as something else. The providers are the bulk anyway — a few hundred megabytes of plugin binaries per root module, times every environment directory in the repository.
JVM, Swift, Dart, C/C++, Zig, Haskell & game engines — opt-in
Build tools whose recoverability proof is the manifest itself: a build tree is regenerated by recompiling the sources sitting next to it, not by downloading.
| Manager | Detected by | Deletes | Verified with read-only | Restored with |
|---|---|---|---|---|
| Gradle | build.gradle[.kts], settings.gradle[.kts] | build, .gradle | manifest present and readable | next ./gradlew build |
| Maven | pom.xml | target | pom.xml parses as a Maven manifest | next mvn package |
| SwiftPM | Package.swift | .build | Package.swift declares a package | next swift build |
| Dart / Flutter | pubspec.yaml | .dart_tool | pubspec.lock complete and not stale | dart pub get / flutter pub get |
| Elixir Mix _build | mix.exs | _build | mix.exs and mix.lock both present | next mix compile |
| vcpkg | vcpkg.json | vcpkg_installed | vcpkg.json declares dependencies | next vcpkg install |
| cmake_build | CMakeLists.txt | any tree holding a CMakeCache.txt | that cache names a source directory in this repository | next cmake --build |
| dotnet_build | *.csproj / *.fsproj / *.vbproj | bin, obj | obj/project.assets.json records the project file beside it | next dotnet build |
| godot | project.godot | .godot, .import | project.godot carries a config_version= line | editor re-imports on next open, or godot --headless --import |
| unity | ProjectSettings/ProjectVersion.txt | Library, Temp | ProjectVersion.txt carries an m_EditorVersion: line, and Temp/UnityLockfile is absent (an open editor is refused, not raced) | editor re-imports on next open |
| unreal | any *.uproject file | DerivedDataCache, Intermediate (never Saved, Binaries or Content) | the .uproject parses as JSON with a "FileVersion" key | editor rebuilds the DDC on next open; regenerate project files |
| defold | game.project | build (never .internal or assets) | game.project carries a [project] section | next editor build recompiles |
| cocos | package.json naming Cocos Creator | library, temp (never build, the exported output) | the package.json has a "creator" key or a cocos-creator engine line, so a plain Node project is never touched | Creator re-imports on next open |
| zig | build.zig | .zig-cache, zig-cache, zig-out | build.zig declares a pub fn build | next zig build |
| stack | stack.yaml | .stack-work | stack.yaml names its resolver: (or snapshot:), so the rebuild compiles against the same pinned snapshot | next stack build |
| cabal | cabal.project | dist-newstyle | cabal.project declares its packages; a lone *.cabal file is never claimed | next cabal build |
| sbt | build.sbt | target, project/target | build.sbt carries a setting, or project/build.properties pins sbt.version | next sbt compile |
Off until you switch them on. A build directory takes far longer to get back than a dependency directory — a full recompile, not a download — so these seventeen, and Cargo above, ship disabled and invisible. devp config set enable_gradle true / enable_maven true / enable_swift true / enable_dart true / enable_mix_build true / enable_vcpkg true / enable_cmake_build true / enable_dotnet_build true / enable_godot true / enable_unity true / enable_unreal true / enable_defold true / enable_cocos true / enable_zig true / enable_stack true / enable_cabal true / enable_sbt true turns them on, and their candidates wait for build_idle_days (45 by default), applied as the maximum of it and idle_days — the build-tool gate can only ever make pruning later, never earlier. One adapter can be made to wait longer than the rest with devp config set adapter_idle_days cargo=90, which raises that adapter's window and never lowers it. The dependencies themselves never lived in the repository: ~/.m2, ~/.gradle and ~/.pub-cache are machine-wide stores, which is devp caches territory. Dart is here for the same reason as the others and not an obvious one: .dart_tool/ holds a second's worth of pub metadata and, beside it, the build_runner and flutter_build caches, which come back only by recompiling. vcpkg is here because it builds every port from source: what sits in vcpkg_installed/ is a compiled Boost or Qt, and vcpkg install compiles it again. It reads vcpkg.json for a dependencies list before touching anything, because every vcpkg port carries a vcpkg.json too and a port manifest rebuilds nothing. Only manifest mode is claimed — classic mode's one install tree is shared by every project on the machine, so it belongs to devp caches. CMake is the one that has to prove whose build/ it is, and the answer is never the name. cmake writes a CMakeCache.txt at the top of every tree it configures and nobody writes one by hand; that file records CMAKE_HOME_DIRECTORY, the source directory it was configured from. A directory is claimed only when that recorded source directory still exists, still holds a CMakeLists.txt and sits inside this repository — so a build/ you filled by hand is never touched. The search stops at the first cache it finds, so the sub-builds FetchContent leaves under build/_deps/ go with the tree that configured them, and it looks three levels down because Visual Studio configures into out/build/<preset>/.
Three managers, one root
monorepo/ ├── package-lock.json ├── uv.lock └── Cargo.toml
One manager each, nested
monorepo/
├── frontend/
│ └── pnpm-lock.yaml
├── services/api/
│ └── uv.lock
└── tools/cli/
└── Cargo.tomlRoot plus nested, mixed
monorepo/
├── Cargo.toml
├── web/
│ └── package-lock.json
└── scripts/
└── requirements.txtThirty-six would be better than thirty-five
Adding a manager is deliberately small: implement one PackageManager trait — detect, list bloat directories, verify the lockfile, restore — register it in one array, and add its fixtures to the adapter test suite. Nothing else in the codebase has to know it exists. The obvious ecosystems are covered; what is left is the awkward one. Nix would have to reason about the store, which is a different kind of problem from “a lockfile says this comes back”. Beyond that, name the manager and the recipe is written down.
The walkthrough, with the trait signature and a worked example, is in Adding an adapter — and CONTRIBUTING.md covers what a pull request needs before it can be merged.
Rough back-of-envelope
Not a measurement — your numbers, multiplied. Run devp run --dry-run for the real figure.
The whole command surface
dev-prune and devp are the same executable under two names. Both work in every shell — cmd, PowerShell, bash, fish, an IDE terminal, a scheduled task — without a profile alias that has to be re-sourced.
Wherever a command takes [PATH], a literal . means the directory you are standing in. It is the default for init, link, unlink, restore and the config per-repo actions, and run accepts it as its target — so devp run . prunes this repository and nothing else. Worth saying out loud because . is usually treated as a shell detail rather than an argument: here it is a real path, it works on every platform, and it works the same in every command that takes one.
| Command | What it does |
|---|---|
devp init [PATHS] | Crawl for Git repositories and register them, then run the setup pass. --auto works out the paths itself instead of being told them |
devp link [PATH] | Register a single repository |
devp unlink [PATH] | Unregister a repository. Deletes nothing on disk |
devp unlink --missing | Drop every registered path whose directory no longer exists, in one pass |
devp undo | Revert the most recent init or link |
devp run [PATH] | Prune every registered repository, or one target |
devp status [--top N] | Interactive dashboard; a plain table when there is no TTY. --top N lists only the N repositories with the most reclaimable space — the totals above the table still cover every one of them. Once a restore has been timed on this machine, the header also estimates how long putting it all back would take |
devp status --drift | Every environment holding packages its lockfile never recorded, with the one command that records them. A pure read — this is what a prune would refuse on |
devp stats | What has already been reclaimed: lifetime total, prune passes, the most recent pass and how to undo it, and the repositories that gave back the most |
devp history | Which pass deleted what, and what asked it to — one line per pass, then --pass 1 for the exact command line and every directory it took. --export writes the lot to your documents folder |
devp completions <shell> | Print a tab-completion script for bash, zsh, fish, PowerShell or elvish, generated from the same argument definition the binary parses with |
devp caches | Size every package manager cache on the machine — npm to cargo to conda, Maven, Gradle, NuGet, vcpkg, Conan, Composer, CocoaPods and Hex — and print the command that clears each. Reports only — it deletes nothing |
devp caches --volume V: | Only the caches sitting on one drive. --drive is the same flag, and it takes a mount point (/mnt/data, /Volumes/Work) or any path on the drive you mean. The unfiltered report ends with a By drive line splitting the total the same way: on a machine whose projects live on a second disk, the machine-wide total is not the figure that decides anything — the gigabytes on the drive that is full are |
devp caches docker | What a container engine is holding — images, containers, volumes and build cache, each sized, with how much of it the engine says it could give back — then the prune commands and what each takes with it. Also podman, nerdctl, finch, Apple’s container, and devp caches containers for every engine at once plus any local Kubernetes clusters. The report deletes nothing; the next row does |
devp caches clear <engine> | Run the narrow prune commands for you — build cache, unused images, stopped containers — after printing them and asking, and count what came back on devp stats. Never a volume on its own: there is no argument in the table containing the word, and a test fails the build if one appears. --include-volumes (docker and podman) then lists the unused volumes by name for you to pick from, one unforced volume rm per pick, and refuses --yes, --json and a piped stdin. Name the engine; no schedule, hook or clear all reaches it |
devp caches clear <manager> | Empty one cache, a comma list of them (npm,uv,pip), or all of them, after listing and sizing what goes. Runs the manager's own clear command and measures what was actually freed. all --except npm,uv leaves the named ones alone; --over-cap narrows it to the managers that have outgrown their cache_max_gb; --unused to the ones no registered repository uses. Only ever when you type it — no pass, schedule or hook clears a cache, and Maven’s ~/.m2/repository is never cleared at all |
devp trust | What dev-prune is allowed to do on this machine, on one screen: what the code guarantees everywhere, then every setting you have switched on that widens it, by name, then every copy of it on the machine — not just the managed ones: everything on your PATH and in each package manager’s install directory, with the manager that put it there and the SHA-256 an antivirus actually sees — the one on your disk, not the one on the release page |
devp restore [PATH] | Reinstall dependencies for every project in a tree |
devp restore --last-run | Put back exactly what the last prune pass deleted, in every repository it touched |
devp doctor [PATH] | Check the installation, or one repository — ending with the single reason a pass would or would not touch it |
devp doctor --fix | Repair what the checks found — a stale devp twin, a devpw scheduler binary behind the release, hooks or a scheduler pointing at a binary that is gone, dead registry entries. Mends installed-but-broken only; never a first-time install |
devp config … | Global settings, per-repo config, scheduler, hooks, file manager icons. devp config wizard opens every setting in a full-screen configurator, including which adapters to switch off; devp config recommended applies the recommended ones in one command |
devp setup [--status] | Install any missing integration; --status only reports |
devp skill | Export SKILL.md and install it into your AI agent's skills directory; --agent <editor> writes per-repository rules for any of sixteen editors |
devp update [-y | --install] | Print the installed version and the upgrade command, then ask [y/N] when a newer release exists; -y or --install skips the question. A yes downloads the release binary, verifies its checksum and replaces every copy this install runs |
devp install [--channel] | Move this install to another package manager — installs through the one you name, then removes the old copy through the one that put it there; --dry-run prints the plan and runs none of it |
devp man [command] | The contents page: every command grouped by what it is for. devp man run for one page, roff when redirected, --dir for the full set |
devp uninstall [--deep] | Remove the program — scheduler, hooks, agent skill, PATH entry and every copy of the binary, install channels included; --deep also clears config |
devp -V | Version plus an environment audit: OS, arch, config path, PATH |
Global flags
--dry-run— report every candidate and its size; touch nothing--ignore-idle— lift the idle-day wait, and only that. Lockfile verification still applies-y/--yes— skip interactive confirmation
On run: --except keeps named repositories out of the pass, --only / --skip narrow it to certain managers, --min-size sets a size floor, --explain lists every repository with the reason it would or would not be pruned (read-only), and --json emits one machine-readable document instead of the report, on run, status, stats, history, trust and caches.
Exit codes
0— success, including "nothing was idle enough to prune"1— the command failed; the reason is on stderr2— the arguments were not usable
Background automation
The OS scheduler and Git auto-registration hooks install themselves at install time, and again after an upgrade if anything went missing. The pass skips the hooks when git is absent, and when core.hooksPath already belongs to husky, pre-commit or lefthook it says so instead of taking the slot — devp hook install --chain takes it politely, by forwarding every hook on to the tool that had it.
The setup pass registers no repository itself. The scheduled prune pass is the one place that can, and only while auto_discover is on: it looks for repositories you never added, using the same roots as devp init --auto. A manual devp run never does it, a repository holding an ignore.devprune.json is never registered, and being registered still only means being considered — not pruned.
Off switches: auto_daemon, auto_discover, auto_hooks, auto_setup, or DEV_PRUNE_NO_AUTO_SETUP=1.
Per-repository config
{
"project_name": "My App",
"ignore": false,
"override_idle_days": 30,
"disable_daemon": false,
"disable_hooks": false
}.devprune.json is yours: dev-prune writes it into .git/info/exclude, so it never reaches a commit. devp config project . --team writes the same keys to project.devprune.json, which is meant to be committed — every key it names wins, and your file answers the rest. Or drop an empty ignore.devprune.json in the root to opt out entirely. All three are read from the repository root and nowhere else, because every path inside them is relative to that root — a copy one directory down parses, looks applied and is read by nothing. devp doctor names any it finds, and leaves them where they are: moving one up a level would silently change what every path inside it means.
Every flag, alias and shorthand is in the CLI reference .
Your agent already knows how to use it
dev-prune ships a skill file describing its full command surface, its safety rules, its exit codes and a troubleshooting decision tree. It is exported to your config directory automatically and kept in step with the installed binary.
Export it
devp skillWrites SKILL.md to the config directory and prints ready-to-paste onboarding prompts for Claude Code, Cursor, Windsurf, Copilot, Antigravity and anything else that reads a skill file.
Rules for your editor
devp skill --agent antigravityWrites the per-repository rules file that editor actually reads — .agent/rules/ for Gemini Antigravity,.cursor/rules/ for Cursor, and so on. Sixteen targets: Antigravity, Cursor, Windsurf, Cline, Roo, Kilo Code, Continue, Amazon Q, Kiro and Trae get their own file; Copilot, Gemini CLI, Junie, Zed, Aider and AGENTS.md get a marked block inside a file they already share. Aider is the one that has to be told to read its CONVENTIONS.md — the command says so after writing it. Run devp skill --help for the exact paths. Not sure which you have? Plain devp skill reports every editor it detects on the machine or in the repository, and devp skill --detected writes rules for all of them in one pass.
Just ask
“How much space can I get back?” → the agent runs devp run --dry-run and reads you the total. “Clean up but keep the API project” → it runs devp run --except api, which never verifies, deletes or reinstalls that one, rather than pruning everything and downloading it back afterwards. “Why didn't it delete anything?” → it runs devp doctor ., which ends by naming the one reason, and tells you.
You do not have to learn the flags. That is the point.
Machine-readable docs
- /llms.txt — summary for language models
- SKILL.md — the shipped skill, verbatim
- devprune.schema.json — JSON Schema for
.devprune.json
And in your editor
Nothing here is required — the CLI is the product. But the config file is easier to write when the editor knows its shape, and a repository's reclaimable size is easier to notice when it is already on screen.
The extension
code --install-extension VKrishna04.dev-pruneValidates .devprune.json as you type — every key, every adapter name, every enum — from the schema bundled inside it rather than fetched, so it works offline. The workspace's reclaimable size sits in the status bar.
VS Code Marketplace · Open VSX — the same extension for VSCodium, Cursor, Windsurf, Positron and Kiro, which cannot reach Microsoft's registry.
No extension needed
The config schema is registered with SchemaStore, so IntelliJ, PyCharm, WebStorm, GoLand, RubyMine, Rider, Visual Studio, Neovim and Zed validate .devprune.json out of the box. There is nothing to install and nothing to configure.
devp setup offers the extension once, at a terminal, into whichever editors it finds — each from its own registry.
And your coding agent
devp skill --agent cursorWrites the rules file that editor actually reads — .github/copilot-instructions.md, .cursor/rules/, CLAUDE.md, .junie/guidelines.md and the rest — so an agent working in the repository knows what dev-prune will and will not delete before it suggests anything.
Questions worth asking first
Can it delete something I cannot get back?
--ignore-idle, not -y, not the daemon.Is there a --force flag?
--ignore-idle, which is all it has ever done: lift the idle-day wait, and nothing else. It was renamed because “force” reads like “override the safety checks”, and there is no flag that does that. Typing --force prints a one-line note pointing at the new name, along with the seven reasons a directory gets skipped and how to fix each one.What about uncommitted work?
git log and source file modification times. A repository you edited yesterday without committing is Active, and is not a candidate.Does it work on monorepos?
devp config set scan_depth N to change it — and handled independently, and each directory is reported by its repository-relative path.Will it break husky or pre-commit?
core.hooksPath, and Git allows exactly one hooks directory with no way to chain them. If that setting already belongs to another tool, the setup pass leaves it alone and says so rather than silently taking the slot. When you want both, devp hook install --chain claims the slot and writes a shim for every hook the displaced directory has, each one running dev-prune's registration and then exec-ing the original — so husky still fires, and devp hook uninstall puts the old path back exactly as it was. If the other tool later adds a hook, the next setup pass notices the drift and rebuilds the shims.How do I stop it installing background things?
devp config set auto_setup false turns off the whole pass, auto_hooks, auto_daemon and auto_discover turn off one part each, and DEV_PRUNE_NO_AUTO_SETUP=1 overrides all of them without a config file — useful in a Dockerfile, where you can set it before the binary is ever run.Does it send anything over the network?
GET to GitHub's public releases page, so it can tell you when a newer version is out. It has no body, carries no identifier, and runs at most once a week. It is opt-out rather than opt-in, because a tool that deletes directories is one whose fixes you want: devp config set update_check false switches it off, and devp update --offline skips it once. Everything else on the wire is your own package manager during verification or restore.Does it delete build output — dist/, .next/, target/?
dist/ rebuilds byte-for-byte, so there is no dist/ rule and there will not be one. A repository can still declare it — prunable.directories names the path and the rebuild command that puts it back, which dev-prune checks is installed before it deletes. That file is committed, so prunable.exclude is the other half: it takes a declared path back out on the one machine that is keeping it, without editing a file the whole team shares. Across the two files that is the whole point; naming one path in both lists of the same file is a typo rather than a decision, because the exclusion still wins and the declaration then never runs — devp doctor says so rather than letting it pass quietly. The eighteen exceptions are opt-in and say so — devp config set enable_cargo true (target/), enable_gradle (build/, .gradle/), enable_maven (target/), enable_swift (.build/), enable_dart (.dart_tool/), enable_mix_build (_build/), enable_vcpkg (vcpkg_installed/), enable_cmake_build (a tree holding a CMakeCache.txt that names sources in this repository), enable_dotnet_build (bin/+obj/, proven by project.assets.json), enable_godot (.godot/, the editor's import cache), enable_unity (Library/+Temp/, refused while the editor holds Temp/UnityLockfile), enable_unreal (DerivedDataCache/+Intermediate/), enable_defold (build/), enable_cocos (library/+temp/, only when the package.json names Cocos Creator), enable_zig (.zig-cache/+zig-cache/+zig-out/), enable_stack (.stack-work/), enable_cabal (dist-newstyle/) and enable_sbt (target/+project/target/) — whose claim is rebuild-from-source rather than reinstall-from-lockfile, which is why they ship off and wait an extra build_idle_days (45) before they touch anything.Can I turn one ecosystem off for good?
devp config set disabled_adapters go,composer makes dev-prune behave as though those two were not installed at all: not detected, not counted, not probed for by doctor, never pruned and never restored. devp config set disabled_adapters - turns them back on, and devp config wizard offers the same list as a checklist. For a single pass, devp run --only npm,cargo and --skip go do the same thing temporarily. Every setting that widens what is deletable is listed by name in devp trust — no letter grade, just which switches are on and how to put each one back.Does it touch anything outside my repositories?
.git boundary. The machine-wide package caches (~/.npm, ~/.cargo, ~/.m2, ~/.gradle and the rest) are a separate, explicit command: devp caches reports them and only devp caches clear <manager> removes one, after confirming. Maven’s ~/.m2/repository is the one it will not remove even then: mvn install:install-file puts artifacts there that exist in no remote, so dev-prune sizes it, prints the command and lets you decide.Docker is bigger than all of these put together. Does it help?
devp caches docker — or podman, nerdctl, finch, Apple’s container, or devp caches containers for all of them — breaks the space down into images, containers, local volumes and build cache, says how much of each the engine itself calls reclaimable, and then prints the prune commands narrowest first with what each one takes with it. Then devp caches clear docker runs the narrow ones for you: build cache, unused images, stopped containers, printed first, after a prompt, and counted on devp stats so the space you reclaimed on its advice is space it can account for. It will not touch a volume on its own, and it never runs volume prune — an image can be pulled again and a build cache rebuilt, but what is inside a named volume is the only copy, so a volume goes only when someone names it. --include-volumes (docker and podman) is that naming: it lists the unused volumes by name, you type the numbers of the ones to delete, and each pick is one unforced volume rm. The flag refuses --yes, --json and a piped stdin, so only a person at a terminal can use it. With --dry-run it deletes nothing and instead lists the unused volumes with the devp command to paste, so an agent can prepare everything and hand the final command to a person; picks made through devp are counted on devp stats, which a raw volume rm would not be. The pick list only arms within ten minutes of a completed dry run for that engine: typed cold, the real command runs the dry run instead and says so, and the same line typed again inside the window reaches the picking. Nothing on a schedule goes near any of it. The figures come from the engine's own system df rather than a look at the disk, because on Docker Desktop and Podman the store lives inside a VM disk image your filesystem cannot see — and because asking is the only way to learn what is reclaimable, which is the number that decides anything. If the daemon is stopped, you get that sentence and no figures: a blank, not a zero.Which of these caches does anything still need?
devp caches answers it per manager: how many of your registered repositories use it, and what its cache works out to per repository. Two repositories sharing a 12 GiB cache is 6 GiB each and worth a look; forty sharing the same 12 GiB is 300 MiB each and is the cache doing its job. A manager no registered repository uses is the one case where a count is enough to act on — everything in it was downloaded for projects that are not on this disk any more — and devp caches clear --unused all empties exactly those. The count ignores whether an adapter is switched on, because the question is which managers your projects use, not which ones a prune pass would touch. It is shown only for the managers that are also adapter names; pip, conda, nuget, conan and hex get no number rather than a guess. With nothing registered, nothing is counted and --unused refuses to run — every cache would look unused.My projects are on a second drive. Did it find the right pnpm store?
node_modules it fills, and a hardlink cannot cross a filesystem — so projects kept off the system disk get a store of their own at the root of their filesystem: V:\.pnpm-store on a second Windows drive, /mnt/data/.pnpm-store on Linux, /Volumes/Work/.pnpm-store on an external macOS volume. It is not a Windows idea; it is wherever a developer keeps projects off the system disk. pnpm store path only ever answers for the filesystem it is run on, so a machine-wide report asked from your home directory finds the small store beside it and misses the multi-gigabyte one holding your actual projects. devp caches looks at the root of every filesystem that holds a registered repository, plus the one you are standing in, and gives each store a row that names itself in its clear command: pnpm store prune --store-dir <path>.My uv cache is over 10 GB. Can it tell me?
devp config set cache_max_gb default=10 writes down how big is too big — one ceiling for every manager, in gibibytes, GiB being the unit the report prints. Write default=10,npm=4 to give one manager a figure of its own; a manager named outright is held to that and not also to the default. The first run suggests it, and a ceiling you set yourself is never overwritten by it. A cache is a bet that re-downloading costs more than the disk it occupies, and somewhere the bet stops paying — this is where you say where. A manager past its ceiling is marked in devp caches, measured against its whole footprint, so cargo’s registry cache and its unpacked sources are weighed together. Setting a cap deletes nothing. It marks, and devp caches clear --over-cap all empties exactly what is marked, when you type it. Empty by default: no cache is too big until you say what too big is. The keys are the names devp caches clear takes — npm, pnpm, uv, pip, cargo, go, nuget and the rest — and devp config wizard sets them as a column beside the adapter checklist.Can I install it with npm?
npm install -g dev-prune, or npx dev-prune status to run it once without installing anything. It works the way esbuild and Biome do: a small dev-prune package lists seven platform packages as optional dependencies, and npm installs only the one matching your machine. The binary lives inside that package, so there is no download step at install time — it installs under npm ci --ignore-scripts, behind a registry mirror, and with no access to GitHub. Windows needs 1.8.0 or later: the three Windows packages did not exist before then, and a published manifest cannot be edited, so a machine still holding [email protected] needs npm install -g dev-prune@latest rather than a repair. bun, pnpm and Yarn install that same package — bun add -g dev-prune, pnpm add -g dev-prune, yarn global add dev-prune — and dev-prune treats each as a channel of its own rather than as npm, because the four do not share records. A copy bun installed is upgraded with bun and removed with bun; running npm against it would add a second copy under npm’s prefix and leave bun’s, still on PATH, at the old version. devp update --channels prints every channel’s upgrade command.Which copy of devp am I actually running?
devp doctor answers that. Installing over time from pip, npm, cargo or uv leaves copies in several places, and the one first on PATH is not always the one the scheduler and git hooks invoke — doctor reports every copy it finds and the version each is, plus an Install receipt line for a copy one of the install scripts wrote, naming its version, which script wrote it and when. The one-liner asks about a copy it finds rather than deleting it: answer y and the older binary runs devp install --channel installer itself, installing here and uninstalling there through the manager that owns it. devp update --install instead upgrades all of them at once: it downloads the release binary for your platform, checks it against the SHA-256 published beside it, and installs nothing if that does not match. The windowless devpw.exe the scheduler runs is a separate build, so it gets its own verified download in the same pass, and a stale one is brought forward even when the console binaries are already current.Does it work with Chinese, Japanese or Korean paths?
ワークスペース/项目目录名称测试 is scanned, pruned and restored exactly like an ASCII one. The terminal output measures display columns rather than characters, which is what keeps the tables in devp status and devp doctor aligned when a name is full-width — one CJK character occupies two columns, and padding it by character count is the usual reason a tool's columns go crooked. The same holds for accented Latin, Cyrillic, Arabic and emoji directory names.How do I remove it?
devp uninstall removes the program: the scheduler, hooks, the installed agent skill, the PATH entry and the binaries themselves — then finds every other copy that pip, npm, cargo or uv left behind and removes them all after one confirmation, printing each manager's own uninstall line so its records clear too. On Windows a running program cannot delete itself, so the copy you invoked is renamed aside — the command stops resolving straight away — and Windows removes what is left at the next restart. Nothing is left running behind it. Add --deep to also wipe the configuration directory and every registered repository's .devprune.json — it asks first.Everywhere developers have disks
Paths in any script prune and restore the same way — and the headings above them come in twelve languages, not just this pitch.
Your disk is full of dependencies you can rebuild. dev-prune deletes them — only after the package manager proves a lockfile can put them back.
Try it in dry-run. It deletes nothing.
curl -fsSL https://devprune.vkrishna04.me/install.sh | shdevp init ~/Code && devp run --dry-runApache-2.0 · no telemetry in the CLI · Windows, macOS and Linux · Rust 1.88