Diet Remove: $ARGUMENTS
Analyze and plan the removal of dependency $ARGUMENTS, then take action.
Mode selection
- Default (Issue mode): Run Phase 1 analysis, then file a GitHub Issue with the findings and proposed migration plan. Appropriate for external OSS contributions and large projects where you don't own the build environment.
--pr(PR mode): Run the full removal lifecycle locally: analysis → replacement → verification → commit. Use this only when you own the project and can run build/test locally.
Parse $ARGUMENTS for flags:
- If
--pris present → PR mode (direct implementation) - If
--repo owner/repois present → target that repository for the issue - Otherwise → Issue mode (default)
When to use: After /diet-evaluate-removal confirms the dependency is worth removing, or when uzomuzo diet ranks it as trivial/easy.
Safety principle: Every removal must pass build + vet + test before committing. If any step fails, stop and diagnose — don't force it.
Ecosystem detection
Detect the ecosystem from the PURL scheme or module path (pkg:golang/ → Go, pkg:npm/ → npm, pkg:pypi/ → Python, pkg:maven/ → Maven, pkg:githubactions/ → GitHub Actions).
If the ecosystem is not Go, display this notice to the user before proceeding:
Note: This skill's PR-mode commands and common patterns are optimized for Go. Issue mode and IBNC safety checks work for any language, but ecosystem-specific PR-mode guidance (verification commands, edge cases, lockfile handling) is still being developed for {ecosystem}. If you discover surprises or improvements during this removal, please contribute them via an issue or PR to future-architect/uzomuzo-oss.
Then continue with the rest of the flow — the analysis, IBNC checks, and issue template are language-agnostic.
GitHub Actions detection
If the target PURL starts with pkg:githubactions/ or is a GitHub Action name (owner/action):
- Find usage:
grep -rn "{action-name}" .github/workflows/ --include="*.yml" --include="*.yaml" - Assess impact: Count workflow files and jobs affected. Note whether release-critical pipelines are involved.
- Find replacement: Check if an official replacement, maintained fork, or first-party alternative exists (e.g.,
tibdex/github-app-token→actions/create-github-app-token). - Pin strategy: Replacement should use SHA pins with version comments (e.g.,
uses: actions/checkout@<sha> # v4.2.0).
The rest of the flow (duplicate check, issue template, etc.) proceeds identically. In the Usage breakdown table, list workflow files instead of source files.
Phase 1: Pre-flight checks
Before writing any code, run through these checks:
1. Will it actually disappear?
Check the STAYS column in the uzomuzo diet output (or stays_as_indirect in JSON):
- STAYS =
-→ Removing this dep fully removes it from the dependency tree. Go ahead. - STAYS =
yes→ Another direct dep depends on this transitively. It will remain as an indirect dependency after removal. Still worth doing (version management delegation, future removal readiness), but set expectations: it won't leave go.sum / lockfile.
In detailed output, the IndirectVia field shows exactly which direct deps pull it in transitively. These are the upstream targets for Phase 5 (Upstream Diet).
If diet output is not available, you can verify manually:
# Go
go mod why -m $ARGUMENTS
# npm
npm ls $ARGUMENTS
# pip
pip show $ARGUMENTS | grep "Required-by"
2. What's the replacement?
Determine the replacement strategy. In order of preference:
| Strategy | When to use | Example |
|---|---|---|
| Delete | Unused (0 imports) | Remove from go.mod/package.json, run tidy |
| Standard library | stdlib equivalent exists | go-homedir → os.UserHomeDir() |
| Consolidate | Another dep already does this | Two JSON libs → keep one |
| Self-implement | Small, non-crypto, well-defined API | lfshook → 20-line logrus.Hook impl |
| Submodule isolate | Used only in one subcommand/tool | gosnmp → contrib/snmp2cpe/go.mod |
| Framework peel | Dep comes via a framework you don't fully need | Trivy fanal → direct parser calls |
NEVER self-implement: crypto, TLS, protocol negotiation, auth token handling, or anything where subtle bugs create security vulnerabilities.
3. Are there hidden complications?
Check these before starting:
API leakage: Does this dependency's types appear in exported identifiers? If yes, removal is a breaking change — needs major version bump or deprecation period.
# Go: search for exported identifiers using the dep's types grep -rn "func.*$ARGUMENTS\|type.*$ARGUMENTS" --include="*.go" | grep -v _test.go | grep "^[A-Z]"Build tags: Is the import behind a build tag? (e.g.,
//go:build jsoniter) If yes, the dep may not affect default builds — consider just deleting the tagged file.Generated code: Files with
// Code generatedheaders are trivially migrated by re-running the generator with the replacement tool.Blank / side-effect imports: See the IBNC checklist in step 4 below.
4. IBNC safety check (imports-but-no-calls patterns)
If the dependency shows 0 call sites but >0 import files, it may still be required. Verify it is not:
- Side-effect import (
import _ "pkg",import 'pkg',require('pkg')without assignment) - Database / driver registration (blank import or conditional
require()) - Config-driven plugin (eslint, tailwind, babel, postcss — referenced in config files, not imports)
- Framework DI / decorator (
@Entity,@Autowired,extends Framework, Ember DI) - Annotation-only usage (Java:
@NotNull,@JsonProperty— the annotation is the usage) - Type-only / constant-only package (imported for types or constants, zero function calls)
- Delegated composition (called indirectly through SDK wrappers or framework context objects)
If any apply, the dependency is not safe to remove even if call-site analysis shows 0 calls. See docs/ibnc-patterns.md for the full pattern taxonomy with evidence from 79+ OSS projects.
5. SBOM tool awareness
Note which SBOM tool (trivy/syft/cdxgen) and version generated the dependency data. Tool choice can produce 10-20x variance in dependency counts for the same project, affecting which dependencies appear and their coupling scores. If the dep count seems unexpectedly low, cross-check with a different tool.
Issue mode (default): File a GitHub Issue
Step 0: Duplicate check (MANDATORY)
Before filing anything, search for existing issues and discussions. GitHub search is word-level tokenized — not semantic — so run multiple queries with different phrasings to reduce false negatives:
# Search by package name (exact)
gh search issues "{dependency}" --repo {owner/repo} --limit 10
# Search by replacement package name
gh search issues "{replacement}" --repo {owner/repo} --limit 10
# Search by keywords describing the change
gh search issues "replace deprecated {short-name}" --repo {owner/repo} --limit 10
# Search discussions (same queries)
gh api graphql -f query='{ search(query: "repo:{owner/repo} {dependency} type:discussion", type: DISCUSSION, first: 10) { nodes { ... on Discussion { title url } } } }'
Example for @vercel/kv:
gh search issues "@vercel/kv" --repo vercel/next.js --limit 10
gh search issues "@upstash/redis" --repo vercel/next.js --limit 10
gh search issues "replace deprecated kv" --repo vercel/next.js --limit 10
Post-filter: GitHub fuzzy search can return false positives. Verify that each hit is actually about the same dependency removal — not just a mention in passing.
If a matching issue/discussion already exists, do not file a duplicate. Instead, add a comment with any new analysis (e.g., impact data from diet) and stop.
Step 1: File the issue or discussion
After completing Phase 1, stop and file an issue instead of implementing. This is the default because:
- External contributors cannot run CI or regenerate lockfiles
- Maintainers need context to evaluate the change
- Large monorepos have project-specific build/test requirements
Issue template
Use gh issue create with the following structure:
Title: dep: replace EOL {dependency} with {replacement}
Body:
## Problem
`{dependency}` is {lifecycle status}.
{1-2 sentences on why this matters — security risk, no more patches, etc.}
## Impact analysis
- **Detected by**: [uzomuzo diet](https://github.com/future-architect/uzomuzo-oss) with {sbom-tool} {version}
- **Files**: {N} files import this dependency
- **Call sites**: {N} calls across {N} APIs
- **Exclusive transitive deps**: {N} (removed together)
- **Stays as indirect**: {yes/no}
- **Difficulty**: {trivial/easy/moderate/hard}
### Usage breakdown
| File | Usage | Category |
|------|-------|----------|
{table of files and how they use the dependency}
## Proposed replacement
{replacement} — {why this is the right alternative}
### API mapping
| Current | Replacement |
|---------|-------------|
{API-level migration table}
### Environment variable changes
{any env var renames needed, or "None"}
## False-positive risk
{If the dependency matches an IBNC pattern (side-effect import, config-driven plugin, framework DI, etc.), note it here. If none apply, write "None — all usage is via direct function calls."}
## Cross-project context
{If the same dependency is known to be EOL/archived in other major OSS projects, note it here. E.g., "mitchellh/go-homedir is archived and also affects Trivy, Terraform, Vault, and MinIO." If no cross-project data is available, write "No cross-project data available."}
## Notes
- {any hidden complications from Phase 1 step 3}
- {API leakage? build tags? generated code?}
Choosing the right channel
Before filing, check the target repository's issue templates:
- Run
ls <repo>/.github/ISSUE_TEMPLATE/or checkconfig.ymlforblank_issues_enabled - If
blank_issues_enabled: falseand only bug/docs templates exist, the project likely uses Discussions for proposals. File in theIdeascategory instead:# Use GitHub Discussions when issues require a specific template gh api graphql -f query='mutation { createDiscussion(input: { repositoryId: "...", categoryId: "...", title: "...", body: "..." }) { discussion { url } } }' - If blank issues are enabled or a "feature request" template exists, use
gh issue create
After filing
Do not proceed to implementation. The issue/discussion is the deliverable.
Follow-up guidance:
- If no maintainer response after 2 weeks, add a polite ping comment.
- If the maintainer responds with "PR welcome", you may re-run with
--prto implement (but check if you can reproduce CI locally first). - If the same dependency is EOL across multiple projects (e.g., mitchellh/* packages), cross-reference the issues in each body so maintainers see the ecosystem-wide pattern.
- If the dependency would benefit from structural reform rather than individual removal (e.g., a framework that pulls in many EOL deps), mention this in the Notes section — the insight is valuable even if you can't implement it yourself.
If --pr was specified, skip this section and continue to Phase 1.5 below.
PR mode (--pr): Direct implementation
The following phases apply only in PR mode. Use this when you own the project.
Phase 1.5: Test coverage check — before you touch anything
Before writing any replacement code, check if the code that uses this dependency has tests.
# Find all files importing the dependency (production code only)
grep -rn "$ARGUMENTS" --include="*.go" -l | grep -v _test.go
# For each file, check if a corresponding test file exists
# e.g., reporter/email.go → reporter/email_test.go
If tests exist: You're safe to proceed
The existing tests define the expected behavior. After replacement, run them — if they pass, the replacement is correct.
If tests DON'T exist: Write tests FIRST, before changing anything
This is the most important step in the entire process. Write tests against the current (working) implementation before replacing it. This gives you a safety net that catches behavior differences in the replacement.
- Identify the contract: What does the code do with this dependency? What are the inputs and outputs?
- Write tests that capture current behavior:
- Normal cases (happy path)
- Edge cases specific to the dependency's behavior (e.g., how does it handle nil? empty input? unicode?)
- Error cases (what happens when the dependency returns an error?)
- Run the tests against the current code — they must pass before you change anything
- Then proceed to Phase 2
Why before, not after? If you write tests after replacing the code, you're only testing that your new code does what you think it should do — not what the old code actually did. Behavior differences slip through.
Real example: c-robinson/iplib handled IPv4 /31 and /32 CIDR prefixes differently from net/netip. If tests had been written after the replacement, the edge case would have been missed because the test would match the new (wrong) behavior.
For framework peels: Build a regression harness
For high-impact removals (framework replacement, parser rewrite), unit tests aren't enough. Build a comparison harness:
# Build before and after binaries
git stash && go build -o /tmp/before ./cmd/... && git stash pop
go build -o /tmp/after ./cmd/...
# Run both against real-world inputs and diff the output
/tmp/before < input.json > /tmp/out-before.json
/tmp/after < input.json > /tmp/out-after.json
diff /tmp/out-before.json /tmp/out-after.json
The vuls fanal framework removal used this approach: 17 real OSS lockfiles, 7,198 libraries compared — found 1 legitimate difference (a pnpm bug fix).
Phase 2: Implementation
Step 1: Create the replacement
Based on the strategy from Phase 1:
For stdlib replacement:
- Find all import sites:
grep -rn "$ARGUMENTS" --include="*.go" | grep -v _test.go - For each site, replace the API call with the stdlib equivalent
- Update imports
- If the replacement API has different error handling or return types, adapt the call site
For self-implementation:
- Write the replacement in the same package that uses it (don't create a shared utility for a single use site)
- Keep it minimal — match only the API surface actually used, not the full library
- Write tests that cover the same behavior as the original
For submodule isolation:
- Create
contrib/<tool>/go.modwith its own module path - Move the relevant code under
contrib/<tool>/ - Watch for imports back to the root module (especially
version.go,configpackages) - Add
go.workif needed for local development
For framework peel:
- Identify which specific functions you actually call through the framework
- Call them directly, bypassing the framework's registration/discovery layer
- This is the highest-effort strategy but has the highest payoff
- Build a comprehensive regression test BEFORE starting (golden files, A/B comparison)
Step 2: Handle edge cases
Lessons learned from real dependency removals:
Mechanical replacements aren't fully mechanical.
xerrors→fmt.Errorflooked like a sed job, but 10 of 788 call sites had edge cases:[]errorpassed to%w(needserrors.Join)- Non-error types passed to
%w(needs%v) - Existing bugs hidden by the old library's lax type checking
Check for behavior differences in the stdlib equivalent:
net/smtpis frozen but not deprecated — safe to use- IPv4 network/broadcast address handling differs between libraries
tls.Dial+smtp.NewClientis not the same as a library'sDialTLS
Linter rules may change: Switching from
xerrors.Newtoerrors.Newmay triggerreviverules about error message capitalization that the old library was exempt from.
Step 3: Update dependency manifest
For Go:
# Remove the direct dependency
go mod edit -droprequire $ARGUMENTS
go mod tidy
For npm: Remove from package.json, run npm install or equivalent.
For Python: Remove from requirements.txt / pyproject.toml, run pip install.
For Maven: Remove from pom.xml.
Monorepo warning: In monorepos with workspace-managed lockfiles (pnpm, yarn workspaces, Gradle multi-module), the lockfile must be regenerated in the monorepo environment. If you cannot run the package manager locally, use Issue mode instead of PR mode — the next.js @vercel/kv removal failed CI entirely because pnpm-lock.yaml was not regenerated.
Phase 3: Verification
All three must pass. No exceptions.
# 1. Build
go build ./...
# 2. Static analysis
go vet ./...
# 3. Tests
go test ./... -count=1
For non-Go projects, run the equivalent build + lint + test pipeline.
Additional verification for high-impact removals:
Check go.sum reduction:
wc -l go.sum # compare with beforeIf go.sum didn't shrink, the dependency remains as indirect.
Check binary size (for compiled languages):
go build -o /tmp/binary-after ./cmd/... ls -la /tmp/binary-afterFor framework peels: A/B comparison Build both old and new versions, run them against real-world inputs, diff the outputs. This catches subtle behavior changes that unit tests miss.
Phase 4: Commit
Create a focused commit with clear metrics:
fix: remove {dependency} — replace with {replacement}
{What was done and why}
Before: {N} direct deps, go.sum {N} lines
After: {N} direct deps, go.sum {N} lines
Binary: {size before} → {size after} ({reduction}%)
Phase 5: Upstream Diet — when the dep stays as indirect
If uzomuzo diet showed STAYS = yes for this dependency (or you see it still in go.sum / lockfile after removal), it remains as an indirect dependency.
The diet detailed output's IndirectVia field already tells you exactly which direct deps pull it in. Use this to plan upstream removals.
Is the upstream under your control?
If the upstream is your organization's repo (e.g., your company's other OSS projects):
- Submit the same removal PR to that upstream repo
- Wait for it to be merged and tagged
- Update your
go.modto the new version:go get -u <upstream>@latest - Run
go mod tidy— now it should disappear fromgo.sum
Real example: Removing go-homedir from vuls required PRs to 6 upstream repos (go-exploitdb, go-kev, gost, go-cti, go-msfdb, go-cve-dictionary). All used the identical os.UserHomeDir() replacement. Only after all 6 were merged and tagged did go-homedir finally leave vuls's go.sum.
If the upstream is a third-party project you don't control:
- You can submit a PR, but you can't control the timeline
- In the meantime, the removal from your direct deps still has value:
- Version management is delegated to the upstream
- Your code no longer calls the dependency directly
- You're ready for instant cleanup when the upstream removes it
- Note this in the commit message: "Direct usage removed. Remains as indirect via X."
Don't stop at one repo
If you manage multiple repos that share the same dependency, plan the removal across all of them:
Week 1: Remove from leaf repos (no downstream dependents)
Week 2: Tag releases of leaf repos
Week 3: Update parent repos to new versions, remove there too
Week 4: Tag releases, final go mod tidy in the top-level repo
Phase 6: Step back — individual removal vs structural reform
After removing a few dependencies individually, check the cumulative effect:
# Compare with the original baseline
wc -l go.sum # did it actually shrink meaningfully?
go build -o /tmp/bin . && ls -la /tmp/bin # did binary size change?
The "9 deps removed, binary unchanged" lesson
In the vuls Code Diet project, removing 9 individual dependencies barely changed the binary size. The dependencies were gone from go.mod, but their code was still pulled in transitively through a framework layer (Trivy's fanal).
The breakthrough came from removing the framework itself — replacing the fanal analyzer registration with direct parser calls. That single change dropped the scanner binary from 106.6 MB to 34.1 MB (-68%).
When to shift from individual removal to structural reform
| Signal | Action |
|---|---|
| go.sum keeps getting longer despite removals | Check for a framework pulling everything back in |
| Binary size doesn't change after 3+ removals | Look for a shared layer that imports the deps transitively |
| Multiple deps serve the same framework | Remove the framework, not the individual deps |
diet shows many deps with 0 ONLY-VIA-THIS |
They're all shared through a common layer — peel the layer |
Structural reform approaches:
- Framework peel: Call specific functions directly instead of going through a plugin/registration layer
- Reporter/plugin extraction: Move optional integrations to submodules with their own
go.mod - Binary split: Separate CLI subcommands into separate binaries (git-style delegation — like
uzomuzo-dietitself)
Common patterns (from real removals)
| Dependency | Replacement | Effort | Surprise |
|---|---|---|---|
mitchellh/go-homedir |
os.UserHomeDir() |
15 min | None — simplest possible removal |
rifflock/lfshook |
20-line logrus.Hook impl |
30 min | Test was 131 lines (file I/O) |
samber/lo (UniqBy) |
Per-package uniqBy helper |
30 min | Better to duplicate than abstract |
golang.org/x/oauth2 |
10-line bearerTransport |
30 min | Just a RoundTripper wrapper |
golang.org/x/xerrors |
fmt.Errorf + errors |
2 hr | 10 edge cases in 788 call sites, found existing bug |
gosnmp/gosnmp |
Submodule contrib/snmp2cpe/ |
1 hr | version.go imported root module |
emersion/go-smtp |
net/smtp + LOGIN auth |
1.5 hr | TLS connection setup differs |
c-robinson/iplib |
net/netip CIDR enumeration |
1 hr | IPv4 network/broadcast edge cases |
| Trivy fanal framework | Direct parser calls | 2 days | -68% binary size, found pnpm bug |
Language coverage: PR mode commands and common patterns are currently Go-centric, built from real removal experience. Issue mode and IBNC checks are language-agnostic. PRs adding equivalent PR-mode guidance for npm, pip, Maven, or other ecosystems (backed by actual removal data) are welcome.
Primary Source Links
All generated content (commit messages, issues, PRs, discussions) MUST include primary source links so reviewers can verify claims independently.
Required links
| Claim | Required link |
|---|---|
| "Dependency X is Archived/Deprecated" | Registry page (npm/pkg.go.dev/PyPI/Maven Central) showing the status |
| "Replace with Y" | Registry page for the replacement package |
| "Official migration guide available" | Direct URL to the migration doc |
| "Has known vulnerability" | CVE/Advisory URL (NVD, GitHub Advisory, or OSV) |
PURL → Registry URL
| Ecosystem | URL pattern |
|---|---|
pkg:npm |
https://www.npmjs.com/package/<name> |
pkg:golang |
https://pkg.go.dev/<namespace/name> |
pkg:pypi |
https://pypi.org/project/<name> |
pkg:maven |
https://central.sonatype.com/artifact/<namespace>/<name> |
Example commit message with sources
fix: remove @vercel/kv — deprecated, replace with @upstash/redis
@vercel/kv is officially deprecated (https://www.npmjs.com/package/@vercel/kv).
Source removed from vercel/storage repo on 2026-01-23.
Replacement: @upstash/redis (https://www.npmjs.com/package/@upstash/redis)
Before: 321 direct deps
After: 320 direct deps
Example issue body with sources
When filing an issue (default mode) or PR, include:
- Link to the registry page showing deprecation/archived status
- Link to the replacement package's registry page
- Link to migration guide if one exists
- Link to the GitHub repo showing archived banner (if applicable)
Important rules
- One dependency per commit. Don't bundle multiple removals — if one breaks, you can revert cleanly.
- Don't over-engineer the replacement. Match only the API surface actually used. Three similar lines > premature abstraction.
- Run the full test suite, not just affected packages. Transitive effects are real.
- If
go mod tidyre-adds the dependency, something still imports it. Usego mod why -mto find out what. - Measure before AND after. go.sum line count, binary size, build time. Put the numbers in the commit message.
- If it's harder than expected, stop and reassess. The removal may not be worth the effort — that's a valid conclusion.
- Always include primary source links. Never claim "deprecated" or "archived" without evidence URLs.