Finding unmaintained dependencies is the easy part. Removing them is where it gets hard.
This is the story of how we slimmed down vuls, the open-source vulnerability scanner I created — what didn't work, what did, and the tooling we built after doing it all by hand.
The easy win: 15 minutes
mitchellh/go-homedir has been archived for years. Vuls used it in a couple of lines in two source files:
- import "github.com/mitchellh/go-homedir"
- dir, err := homedir.Dir()
+ dir, err := os.UserHomeDir()
Fifteen minutes, start to finish. The standard library has done this since Go 1.12.
Trivy used the same package, so we sent them a similar fix. The Trivy maintainers reviewed and merged it within three weeks.
We felt good. Let's keep going.
The part where almost nothing happened
We kept going. Over two weeks we replaced 9 direct dependencies with standard-library code or a few lines of our own. go.mod went from 60 direct dependencies to 51.
Then we looked at what had actually left the build. go.sum went from 1,220 lines to about 1,210. Only 5 modules disappeared completely. Most of the packages we had stopped calling were still required by other modules, so they stayed in the dependency list anyway.
Adding a dependency takes one line. Removing one can take real work and barely change what you ship.
Just like a real diet.
What worked: removing a whole layer
So we stopped picking off packages one by one and looked at which layer they came in through.
Vuls uses Trivy's parsers to read lockfiles (package-lock.json, go.mod, Cargo.lock, and so on). We called them through fanal, Trivy's analysis framework. fanal is built for everything Trivy scans — container images, disk images, filesystems, infrastructure-as-code. That breadth is exactly what Trivy needs. Our scanner only needed the part that reads lockfiles, but the way we had wired fanal in registered 25+ analyzers through blank imports, and with them the infrastructure-as-code and misconfiguration packages that Trivy needs and we never called.
We changed Vuls to call Trivy's lockfile parsers directly. Much less code compiled in, and — as we checked below — the same results on almost every project we tested. (Vuls still depends on Trivy — we use its parsers, just not through fanal.)
The result (vuls PR #2476):
| Metric | Before | After | Change |
|---|---|---|---|
| Scanner binary | 106.6 MB | 34.1 MB | -68% |
| Scanner build time | 7.8 s | 3.2 s | -59% |
How we measured: scanner-only builds (-tags scanner) with our release build settings, CGO_ENABLED=0 GOEXPERIMENT=jsonv2 go build -a -trimpath -ldflags "-s -w". The build times are with a warm build cache.
To be precise about what shrank: this is the scanner binary. The full vuls binary, which also includes the vulnerability detection and reporting code, went from 139.1 MB to 138.5 MB after this change and a follow-up one. That code still uses Trivy's vulnerability database layer, so most of the weight stays.

Packages in the scanner build counted with go list -tags scanner -deps ./cmd/scanner before and after PR #2476.
Don't pick leaves one by one. Find the branch they hang from.
How we tested it
The change rewrote how the whole scanner package reads lockfiles. Here's how we checked it.
Layer 1: Golden tests. JSON output for 35 lockfile types, compared for an exact match.
Layer 2: A/B comparison on real projects. We collected real lockfiles from 129 open-source projects across 13 ecosystems. Then:
old code + lockfile → result A
new code + lockfile → result B
diff(A, B)
No mocks. No hand-written expected values. Just: "does the new code produce the same results as the old code?"
127 of 129 were identical. The other 2 differed only in the pnpm dev flag, which our old wrapper code had been losing along the way. Calling Trivy's pnpm parser directly gave us the parser's correct output.
Building the comparison set taught us something too. On the first pass, many fixtures produced zero packages — requirements.txt files with no pinned versions, a nearly empty yarn.lock, a root pom.xml with only <modules>. "Identical" on an empty result proves nothing, so we replaced those fixtures with ones that actually exercised the parsers.
This lives in vuls as make compare-lockfile. It didn't catch everything: code review and testing on Windows still found a case the comparison had missed (.exe files weren't detected as executables), which we fixed before merging.
The tool we built afterwards: uzomuzo diet
We did all of the above by hand, in March 2026. Looking back, the expensive part wasn't writing code. It was deciding where to start — and our first choice was wrong.
So in April we added a diet command to uzomuzo, our dependency lifecycle scanner. It ranks your direct dependencies as candidates for removal:
- Dependency graph — from a CycloneDX SBOM: what would disappear along with each direct dependency?
- Source coupling — tree-sitter reads your code: how many files import it, how many call sites?
-
Health — maintenance status (Active, Stalled, Archived, ...), from the same signals
uzomuzo scanuses - Score — high impact, high risk, and easy to remove rises to the top
scan and diet are rule-based. No LLM is involved. Their results reflect the dependency and maintenance data available when you run them.
One caveat up front: diet is experimental. An audit against hand-checked expected values found cases where a dependency that is in use is reported as unused or trivial — for example when it's imported from a skipped directory or in an import style the analyzer doesn't recognize yet. The CLI prints a notice saying so after every run, and the Accuracy section of the docs lists what to check. Treat its output as a list of places to look, not a list of things to delete.
Here's an excerpt from a run on Terraform (commit 9ec49e0, October 13, 2026; SBOM generated with cyclonedx-gomod; run on October 26, 2026):
$ uzomuzo diet --sbom terraform-bom.json --source ./terraform
RANK SCORE EFFORT PURL REMOVES IMPORTS CALLS STATUS
3 0.13 trivial github.com/mitchellh/gox 1 1 1 Archived
5 0.10 hard github.com/mitchellh/colorstring 0 22 79 Archived
6 0.10 moderate github.com/mitchellh/go-homedir 0 9 11 Archived
...
72 0.00 hard github.com/hashicorp/hcl/v2 0 352 6764 Active
(Edited for width: four of the 77 direct dependencies, PURLs shortened to module paths, and the REMAINS column omitted. Ranks 1, 2 and 4 are not shown: 1 and 4 are Terraform's own nested internal/backend/remote-state/* modules, which the SBOM lists as direct dependencies, and 2 is an active OpenTelemetry package. The full run also reported zero "quick wins".)
The ranking is a starting point for investigation, not a verdict:
-
goxis archived and has one import and one call site, so it gets atrivialeffort label. That's exactly the kind of label to verify: check that one call site, what it does, and what replaces it. -
go-homediris the same package we removed from vuls in 15 minutes. Here it's imported in 9 files, so it's rated moderate. Removing the direct use wouldn't drop it from Terraform's dependency list either — another module still requires it. -
hcl/v2is at rank 72: 352 importing files, 6,764 call sites, and actively maintained. Don't start here.
It works for Go, Python, JavaScript/TypeScript, and Java.
The last mile: Claude Code skills
Ranking is automated. Actually removing a dependency — tracing what data flows through it, writing the replacement, getting the change accepted — still takes judgment. uzomuzo ships three Claude Code skills for that part:
/diet-assess-risk (how risky is it to keep?), /diet-evaluate-removal (is it worth the effort?), and /diet-remove (draft the change or the upstream report). They use an LLM, so review what they propose before you use it.
The rules in /diet-remove come from our own mistakes. When we found that Next.js still depended on @vercel/kv, which Vercel has deprecated on npm in favor of Upstash Redis, we tried three channels by hand:
- PR → we closed it ourselves after CI failed. It's a large pnpm-workspaces monorepo, and we couldn't reproduce its CI locally.
- Issue → the repository's bot closed it within seconds, pointing to the bug report template. The issue tracker is for bugs with a reproduction, and ours wasn't one.
- Discussion → the channel the repository's templates point to. It's still open with no reply yet.
So the skill now defaults to drafting an issue rather than a PR for projects you don't own, checks whether the repository accepts blank issues, and falls back to a Discussion when it doesn't. Those rules are written from that manual experience; we haven't yet published an end-to-end run of the skill itself. And getting the channel right doesn't guarantee a response. Of our four upstream reports in April, two have been fixed and two are still waiting. Whether to fix something is the maintainers' call.
Start your diet
go install github.com/future-architect/uzomuzo-oss/cmd/uzomuzo@latest
CGO_ENABLED=1 go install github.com/future-architect/uzomuzo-oss/cmd/uzomuzo-diet@latest # needs a C compiler
export GITHUB_TOKEN=... # read-only is enough; gives uzomuzo repository signals such as archived status
The output in this post was produced with v0.10.0.
- Run
uzomuzo scan --file go.mod(or pipe in an SBOM for other ecosystems). See what's unmaintained. - Generate a CycloneDX SBOM — for example
trivy fs . --format cyclonedx -o bom.json, or for Go,cyclonedx-gomod mod -json -output bom.json— and runuzomuzo diet --sbom bom.json --source .. - Pick a dependency whose use and replacement you understand. Remove it, then measure: binary size, dependency list, build time. If nothing moved, look for the layer it came in through.
And don't give up on your diet.
Earlier posts: "Your dependencies are 48% unmaintained — and SCA tools can't see it" and "Your CI runs other people's code. Is anyone still maintaining it?"
Source and docs: github.com/future-architect/uzomuzo-oss (Apache 2.0)
Kota Kanbe — Creator of vuls, an open-source vulnerability scanner.
Top comments (1)
The contrast between the nine dependency swaps and the single layer removal is striking. Since the diet ranking flags a dependency as trivial based on call sites, how do you guard against removing something that is rarely called but critical when it is used?