One NuGet store, byte-identical builds, no new manifest format.
PCPM is a content-addressable package manager for .NET, built on Central Package Management. It cuts the on-disk NuGet footprint by 70-90%, pins every transitive to a content hash, and ships a workspace doctor + security audit out of the box.
$ # From an empty directory…
$ pcpm init
✓ Created pcpm.json
✓ Created Directory.Packages.props
$ pcpm add Newtonsoft.Json
→ Resolved Newtonsoft.Json@13.0.3
✓ Wrote Directory.Packages.props
✓ Hardlinked to ~/.nuget/packages
$ pcpm install
→ Resolving 12 packages…
→ 11 served from store (0 KB)
→ 1 new package (387 KB) written to store
✓ dotnet restore completed in 1.4s
$ pcpm doctor
✓ CPM enabled, no floating versions
✓ Lockfile in sync, no known CVEs
✓ Workspace is clean What it does for you, concretely
Not a marketing pitch — the four things people switch to PCPM for.
-
70-90% less disk on the NuGet cache
Every distinct .nupkg is stored once in a content-addressable store and hardlinked into ~/.nuget/packages. The store does not grow with the number of projects — adding a 31st project that uses the same 200 packages adds zero bytes.
-
Reproducible builds, enforced
pcpm.lock records the resolved version and the sha256 of the .nupkg for every entry. pcpm ci refuses to proceed if the lockfile is stale, missing, or has hashes that don't match the store — a clean CI run that resolves a new transitive becomes a build failure, not a silent regression.
-
Clean CI restores that aren't slow
First install downloads the world. Every subsequent install — including on a clean CI runner with a warm cache — only fetches the deltas. The hardlink step is metadata-only, and dotnet restore sees a standard ~/.nuget/packages layout.
-
Tells you who pulled in what
pcpm why <package> walks the lockfile and shows the full chain of dependents that brought a transitive into your build. When a surprise bump shows up in CI, you know exactly which direct dependency to negotiate with.
Workspace health, before CI does it for you
PCPM ships two real diagnostic tools — a doctor for CPM hygiene, and an audit for security and licensing.
pcpm doctor
exit 1/2/0Runs a battery of checks against the current workspace and exits non-zero on real problems. Wire it into your build gate.
- ManagePackageVersionsCentrally is set
- No floating versions (1.0.*, *) in Directory.Packages.props
- No <PackageReference Version=…> left over from a pre-CPM era
- No orphaned CPM entries (declared, unused)
- No missing CPM entries (referenced, undeclared)
- Lockfile is in sync with the workspace
- Known CVEs from the NuGet feed (opt-out with --no-cve)
Exit 1 = errors, 2 = warnings, 0 = clean.
pcpm audit
SBOMScans the resolved graph against vulnerability advisories and license metadata. --output <dir> writes a CycloneDX SBOM plus a license report.
- Vulnerability scan against the NuGet advisory feed
- License detection and report (SPDX identifiers)
- CycloneDX SBOM output (--no-sbom to skip)
- JSON output mode for CI piping
Already have a .NET solution? Migrate in one command.
pcpm convert walks your solution and rewrites it in place: hoists every per-project Version= into <PackageVersion />, adopts an existing Directory.Packages.props, writes pcpm.json and pcpm.lock.
- Adopts existing Directory.Packages.props — never overwrites
- Strips Version= from <PackageReference /> in every .csproj
- --dry-run previews the diff before any change
- --revert undoes a previous convert
- --workspace covers every project in the monorepo
$ pcpm convert --dry-run
→ Will write Directory.Packages.props (new)
→ Will strip Version= from 14 .csproj files
→ Will write pcpm.json + pcpm-workspace.yaml
$ pcpm convert
✓ Created Directory.Packages.props
✓ Stripped 14 Version= attributes
✓ Wrote pcpm.json
✓ pcpm install completed in 3.1s
→ Done. Roll back with: pcpm convert --revert Everything you need, nothing you don't
Twelve verbs. No 80-flag flags, no plugin sprawl.
| Command | What it does |
|---|---|
| pcpm init | Initialise a workspace (pcpm.json, Directory.Packages.props, pcpm-workspace.yaml for monorepos). |
| pcpm add <pkg> | Add a direct dependency. --version <v> to pin, --project <path> to target one csproj, --no-install to skip the implicit install. |
| pcpm install | Resolve the transitive graph, materialise packages into the store, hardlink to ~/.nuget/packages, run dotnet restore. |
| pcpm remove <pkg> | Remove from CPM and every referencing .csproj. |
| pcpm list | Pretty-print pcpm.lock as a table. |
| pcpm why <pkg> | Show the chains of dependents that pull <pkg> into the lockfile. |
| pcpm outdated | Query the feed for newer versions, report bump type (major / minor / patch). |
| pcpm doctor | CPM hygiene, floating versions, hardcoded Version=, orphans, missing entries, lockfile sync, CVE feed. |
| pcpm audit | Vulnerability scan, license report, CycloneDX SBOM (--output <dir>, --no-sbom). |
| pcpm convert | Migrate an existing solution to CPM + pcpm. --dry-run, --revert, --workspace. |
| pcpm store status | Disk usage of the global content-addressable store. |
| pcpm ci | Strict, fail-fast install for CI — refuses if pcpm.lock is stale or hashes don't match. |
An architecture you can reason about
Three layers, each with a single responsibility, all wired through constructor injection. Zero I/O in the core, so the domain rules are testable in isolation.
pcpm.Cli
Spectre.Console.Cli commands (one per verb) — thin orchestration only, no business logic.
pcpm.Core
Domain models, abstractions, pure logic — DependencyResolver, version parsing, conflict detection. No I/O.
pcpm.Infrastructure
Implementations: NuGetFeed (raw HTTP), PackageStore (content-addressable), HardlinkCreator (Win32 P/Invoke), CpmFileService, LockfileService, ConvertService, …
Ready to give PCPM a spin?
MIT-licensed, open to contributions. The repo builds with a single dotnet build; the docs cover installation, migration, CI integration, and the full command reference.