npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

debtctl

v0.3.0

Published

Govern npm overrides, pnpm.overrides, and yarn resolutions as documented technical debt. Fails CI when overrides go stale.

Downloads

639

Readme

debtctl

CI npm version

Govern npm overrides, pnpm.overrides, and yarn resolutions as documented technical debt — with a check command that fails CI when overrides go stale.

debtctl is a small CLI that treats your npm overrides, pnpm.overrides, and yarn resolutions as what they actually are: technical debt. It maintains a sidecar file (.debtctl.json) that records why each override exists, who owns it, and when it should be revisited — then surfaces stale overrides in CI before they rot for another two years.

Philosophy

Overrides are routinely added to patch a CVE, force peer compatibility, or pin a transitive dependency. They're almost always meant to be temporary. They are almost never reviewed again.

debtctl does one job: it enforces three pieces of metadata next to every override, and fails CI when an override has drifted past its expected lifetime. No metadata, no merge. No revisit trigger, no merge. Reviewer changed the override's range and forgot to update the rationale? CI catches it.

The tool has four runtime dependencies (commander, chalk, semver, yaml) and is deliberately small. It does not patch your package.json, run installs, or talk to a registry — it only reads, classifies, and reports.

How this compares to Renovate / Dependabot

Renovate and Dependabot bump dependencies. They open PRs when a new version is available, including for overridden packages. They don't enforce why an override exists, who owns it, or when it should be revisited.

debtctl is the layer next to them, not a replacement:

  • Renovate / Dependabot open the PR that bumps the underlying package.
  • debtctl fails CI when the override pinning it has no rationale, no owner, or has drifted past its revisit trigger.

It does not call a registry, run installs, or open PRs — it reads package.json, reads .debtctl.json, and reports. Best suited for teams with a backlog of undocumented overrides who want a CI gate to stop the bleeding.

Install

Install as a dev dependency so the version is pinned in your project and CI runs match local:

npm install --save-dev debtctl
# or
pnpm add -D debtctl
# or
yarn add --dev debtctl

Then invoke via your package manager's runner:

npx debtctl init           # npm
pnpm debtctl init          # pnpm (shortcut for `pnpm exec debtctl`)
yarn debtctl init          # yarn

Or add a package.json script and call it via the manager:

{
  "scripts": {
    "debt:check": "debtctl check --strict"
  }
}
npm run debt:check

Alternatives

  • Global installnpm install -g debtctl. Convenient for ad-hoc use across many projects; not recommended for CI, since the version drifts independently from your repo.
  • No installnpx [email protected] check. Pinned to a version, fetched on demand. Fine for one-off exploration; slower on every CI run than installing.

Requires Node.js ≥ 20.

Quickstart

cd your-project
npm install --save-dev debtctl
npx debtctl init        # scaffolds .debtctl.json with TODO stubs for current overrides
$EDITOR .debtctl.json
npx debtctl check       # fails if anything is undocumented or due for review

Commit .debtctl.json to your repo. Run npx debtctl check --strict in CI.

The command examples below show the bare debtctl invocation. Prefix with npx, pnpm, or yarn per your package manager.

Commands

debtctl init

Scans your package manifest for overrides, then creates or updates .debtctl.json with stub metadata for each. Existing documented entries are preserved. Orphaned entries (sidecar entries with no matching override) are kept but counted in the summary.

$ debtctl init
Detected: npm
Found 3 overrides. 1 documented, 2 need metadata.

debtctl check

Reports overrides that are missing metadata, incomplete (still contain TODO fields), or due for review (their trigger has fired). Read-only — never modifies your sidecar.

$ debtctl check
Missing metadata (1):
  - some-package

Incomplete (1):
  - other-package: TODO fields present

Due for review (1):
  - third-package: Expired on 2025-09-01

✖ 3 problems: 1 missing, 1 incomplete, 1 due for review

Options:

| Flag | Effect | | ----------------- | -------------------------------------------------------------------- | | --strict | Escalate dueForReview from a warning to a failure. | | --json | Emit machine-readable JSON; suppress human output. | | --only <bucket> | Filter to one of missing, incomplete, dueForReview, orphans. |

Detection precedence

debtctl picks the package manager in this order:

  1. The packageManager field in package.json (e.g. "[email protected]").
  2. The lockfile present in the working directory.
  3. If multiple lockfiles are present, debtctl reports the ambiguity and falls back to a fixed priority order: npmpnpmyarn-classicyarn-berry.

| Manager | Lockfile | Override location | | -------------- | ------------------- | --------------------------------------------- | | npm | package-lock.json | overrides (in package.json) | | pnpm | pnpm-lock.yaml | pnpm.overrides and/or pnpm-workspace.yaml | | yarn-classic | yarn.lock (v1) | resolutions (in package.json) | | yarn-berry | yarn.lock (v6+) | resolutions (in package.json) |

For pnpm projects, debtctl reads overrides from both pnpm.overrides in package.json and the overrides: block of pnpm-workspace.yaml (the recommended location since pnpm 9). When the same key appears in both, the workspace YAML value wins — matching pnpm's own precedence.

Ambiguous lockfiles

When debtctl finds more than one lockfile and no packageManager field disambiguates, it prints a warning to stderr and proceeds with the highest-priority match:

Warning: multiple lockfiles found (package-lock.json, yarn.lock). Using npm. Consider removing the unused lockfile.

In --json mode the warning is suppressed and the matched lockfiles are surfaced on the result instead:

{
  "manager": "npm",
  "ambiguous": ["package-lock.json", "yarn.lock"],
  "entries": [...],
  "orphans": [...]
}

Ambiguity never affects the exit code — it's repo hygiene, not override debt.

Metadata model

.debtctl.json is a versioned JSON file with one entry per override. Three trigger types are supported (version-anchor, date, patch-hash):

{
  "version": 2,
  "overrides": {
    "some-package": {
      "reason": "Patches CVE-2024-XXXX until upstream v3 ships",
      "owner": "team-security",
      "revisitWhen": {
        "type": "version-anchor",
        "package": "some-package",
        "declaredRange": "^2.4.0"
      }
    },
    "other-package": {
      "reason": "Force peer compatibility with legacy-ui",
      "owner": "alice",
      "revisitWhen": {
        "type": "date",
        "expires": "2026-09-01"
      }
    }
  },
  "patches": {}
}

Sidecar files written by older versions of debtctl ("version": 1) are auto-migrated to v2 on first read. Your existing file on disk is left untouched until you re-run debtctl init.

version-anchor (recommended for overrides)

Fires when the declared range for the named package in package.json no longer matches declaredRange. This is usually what you want for overrides: if upstream releases a fix and someone bumps the dependency, you'll be prompted to revisit the override automatically — no human-set deadline required.

Ranges are compared semantically via semver.subset, so ^1.0.0 and ^1.0.0 match, but ^1.0.0 and ^2.0.0 don't. Non-semver ranges like latest or workspace:* fall back to string equality.

date

Fires when today is on or after expires. Use this only when there's no natural dependency or patch file to anchor against (e.g., you're waiting on an external schedule).

patch-hash (recommended for patches)

Fires when the patch file's content hash differs from the recorded hash. See Patches for the full story.

Patches

debtctl also governs code patches managed by patch-package, pnpm's patchedDependencies, and yarn berry's patch: protocol. Patches are arguably worse than overrides for rot: they include explicit code diffs that can fail to apply silently when upstream shifts, or apply wrongly and quietly diverge from the patch's original intent.

Patches share the same metadata shape as overrides (reason, owner, revisitWhen) and live under the patches key of .debtctl.json. debtctl init detects patches in your project, computes a SHA-256 hash of each patch file, and scaffolds an entry with a patch-hash trigger pre-armed at the current content.

| Manager | Where patches are declared | Where patch files live | | -------------- | -------------------------------------------- | ---------------------------------------------- | | npm | package.json scripts.postinstall | patches/*.patch | | yarn-classic | package.json scripts.postinstall | patches/*.patch | | pnpm | pnpm.patchedDependencies in package.json | path declared per entry (typically patches/) | | yarn-berry | resolutions field with patch: protocol | .yarn/patches/*.patch |

Example sidecar with both an override and two patches:

{
  "version": 2,
  "overrides": {
    "some-package": {
      "reason": "Patches CVE-2024-XXXX until upstream v3 ships",
      "owner": "team-security",
      "revisitWhen": {
        "type": "version-anchor",
        "package": "some-package",
        "declaredRange": "^2.4.0"
      }
    }
  },
  "patches": {
    "react-router": {
      "reason": "Backport upstream PR #1234 until v6.20 ships",
      "owner": "team-frontend",
      "revisitWhen": {
        "type": "patch-hash",
        "hash": "sha256:abc123def456..."
      }
    },
    "legacy-thing": {
      "reason": "Block calls to deprecated endpoint until migration completes",
      "owner": "alice",
      "revisitWhen": {
        "type": "date",
        "expires": "2026-12-01"
      }
    }
  }
}

The patch-hash trigger

debtctl init records the SHA-256 hash of each patch file at the moment it's detected. debtctl check recomputes the hash on every run and fires the trigger if it has drifted. This is the killer feature: if a developer edits a patch file without also updating the metadata, CI catches it.

debtctl does not automatically update the stored hash on subsequent init runs. The hash is your snapshot — when the trigger fires, you should review what changed in the patch, update the reason if needed, and then update the hash to the new value to "acknowledge" the change.

Line endings: patch contents are normalized to LF (\n) before hashing. This means a patch file checked out on Windows (CRLF) and macOS/Linux (LF) produces the same hash. Without this, every Windows contributor would see all patch triggers fire on every install.

Out of scope (for now)

  • Workspace-sourced yarn berry patches (patch:.../workspace#...) — only npm-sourced patches stored in .yarn/patches/ are detected.
  • Inline .pnp.cjs diffs.
  • Auto-applying or auto-fixing patches — debtctl is strictly read-only.

CI usage

With debtctl installed as a dev dependency, run check --strict after npm ci (or the equivalent for your manager). Example for GitHub Actions:

- uses: actions/checkout@v4
- uses: actions/setup-node@v4
  with:
    node-version: 20
- run: npm ci
- name: Verify dependency overrides
  run: npx debtctl check --strict

Pinning debtctl in devDependencies keeps the version stable across CI runs and matches whatever you use locally.

Exit codes are designed for CI:

| Code | Meaning | | ---- | ------------------------------------------------------------------------------------------ | | 0 | Clean, or only warnings present without --strict. | | 1 | Errors found: missing or incomplete metadata, or (with --strict) due-for-review entries. | | 2 | Misuse: invalid --only value, or check run before init. |

Orphans never cause a non-zero exit; they're informational.

Status

debtctl is at 0.3.0. The metadata schema is at version 2; v1 sidecars are auto-migrated on read. Future schema changes will bump it explicitly. Bug reports and PRs welcome.

License

MIT