depcurfew v1.6.1

depcurfew

One supply-chain security policy for npm, pnpm, Yarn, Bun, Deno, uv, pip, Poetry, PDM, Pipenv, Composer, Bundler, Cargo, Renovate and Dependabot, and a doctor that finds configuration that silently does nothing.

A sibling of agentcurfew: agentcurfew guards the files that steer AI coding agents, depcurfew guards the packages that people and agents install. Documentation: depcurfew.github.io.

Start in five minutes GitHub npm

On this page

Why

Package managers and update bots now each protect you against freshly published malicious versions: a release-age delay, an allow-list for install scripts, limits on git and tarball dependencies. Each tool names these settings differently, counts time in its own unit, reads them from its own files and supports them only from some version on. A setting can also look right and do nothing, for example because CI pins an older version of the package manager, or because a bot proposes versions the package manager would refuse.

depcurfew keeps one policy file, depcurfew.json, writes it into the configuration of every tool in the repository, and checks that the result really works.

What it stops and finds:

  • Versions that are too new. Every package manager depcurfew configures and every update bot waits cooldown.minimum before it takes a new version, each in its own setting, unit and file; pip waits where its environment sets the delay, which apply names. For the ecosystems whose package manager depcurfew does not configure, only the update bots wait.
  • Install scripts of dependencies that the policy does not allow, and Composer plugins.
  • Git and tarball dependencies where the policy denies them.
  • Look-alike and made-up names. depcurfew check, the Claude Code hook and the MCP server stop a name one or two letters away from a popular npm package or PyPI project, the usual form of typosquatting and of package names an AI model invents.
  • Known malicious versions. After depcurfew osv update, the same checks and doctor block the versions OSV.dev lists as malicious, offline, without sending a package name anywhere: see Known malicious versions.
  • Settings that do nothing. doctor finds a pinned package manager too old for a setting, a bot that waits less than the package manager, a CI job or Dockerfile that installs with another version, and more: see doctor rules.

Five-minute start

  1. npx depcurfew doctor works before init: without depcurfew.json it compares the repository with the policy init would write and shows, as notes in a block of their own, what init and apply would change, for example a package manager that installs new versions without a release-age delay, with the settings apply would write for it. These notes never make it fail.
  2. npx depcurfew init writes depcurfew.json with the default policy below and lists the tools it detected. When the repository already sets a longer release-age delay for one of them, cooldown.minimum keeps the longest one, and init says where it found it.
  3. npx depcurfew explain shows what it detected and what the policy means for each tool.
  4. npx depcurfew apply --dry-run prints the changes as a diff, without writing anything.
  5. npx depcurfew apply writes them. Run it again whenever depcurfew.json changes; a second run with the same policy changes nothing.
  6. npx depcurfew doctor reports configuration that silently does not work and drift from the policy, and exits with 1 when it finds an error or, with the "doctor": { "failOn": "warning" } that init writes, a warning.
  7. Add the GitHub Action so that doctor runs on every push and reports to code scanning.

The default policy:

{
  "$schema": "https://unpkg.com/depcurfew/schema.json",
  "version": 1,
  "cooldown": {
    "minimum": "3d",
    "exclude": [],
    "securityUpdates": "bypass"
  },
  "installScripts": {
    "default": "deny",
    "allow": [],
    "deny": []
  },
  "sources": {
    "git": "deny",
    "remoteTarball": "deny",
    "file": "allow",
    "directory": "allow"
  },
  "targets": "auto",
  "doctor": {
    "failOn": "warning"
  }
}

Documentation

The documentation site, depcurfew.github.io, shows these parts of this README on pages of their own:

Installation

depcurfew runs on Node.js 18.3.0 or later, so it also works where a project or a coding agent still has an older Node.js (CI runs every command on the oldest version it supports), and has a single runtime dependency, yaml. Node.js 18 and 20 no longer get security fixes from the Node.js project; prefer a supported line where you can. Working on depcurfew itself needs Node.js 22.18 or later.

Add it to the project as a development dependency, so the team, CI and coding agents all run the version pinned in the lockfile. Once apply has written the project's release-age delay, later updates of depcurfew go through that delay as well:

npm install --save-dev --save-exact depcurfew
npx --no dcf doctor

dcf is the short name of the depcurfew program. Run it with npx --no: npm has an unrelated package named dcf, and where depcurfew is not installed yet (a fresh clone before npm ci, a repository without package.json), npx without --no would download and run that package instead, and in CI without asking. With --no, npx stops instead. The long name runs the same program, and npx --no depcurfew stops the same way.

Pin the exact version (--save-exact, or save-exact=true in .npmrc) and update it on purpose: depcurfew is a young project with frequent releases, and a range would let a new release in without review.

Who needs it installed:

  • Developers working on the project: nothing to do. The protection does not depend on depcurfew being installed: it lives in the package manager configuration that apply writes into the repository (.npmrc, package.json, pnpm-workspace.yaml, .yarnrc.yml, bunfig.toml, deno.json, pyproject.toml or uv.toml, poetry.toml, Pipfile, requirements.txt, composer.json, Gemfile or gems.rb, .cargo/config.toml, and the bot configuration), and the package managers read it on their own. pip's release-age delay is the exception: pip reads it only from the environment where it runs, and apply says what to set there. The project install brings depcurfew along as a development dependency anyway.
  • Whoever changes depcurfew.json: runs npx --no dcf apply and commits the result.
  • CI: runs doctor, with the GitHub Action below or with the project's own copy on other CI systems.

A global install (npm install -g depcurfew) works, but teams should not rely on it: each machine may run a different version, the lockfile does not pin it, and npm does not read the project's .npmrc in global mode, so the project's release-age delay does not cover the install.

To work on depcurfew itself, run it from a clone of this repository:

git clone https://github.com/DevXord/depcurfew.git
cd depcurfew
npm ci
npm run build
node dist/cli.js doctor /path/to/your/repository

In a repository without package.json

depcurfew is a Node.js program, and a repository without package.json in its root, such as a Rust, .NET, Python, PHP or Ruby project, does not need one to run it.

In GitHub Actions, the GitHub Action needs nothing installed.

Once, by hand, run an exact release with npx:

npx --yes depcurfew@1.5.2 doctor

Name a release that is at least as old as cooldown.minimum of the policy: depcurfew's own Claude Code hook denies a younger one, and so does npm where its min-release-age is set (npx then fails with ETARGET). The example names the newest release that was at least 7 days old when this README was written, usually the one before the newest. Without --yes, npx asks before it downloads a package the project does not have, and without a terminal or in CI it downloads without asking.

Pinned in the repository, keep depcurfew in the tools directory .claude/depcurfew: its own package.json pins depcurfew exactly, its package-lock.json is committed and .claude/depcurfew/node_modules is git-ignored. npm ci installs what that package-lock.json locks, and --prefix runs it in the tools directory, so both commands run from the root of the repository, without cd:

npm ci --ignore-scripts --prefix .claude/depcurfew
node .claude/depcurfew/node_modules/depcurfew/dist/cli.js doctor

doctor reads this copy too: depcurfew-outdated says when the depcurfew that runs is older than it, and the Claude Code hook can run it. To be offered new releases of the pin, you can add a Dependabot npm entry for the directory /.claude/depcurfew; apply gives it the cooldown of the policy like any other npm entry. The tools directory and the Action pin their versions separately, so update both.

Verifying a release

Every release is built and published from its git tag by the Release workflow with npm trusted publishing, so the npm page of each version shows its provenance: the repository, the workflow and the commit it was built from. One job of the workflow installs, tests and packs the package without the right to publish it. Before anything is staged, CHANGELOG.md must start with the section of the version, CI must have passed on the tagged commit (on Linux and Windows, down to the oldest Node.js the CLI supports), and another job must build the package again from a fresh checkout of the tag on another runner and get the same checksum. Only then does a last job, which runs no code of the repository or of its dependencies, stage exactly the file the first one packed, after comparing its SHA-512 checksum. After installing, npm audit signatures checks the registry signature and the provenance attestation of every installed package, depcurfew included.

The package contains nothing but the build of that commit, and anyone can check it: check out the tag, build it and compare the checksum with the one the registry serves:

git clone --branch v1.6.0 https://github.com/DevXord/depcurfew.git
cd depcurfew
npm ci --ignore-scripts
npm run build
npm pack --dry-run --json | grep shasum
npm view depcurfew@1.6.0 dist.shasum

The two checksums are the same. For 1.5.0 they differ: its JavaScript files are the build of the tag byte for byte, but its declaration file dist/index.d.ts lists the properties of the policy types in another order, which a build that ran right after the tests produced; later releases write those types out, so every build gives the same file. The Verify release workflow does this for the newest version on npm every day and on demand, and its runs are public in the Actions tab of the repository.

On this site

Start

Reference

Integrations

Project