From esond
Remove unused PackageVersion entries from `Directory.Packages.props` files in a .NET repo using Central Package Management (CPM). Scans every `.csproj`, `.props`, and `.targets` file for PackageReference includes, computes which PackageVersion IDs aren't referenced anywhere, removes the unused entries with line-targeted edits that preserve formatting and comments, then verifies with `dotnet restore`. Use this skill whenever the user wants to clean up, prune, or remove unused entries from `Directory.Packages.props`, or says things like "clean up CPM", "remove unused packages from central package management", "the props file has stale entries", "prune Directory.Packages.props", or "dead packages in the central versions file" — even if they don't literally say "CPM". Trigger proactively when the user mentions `Directory.Packages.props` alongside cleanup, removal, dead, stale, or unused. Refuses to run on repos without a `Directory.Packages.props` (CPM not in use).
How this skill is triggered — by the user, by Claude, or both
Slash command
/esond:clean-unused-cpm-packagesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Remove `<PackageVersion>` entries from `Directory.Packages.props` files when no project, `.props`, or `.targets` file in the repo references the corresponding package. Conservatively compute the unused set by scanning every place a `PackageReference` could live, then make the smallest edits possible while preserving comments, blank lines, and hand-curated grouping.
Remove <PackageVersion> entries from Directory.Packages.props files when no project, .props, or .targets file in the repo references the corresponding package. Conservatively compute the unused set by scanning every place a PackageReference could live, then make the smallest edits possible while preserving comments, blank lines, and hand-curated grouping.
This is a destructive operation against tracked source files. Work through the steps carefully, stop at the confirmation checkpoint, and verify with dotnet restore before declaring success.
PackageVersion sources (entries considered for removal):
Directory.Packages.props in the repo, including nested ones.PackageReference sources (anything that "uses" a PackageVersion):
.csproj..props and .targets file other than Directory.Packages.props itself — including Directory.Build.props, Directory.Build.targets, and any custom imports.Excluded from removal:
<GlobalPackageReference> entries. They auto-apply to every project — the entry itself is the reference. Removing one drops an analyzer/generator from every project, which is a deliberate user decision, not something to automate.<PackageVersion ... Condition="..." />): treat them like any other entry. The condition gates when the version applies, not whether the package is referenced — so if no project references the ID, the entry is unused regardless of the condition.Stop and report if any fail.
CPM is in use. At least one Directory.Packages.props exists in the repo:
find . -type f -name 'Directory.Packages.props' \
-not -path '*/node_modules/*' -not -path '*/bin/*' -not -path '*/obj/*'
If none, this isn't a CPM repo — stop and tell the user.
Working tree is clean for the files this skill will touch.
git status --porcelain -- ':(glob)**/Directory.Packages.props'
The :(glob) magic is required so the pathspec crosses directory boundaries — without it, nested Directory.Packages.props files aren't checked. If any have uncommitted edits, ask before mixing in the skill's changes.
dotnet is on PATH. The Step 6 verification depends on it; the skill does not run without it:
dotnet --version
If dotnet is missing or fails, stop and ask the user to install/fix it before re-running.
No .fsproj or .vbproj projects in the repo. This skill only scans .csproj. References held by F#/VB projects won't be counted, and PackageVersion entries used only by them would be wrongly flagged as unused:
find . -type f \( -name '*.fsproj' -o -name '*.vbproj' \) \
-not -path '*/node_modules/*' -not -path '*/bin/*' -not -path '*/obj/*'
If any are found, warn the user that references in those projects won't be detected and ask whether to proceed regardless.
PackageVersion and GlobalPackageReferenceFor each Directory.Packages.props file, read it and record:
<PackageVersion> entry: package ID, version, and source file.<GlobalPackageReference> entry: same, but tagged "global" so Step 3 skips it.Multi-line entries (rare, but valid) span multiple lines — capture the whole block. Conditional entries (Condition="...") are captured like any other.
PackageReference Include across the repoSearch in:
.csproj files..props and .targets files other than Directory.Packages.props files.Exclude build output and dependency directories: bin/, obj/, node_modules/, .git/. Files under those (e.g. obj/*.g.props) hold generated PackageReference entries that don't reflect actual project intent and would mask legitimately-unused entries.
For each remaining file, find every <PackageReference> element and pull its Include (or Update) attribute value. Update modifies metadata of an existing reference and still counts as a reference. Use multiline-aware matching: <PackageReference> attributes can spill across lines.
Build a deduped set of referenced package IDs. Compare case-insensitively: NuGet package IDs are case-insensitive, so a Directory.Packages.props entry of Microsoft.Extensions.Hosting and a project file with microsoft.extensions.hosting describe the same package — they must match. Keep the original casing for any displayed names and for the line edits in Step 5.
For each PackageVersion from Step 1: if its ID is not in the Step 2 set, it's unused.
GlobalPackageReference entries are skipped — they're never proposed for removal.
If the unused set is empty, tell the user there's nothing to remove and stop.
Print the proposed removals grouped by source file, sorted by package ID:
[<path/to/Directory.Packages.props>]
- SomePackage.A (1.2.3)
- SomePackage.B (4.5.6)
[<path/to/other/Directory.Packages.props>]
- SomePackage.C (7.8.9)
3 unused PackageVersion entries.
Ask: Remove these N entries?
Wait for explicit confirmation. Reasons the user might decline:
<Target> — grep won't find that..targets outside the repo.If the user asks for a diff first, produce a unified diff per file and re-prompt. Do not edit anything until confirmed.
For each unused entry, locate the exact line(s) in its source file and remove them with Edit. Preserve:
<!-- SomePackage.A: pinned for compat --> directly above the line). When in doubt, leave the comment.Multi-line entries: remove the whole block.
Do not reformat the rest of the file. The goal is the minimum change that drops the listed entries.
dotnet restoreRun from the repo root (or each affected solution root, if the repo has multiple):
dotnet restore
If restore succeeds, the cleanup is verified. Summarize what was removed and stop.
If restore fails, the cause is almost always one of:
.fsproj/.vbproj (this skill is .csproj-only — Prereq 4 should have surfaced this), an MSBuild SDK imported from outside the repo, .targets generated at build time, or props injected by another NuGet package.PackageReference items added inside a <Target> or via a conditional <ItemGroup> evaluated only at build time. Grep doesn't see those.Re-add the specific wrongly-removed entry by hand. Use git diff <file> to see what was removed and selectively restore just that line. Don't git checkout -- <file> — that discards every correctly-removed entry along with the false positive. Tell the user which package was wrongly flagged so the edge case is visible.
Directory.Packages.props. The file is curated by hand; preserve comments, grouping, and blank lines.GlobalPackageReference entries as unused. They're always used — removing one is a deliberate user decision..fsproj or .vbproj. This skill is .csproj-only by design.[xml] in PowerShell, xml.etree in Python). It loses comments and reformats whitespace. Always use line-targeted edits.dotnet restore fails after a removal, restore the entry — the failure is the signal.Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub esond/claude-skills --plugin esond