On this page
depcurfew
One supply-chain security policy for npm, pnpm, Yarn, Bun, Deno, uv, pip, Poetry, PDM, Pipenv, Composer, Renovate and Dependabot, and a doctor that finds configuration that silently does nothing.
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.
Five-minute start
npx depcurfew initwritesdepcurfew.jsonwith 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.minimumkeeps the longest one, andinitsays where it found it.npx depcurfew explainshows what it detected and what the policy means for each tool.npx depcurfew apply --dry-runprints the changes as a diff, without writing anything.npx depcurfew applywrites them. Run it again wheneverdepcurfew.jsonchanges; a second run with the same policy changes nothing.npx depcurfew doctorreports 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" }thatinitwrites, a warning.- 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"
}
}
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 dcf doctor
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
applywrites into the repository (.npmrc,package.json,pnpm-workspace.yaml,.yarnrc.yml,bunfig.toml,deno.json,pyproject.tomloruv.toml,poetry.toml,Pipfile,requirements.txtand 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, andapplysays what to set there. The project install brings depcurfew along as a development dependency anyway. - Whoever changes
depcurfew.json: runsnpx dcf applyand 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
The policy file
Every section is optional: a section that is absent means depcurfew does not manage that area. Durations are converted to each tool's own unit and always rounded up, so a tool never waits less than the policy says. Where a tool cannot express part of the policy, depcurfew says so instead of skipping it silently.
| Field | Values | Default | Meaning |
|---|---|---|---|
version | Version of the policy format. | ||
cooldown | Release-age delay (cooldown): freshly published versions wait before they are used. | ||
cooldown.minimum | Minimum age of a release before it is installed or proposed: a whole number followed by m (minutes), h (hours), d (days) or w (weeks). Converted to each tool's unit, always rounding up. | ||
cooldown.exclude | [] | Packages that skip the release-age delay, e.g. your own scope. | |
cooldown. | "bypass", "delay" | "bypass" | Whether security updates skip the delay ("bypass") or wait like other updates ("delay"), where the tool can tell them apart. |
installScripts | Code that dependencies run when they are installed: lifecycle scripts in npm, building from source in Python, plugins in Composer. | ||
installScripts. | "allow", "deny" | Whether dependency lifecycle scripts (preinstall, install, postinstall) run by default; for Python, whether packages may be built from source; for Composer, whether plugins may run. | |
installScripts. | [] | Packages whose lifecycle scripts may run when the default is "deny"; Python packages listed here are always built from source; Composer packages listed here are the plugins allow-plugins lets run. | |
installScripts. | [] | Packages you reviewed that work without their lifecycle scripts, so the scripts stay blocked on purpose and doctor does not report them, for example "fsevents", which ships a prebuilt binary. depcurfew init fills it with the packages of the lockfile whose scripts depcurfew knows to be unneeded (esbuild, fsevents) or to do only a task of the project (lefthook). A package cannot be in both lists, also not through two patterns that match the same name. Python packages listed here must not be built from source: doctor reports a tool that would still build one. Composer packages listed here are plugins that allow-plugins disallows. | |
sources | Where dependencies may come from besides the registry. An absent field is not managed. | ||
sources.git | "allow", "deny" | Dependencies installed from git repositories. | |
sources. | "allow", "deny" | Dependencies installed from tarball URLs outside the registry. | |
sources.file | "allow", "deny" | Dependencies installed from local tarball files. | |
sources. | "allow", "deny" | Dependencies installed from local directories. | |
targets | "auto" | Tools to configure: "auto" detects them in the repository, or list them explicitly. | |
projects | [] | Directories of independent projects in this repository, each with its own manifest and lockfile (e.g. frontend and backend). depcurfew configures each one with this policy as a project of its own, and configures Renovate and Dependabot once, from the repository root, for all of them. The repository root is always configured as well. A project must not be inside another one, must not be a workspace package of the root and must not have a depcurfew.json of its own. | |
doctor | How depcurfew doctor and the GitHub Action treat findings. Without this section only errors fail, as before; depcurfew init writes "failOn": "warning". | ||
doctor.failOn | "error", "warning" | Which findings make depcurfew doctor exit with 1 and fail the GitHub Action: "error" (only errors, also when left out) or "warning" (warnings too). Notes never fail. doctor --fail-on and the input fail-on of the Action override it. |
doctor.failOn decides which findings make depcurfew doctor exit with 1 and fail the GitHub Action: "error" only errors, "warning" warnings too; notes, which say what doctor could not check, never fail. depcurfew init writes "warning", so a new repository stays free of warnings. A policy without the doctor section fails on errors only, as before, so an existing depcurfew.json keeps its exit codes. doctor --fail-on error or --fail-on warning and the input fail-on of the Action override it for one run; an invalid depcurfew.json counts as "error" (it fails doctor anyway, as invalid-policy). The user policy takes the same section for doctor --user.
Several projects in one repository
A repository can hold independent projects side by side, each in its own directory with its own manifest and lockfile, often with nothing in the root but the bot configuration:
.github/dependabot.yml updates entries for /frontend and /backend
depcurfew.json "projects": ["frontend", "backend"]
frontend/ package.json, package-lock.json
backend/ pyproject.toml, uv.lock
List those directories under projects, relative to the repository root with / between directories (no patterns, no .., no / at either end). apply, doctor and explain then treat each listed directory as a project of its own with the policy of the root: its own .npmrc, lockfiles and workspaces, as if the command ran there. The root is configured as before. Renovate and Dependabot are configured once, from the repository root, where they read their configuration, and wait at least as long as the package manager of any project (in the example, npm rounds 36h up to 2 days, so every Dependabot entry gets default-days: 2). Every path in the output is relative to the root (frontend/.npmrc), and all files of all projects are written or none. A renovate.json or .github/dependabot.yml inside a project is never read by the bot, so doctor gives a note at it instead of comparing its delay (and says so when the root has no configuration of that bot at all), and "What was checked" marks it as not read; a project that the Dependabot configuration of the root leaves out while it updates other projects gets a warning.
What the root gives a project, for the npm it runs: the CI jobs of the root that install in its directory (working-directory: frontend, or a literal cd frontend before the install in the same shell); the .nvmrc, .node-version and .tool-versions of the root (nvm, nodenv and asdf use those files in subdirectories too), each unless the project has a file of the same kind, because each tool reads only its own file: with frontend/.nvmrc, asdf still uses the root .tool-versions in frontend; and the stages of a root Dockerfile that copy files only from that project (COPY frontend/package.json frontend/package-lock.json ./). Copy sources are read as Docker reads them, from the root of the build context with . and .. resolved (COPY frontend/../ . copies the whole context, /frontend/x is frontend/x). A stage that copies . or files of several directories is the root's, because it is not clearly a build of one project, except an install after a literal cd frontend (RUN cd frontend && npm ci), which is the project's. A root CI job or Dockerfile stage that installs another npm (npm install -g npm@<version>) before installing in a project is checked with the settings of that project. Volta, devEngines and engines in the root package.json do not count for a project, which has a package.json of its own. explain lists, for each project, the root version files and root Dockerfile stages it uses (fromRoot in --json).
A package manager that the root shows only through a CI setup step (such as pnpm/action-setup or oven-sh/setup-bun in a workflow of the root) is set up there for the projects, not used by the root, when the root has no manifest of any tool of its registry (for pnpm, Bun and Deno no package.json, package.json5, package.yaml, deno.json or deno.jsonc; for uv and Poetry no pyproject.toml, requirements.txt or Pipfile), or when every install with it in the root's workflows runs in a listed directory (by working-directory or a literal cd) and no Dockerfile of the root installs with it. Then apply writes nothing for it in the root, doctor does not list it among the tools of the root or compare Renovate and Dependabot with it, and explain lists it under "Package managers set up in CI for projects" (ciForProjects in --json); manager-version-too-old still checks the version the step installs. When the root has none of its manifests and no project uses it either, apply names it in tool-only-in-ci as before. Without projects such a step counts for the root, as before.
An npm depcurfew cannot tell (a Node.js version newer than the releases it knows, an image pinned only by digest, a version it cannot read) counts as one that runs every dependency install script: apply warns with scripts-not-enforceable and doctor reports packages with install scripts as errors, so an unknown npm never passes for one that blocks them. The npm of every Node.js release down to 0.6.3 is known from nodejs.org/dist/index.json; every release before 18.0.0 comes with npm 8 or older.
depcurfew init in a root without a manifest or lockfile lists the direct subdirectories that have one, except one with a depcurfew.json of its own, which it names and leaves out. init also leaves out a git submodule or another repository of its own (a directory with its own .git file or directory). A listed directory that does not exist, is a file, is reached through a symlink, is a separate git repository or lies in one, has a depcurfew.json of its own or overlaps a workspace package of the root is an error (project-not-found, project-not-a-directory, project-symlink, project-is-repository, project-has-policy, project-in-workspace) at the line of its entry in depcurfew.json: apply writes nothing, explain shows it and exits with 1, and doctor reports it as invalid-project. With a list of targets, each listed package manager is configured only in the directories that use it. check and the Claude Code hook find the depcurfew.json of the root from a project directory, as from any subdirectory of the repository.
Commands
The package installs two names for the same command: depcurfew and the short dcf, so dcf doctor works like depcurfew doctor.
| Command | What it does |
|---|---|
depcurfew init [directory] [--force] [--json] | Writes depcurfew.json with the default policy (keeping a longer release-age delay the repository already sets); --force overwrites an existing one, --json prints the result as JSON. |
depcurfew explain [directory] [--json] | Describes the detected package managers, versions, bots and workspaces, and what the policy means for each tool: the release-age delay, and the settings apply writes for installScripts and sources, or why a tool cannot enforce them. |
depcurfew apply [directory] [--dry-run] [--json] | Writes the policy into the configuration of every target tool; --dry-run prints a unified diff instead, --json the result as JSON. |
depcurfew doctor [directory] [--json | --sarif] [--fail-on error|warning] [--osv] | Finds configuration that silently does not work and drift from the policy; --sarif prints SARIF 2.1.0 for code scanning, and --fail-on overrides doctor.failOn of the policy (--strict is --fail-on warning). --osv also looks every locked package version up in OSV.dev: see Known malicious versions. |
depcurfew check <package...> [--json] [--dir <directory>] [--registry <url>] [--index <url>] [--osv] | Checks packages against the policy before you install them: see Guarding installs. |
depcurfew check --hook claude-code [--osv] | The same check as a Claude Code hook that blocks install commands breaking the policy. |
depcurfew mcp [--dir <directory>] [--registry <url>] [--index <url>] [--osv] | The same check as tools of a Model Context Protocol server, for AI agents in Claude Desktop, VS Code and other clients: see MCP server. |
depcurfew init --user [--force] [--json] | Writes the user policy, which --user uses: see Protecting the whole machine. |
depcurfew apply --user [--dry-run] [--json] [--for <tool@version>] | Writes the user policy into the user-level configuration of the installed package managers. |
depcurfew doctor --user [--json | --sarif] [--fail-on error|warning] [--for <tool@version>] | Checks that configuration against the user policy, and the environment variables that override it. |
Without a directory, explain, apply and doctor work on the nearest directory with depcurfew.json, from the current directory up to the root of the repository (the directory with .git), and say so when that is a parent directory (outside a git repository only the current directory counts, and on Linux and macOS a parent directory counts only when it belongs to you and others cannot write to it, because a shared parent such as /tmp may hold someone else's depcurfew.json; directories inside node_modules are skipped), so doctor run in a subdirectory does not pass for a project without a policy. A directory given on the command line is used as it is, unless the nearest depcurfew.json above it lists it (or a directory it lies in) under projects: then the command works on that root, which checks the directory as one of its projects, as when it runs in that directory, and says so (the GitHub Action does the same for its directory input). check looks for depcurfew.json the same way, starting in --dir.
Exit codes: 0 success (warnings included, except for doctor when warnings fail it), 1 an error (an invalid or unreadable policy, a directory in projects that cannot be used, a file that cannot be edited or written, a doctor finding with level error, or also one with level warning when doctor.failOn in depcurfew.json (with --user the user policy) or --fail-on is warning, which --strict means too; notes never fail), 2 a usage error (such as an unknown option or a directory that does not exist). depcurfew <command> --help describes a command with examples and its exit codes.
Every finding and warning says what to do about it. When a command cannot start, it says why with a code (directory-not-found, not-a-directory, policy-missing, policy-invalid, policy-unreadable, and for init also policy-exists and cannot-write) and what to do; with --json that is a document with errors on stdout, and doctor --sarif prints a run marked as not successful. Text for people is colored on a terminal; NO_COLOR=1 turns colors off and FORCE_COLOR=1 turns them on, for example in a CI log that shows them. Control characters in names and values from the repository or a registry are shown as text (ESC as \x1b, or as \u001b inside a quoted value such as a package name in the problems of depcurfew.json), so a crafted package or file name cannot change the terminal. --json, --sarif and a diff written to a file stay plain, for programs and AI agents.
The --json documents of all commands share these fields: schemaVersion (1), policy (its status in apply and doctor, the path of the depcurfew.json used in check, its values in explain, the file written in init), notices (each with level note, a code, location when it concerns a file, message, and fix when there is something to do; policy-from-parent says that doctor, apply or explain uses the depcurfew.json of a parent directory and works on that directory) and errors, the reasons a command could not start, each with code, level, location.file, message and fix, and issues for policy-invalid (home-missing for a home directory that is not an absolute path, also an empty HOME, names the variable instead of a file: location is {"file": "environment", "variable": "HOME"}, USERPROFILE on Windows, as in doctor --user). A usage error with --json (an unknown option, check without a package, --json with --sarif) prints a document with only schemaVersion and errors, with the code usage and no location, and exits with 2; without --json it is written on stderr like the other errors, as error: <message> (usage) and a fix: line. The warnings of explain have code, level, location (when they concern one file), message, files and fix. Paths are relative to the directory the command works on (a directory that does not exist is named as given). A depcurfew.json that cannot be used is an error with the same code, message and fix in apply, check and explain, which also keeps it in policy; doctor reports it as the finding invalid-policy, so that it reaches code scanning too, and counts it in summary.error. Within a schemaVersion fields and values are only added; a field is removed or changes its meaning only with a new schemaVersion.
explain shows, for each tool in the policy section (and for each directory listed in projects), what installScripts and sources mean there, from the same plan apply makes: the settings apply writes with their files, or the warning apply gives when the tool cannot enforce a part, with its code and what to do. The text has one line per tool and part, nothing to write when apply writes nothing there and has nothing to say, and not managed when the policy has no such section:
installScripts deny, allow esbuild:
<tool>: <setting> and <setting> in <file>, <setting> in <file>; warning: <message> (<code>); fix: <what to do>
sources git deny:
<tool>: nothing written; warning: <tool> has no setting for git dependencies, so "deny" cannot be enforced (source-not-enforceable); fix: <what to do>
In explain --json, policy.installScripts holds default, allow, deny and tools, policy.sources the sources the policy sets and tools, and each entry of projects has installScripts and sources with tools (each null when the policy has no such section). Each entry of tools has tool, settings (setting, file, and table for a TOML table) and warnings, the warnings of apply --json about that part for the tool. A setting that apply writes only to correct a value (allow-all-replaced) is listed only while the file still needs that correction.
How apply edits files:
- It keeps comments, formatting, line endings and key order. In line-based files (INI, TOML) it writes a block between
# >>> depcurfew managed blockand# <<< depcurfew managed blockmarkers and comments out older copies of the settings it manages (they come back when the policy stops managing them); in YAML it marks the keys it writes with# managed by depcurfew; JSON files keep their comments and indentation. - It reads files the way the tool does. In YAML files of a tool whose loader applies merge keys (
<<: *defaultsinpnpm-workspace.yamland pnpm'sconfig.yaml), a setting a merge key brings in counts, andapplywrites its value into the map itself, which wins over merged keys;.yarnrc.ymlanddependabot.ymlare read without merging. A switch that allows every install script counts as on for every value the tool does not read as off: pnpm runs every build script fordangerouslyAllowAllBuilds: "false"(a string) oryes, and npm and pnpm 10 for=1or=yesin.npmrc. - It never removes settings it does not manage. Allow-lists are only added to; exception lists follow the policy, except the entries Renovate adds to pnpm's exception list for security updates, which stay with their comment. An entry in a tool's own install-script list that contradicts
installScripts(it lets a package ofinstallScripts.denyrun its scripts, or blocks one ofinstallScripts.allow) stays as it is, with the warningdeny-conflicts-with-file, andapplyrecords no denial for that package. - When it replaces a longer release-age delay someone set by hand with the policy's shorter one, it warns (
cooldown-shortened) and names thecooldown.minimumthat keeps it. - If any file cannot be edited, nothing is written. If a file cannot be written, the files already written are put back.
- It does not create bot configuration files or project manifests (
package.json,deno.json,pyproject.toml,Pipfile,requirements.txt); it creates a package manager's configuration file when it is missing. A package manager seen only in a CI setup step, with nothing of it in the repository, is not configured undertargets: "auto". - It warns when a file it writes is ignored by git (
.gitignoreor.git/info/exclude), because the change would then stay on one machine.
init warnings and notes
depcurfew init says what it wrote and, as warnings and notes with a code, what to look at before depcurfew apply: the projects it listed, a directory it left out of them, the delay it kept and what the default policy blocks. The text, on stdout, shows each with its level word, the message with its code in parentheses and what to do on the next line. depcurfew init --json (also with --user) prints one JSON document and nothing on stderr: policy (the file written, as the text names it; null when the directory does not exist or the home directory cannot be used), written, action (create or overwrite, null when nothing was written), projects, tools (each detected tool with tool, name, directory and supported; null with --user, which detects none), warnings and notices (each with code, level, location with file and, when known, line, message and fix; paths relative to the directory init works on, under ~/ with --user) and errors, why nothing was written, with the codes of apply (policy-exists, cannot-write, directory-not-found, not-a-directory, home-missing). The exit codes are those of the text: 0 written (warnings and notes included), 1 not written (the file exists without --force, it cannot be written, or with --user the home directory cannot be used), 2 a usage error or a directory that does not exist.
Install scripts: init reads the lockfiles (package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lock, deno.lock, as lockfile-install-scripts reads them) and writes the packages whose scripts depcurfew knows into installScripts.deny, each with a note that says why and what changes (install-script-denied, install-script-project-task). esbuild works without its postinstall as long as optional dependencies are installed, which hold its binary (without the script the esbuild command starts through a small Node.js script instead of the binary); fsevents loses nothing, since the package already contains the built fsevents.node; lefthook's postinstall only runs lefthook install, which installs the git hooks (and does nothing in CI), and the note says to run lefthook install after each clone of the repository, as the lefthook documentation says for projects that do not use its postinstall script (depcurfew never edits scripts). These notes are given for a package from a registry only: a package of the same name from git, a URL or a local path, or an entry whose resolved URL is the tarball of another package, is listed with the other packages to review. A known package that the package manager's own allow-list of install scripts (in package.json, pnpm-workspace.yaml or deno.json) already lets run its script stays out of installScripts.deny, as the project decided otherwise there; a note to review names it with that list and its line (install-scripts-to-review). Each fact names the release it was checked at, and a note says so when the lockfile has another one. The other packages with install scripts are listed in a note to review (install-scripts-to-review), and a lockfile that does not record install scripts (pnpm-lock.yaml, yarn.lock) gets a note too (install-scripts-not-recorded).
Warnings:
- warning
project-left-out: A direct subdirectory with a manifest or lockfile is left out of projects, because depcurfew apply would refuse it (it has a depcurfew.json of its own, or it is a separate git repository).
Notes:
- note
projects-listed: The repository root has no manifest or lockfile, but direct subdirectories do, so init lists them under projects in depcurfew.json. - note
cooldown-kept: The repository (with --user, the user file of an installed tool) already sets a longer release-age delay, so cooldown.minimum keeps it instead of the default. - note
python-builds-denied: installScripts.default "deny" also blocks building Python packages from source, so a package without wheels for the platform stops installing until it is allowed. - note
tool-only-in-ci: A tool appears only in a CI setup step, so depcurfew apply does not configure it under targets "auto". - note
install-script-denied: A lockfile has a package whose install script depcurfew knows it does not need (esbuild, fsevents), so init adds it to installScripts.deny; the note says what changes without the script. - note
install-script-project-task: A lockfile has a package whose install script only does a task of the project (lefthook installs the git hooks), so init adds it to installScripts.deny; the fix says how the project runs that task itself. - note
install-scripts-to-review: A lockfile has other packages with install scripts, or composer.lock has Composer plugins (the note says which ones config.allow-plugins in composer.json already allows), which installScripts.default "deny" blocks, or packages depcurfew knows that a package manager's own allow-list already allows, which init leaves out of installScripts.deny; review each before depcurfew apply. - note
composer-source-checkouts: config.preferred-install in composer.json makes Composer install packages of composer.lock from their VCS repositories, which sources.git "deny" does not allow, so depcurfew doctor reports them; the note says which entry and which packages. - note
bot-config-unreadable: The Dependabot configuration uses a YAML alias, which Dependabot does not accept, so it reads no configuration from the file, and init does not read the delays it sets when it chooses cooldown.minimum. - note
install-scripts-not-recorded: A lockfile does not record which packages have install scripts (pnpm-lock.yaml, yarn.lock), or is in a format depcurfew does not read, so init can name only the packages depcurfew knows.
apply warnings and errors
Each warning and error of apply has a code. The text output shows it in parentheses after the message, with what to do on the next line, like a doctor finding. depcurfew apply --json (also with --dry-run) prints one JSON document and nothing on stderr: changes (each file with its action and unified diff), warnings and errors (each with code, level, message and fix; tool when it concerns one tool; location when it concerns one file, with file and, when known, line; for an invalid policy also issues), snippets to add by hand, notices (such as policy-from-parent) and written. A run that writes nothing because of an error lists no changes and exits with 1. Diffs, on the terminal, in a pipe and in changes, show the values of secrets as <redacted> (_authToken, _auth, _password and the client key key in .npmrc, keys ending in token or password, the user and password of a URL), so that a diff can be shared or logged; the files keep them. The text diff also shows control and bidirectional characters from the files as text (\x1b, ), so that a value cannot drive the terminal or a CI log; changes keeps them, escaped by JSON.
Warnings:
- warning
nothing-to-configure: depcurfew found no supported package manager or bot, so nothing is configured. - warning
environment-overrides: apply --user: an environment variable makes a tool read another user file or none, or overrides a value apply writes there. - warning
depcurfew-outdated: The project installs a newer depcurfew than the one running, which may not know settings that version writes. - warning
file-unreadable: A file that shows which tools a project uses cannot be read or parsed (too large, a symlink out of the repository, no read permission, invalid), so depcurfew may configure fewer tools than the project uses. - warning
tool-not-supported: A detected or installed tool is not supported (Yarn Classic), so nothing is written for it. - warning
target-not-found: With projects, a package manager listed under targets is configured in each directory (the root or a project) that uses it, and none uses it, so nothing is written for it. - warning
tool-only-in-ci: A tool appears only in a CI setup step, with no manifest, lockfile or configuration in the repository, so targets "auto" does not configure it. - warning
version-not-pinned: No exact version of a package manager is pinned, so depcurfew configures for the latest version it knows. - warning
version-too-old: The pinned version of a tool (with apply --user, the installed one) lacks a setting the policy needs, so that setting is not written. - warning
version-unknown: apply --user: the installed version of a tool cannot be read from its files, and a value it does not understand would stop every command, so nothing is written for it. - warning
no-user-config: apply --user: the tool has no user-level configuration file, so nothing is written for it. - warning
user-setting-project-only: apply --user: the tool reads a part of the policy only from project files, so it is not written into the user file. - warning
user-directory-relative: apply --user: the environment variable that names the directory of the user file (XDG_CONFIG_HOME, APPDATA) is a relative path, so the tool looks in another place from each directory, or where it looks then is not known; nothing is written for it. - warning
user-file-uncertain: apply --user: a library the tool calls decides where its user file is (platformdirs for Poetry and PDM on macOS), and its version cannot be read from files, so apply --user writes where the tool's docs say and names the other files the tool may read. - warning
user-setting-overridden: apply --user: a file the tool reads ahead of its user file (~/.npmrc for pnpm 10) sets a key apply writes there to another value, so the value written has no effect. - warning
version-not-used: apply --user: --for gives the version of a tool that apply --user does not configure (not in PATH with targets "auto", or not listed under targets). - warning
cooldown-not-written: The release-age delay cannot be written for a tool. - warning
cooldown-shortened: A release-age delay set outside depcurfew's markers (in JSON files also one an earlier apply wrote) is longer than the policy, and apply replaces it with the shorter one. - warning
cooldown-environment-only: pip reads no project configuration, so the release-age delay has to be set in the environment where pip runs. - warning
exclude-pattern-unsupported: A cooldown exception uses a wildcard the tool does not support, so it is not written. - warning
exclude-unsupported: The tool has no setting for cooldown exceptions, so the release-age delay applies to every package. - warning
exclude-not-written: The setting that holds the cooldown exceptions (Composer's config.policy.cooldown.ignore) has a value of a type the tool does not accept there, so apply leaves it as it is and does not add the exceptions; the release-age delay is written. - warning
exclude-entry-outside-policy: A cooldown exception in a file without depcurfew's markers (Composer's config.policy.cooldown.ignore) exempts a package that cooldown.exclude does not; apply removes only the entries it wrote, recognised by their reason, and leaves this one. - warning
allow-pattern-unsupported: An installScripts.allow entry is a pattern, but the tool takes package names only, so it is not written. - warning
deny-pattern-unsupported: An installScripts.deny entry is a pattern, but the tool records denials by package name only, so it is not written. - warning
allow-all-unsupported: The tool has no setting that allows every install script, so installScripts "allow" is not written. - warning
allow-all-replaced: A setting that allowed every install script (with any value the tool reads as true, also one that a YAML merge key brings in, or pnpm 10's dangerously-allow-all-builds in .npmrc) was set to false, or replaced with the policy's allow-list where the setting is the list itself (Deno's allowScripts, Composer's allow-plugins, which then may be {}), because the policy denies install scripts. - warning
allow-list-entry-outside-policy: An install-script allow-list in the repository (with apply --user, in a user file) lets a package run scripts the policy does not allow; apply does not remove it. - warning
deny-conflicts-with-file: An entry in a tool's own list lets a package of installScripts.deny run its install scripts, or blocks a package of installScripts.allow; apply does not change entries it did not write. - warning
deny-needs-optional-dependencies: A package of installScripts.deny takes its binary from an optional dependency (esbuild), but npm leaves optional dependencies out (omit=optional or optional=false in .npmrc, --omit=optional or --no-optional on an install of a CI job or Dockerfile, NPM_CONFIG_OMIT=optional), so the package cannot find its binary after that install. - warning
unexpected-value: A setting apply adds to has a value of a type the tool does not accept there, so apply leaves it as it is and writes nothing into it. - warning
built-in-list-replaced: Bun's built-in list of trusted packages is replaced by the policy's allow-list. - warning
scripts-not-enforceable: The pinned (with apply --user, the installed) npm or pnpm, or another npm the repository runs (one a declared Node.js comes with, or a CI job or Dockerfile installs), runs every install script and has no allow-list, so "deny" cannot be enforced; also given for an npm the repository runs whose version depcurfew cannot tell, which counts as one that runs them. - warning
strict-allow-scripts-for-npm-in-use: An npm the repository runs (the pinned one, one a CI job or Dockerfile installs, or one a declared Node.js comes with, for a project also through the root version files and root Dockerfiles that build it) is 11.16 to 11.x and runs install scripts the allow-list does not cover unless strict-allow-scripts is set, so apply writes strict-allow-scripts=true, which makes npm 12 stop the install at such a script; not given when that npm is the pinned one and no place runs npm 12. - warning
before-overrides-cooldown: before= in .npmrc takes the place of the release-age delay apply writes, or makes npm stop with an error next to it (apply --user then does not write the delay). - warning
exact-version-gap: The pinned (with apply --user, the installed) pnpm skips the release-age delay for packages requested by exact version. - warning
source-not-enforceable: The tool has no setting for a dependency source the policy denies; doctor checks the lockfile instead. - warning
source-switch-indirect-only: The pnpm setting for git and tarball sources stops them only for indirect dependencies. - warning
source-switch-combined: One pnpm setting covers git and tarball sources, so denying one denies both. - warning
forced-source-build: Python packages on the allow-list are always built from source, because no tool can only allow a build. - warning
build-switch-scope: The tool's switch forbids building only packages from an index; git, local and archive dependencies are still built. - warning
build-switch-install-only: Poetry applies its build switch when installing; poetry lock may still build source distributions. - warning
build-switch-environment-only: The tool has no project setting that forbids building from source, so it has to be set in the environment where the tool runs. - warning
requirements-file-missing: There is no requirements file to write pip options into. - warning
manifest-missing: A setting belongs in a project manifest (package.json, deno.json, pyproject.toml, Pipfile, composer.json) that does not exist, and apply does not create manifests. - warning
lockfile-out-of-date: The lockfile records, or is checked against, a setting apply changed, so it has to be locked again. - warning
file-ignored-by-git: A file apply writes matches a .gitignore rule, so the change may never reach the repository. - warning
bot-config-missing: A bot has no configuration file, and depcurfew does not create one. - warning
bot-config-not-edited: A bot configuration file is in a place or format depcurfew does not edit; a snippet shows what to add. - warning
bot-entries-missing: The Dependabot configuration has no entries for the package managers in use. - warning
bot-config-unreadable: The Dependabot configuration uses a YAML alias, which Dependabot does not accept, so it reads no configuration from the file (the cooldown apply writes included), and depcurfew does not read the delays the file sets. - warning
bot-rule-shortens-delay: A Renovate packageRules entry apply does not manage gives every package of a registry, Renovate manager or category in use, or the actions of the workflows, or some update types of them, a shorter release-age delay than the one apply writes; Renovate uses it, and apply does not change it. - warning
security-delay-unsupported: Dependabot cannot delay security updates, so securityUpdates "delay" is not applied.
Errors (nothing is written):
- error
cannot-edit: A file cannot be read or edited safely (invalid syntax, a symlink, a style depcurfew does not edit), so nothing is written. - error
cannot-write: A file cannot be written (no permission, a read-only file system, a full disk, a directory or symlink in its place); the files already written are put back, so nothing changes. - error
policy-missing: The directory has no depcurfew.json. - error
policy-invalid: depcurfew.json does not follow the schema; issues lists the problems. - error
policy-unreadable: depcurfew.json cannot be read. - error
policy-exists: depcurfew init found a depcurfew.json and writes it only with --force. - error
directory-not-found: The directory given on the command line does not exist or cannot be opened. - error
not-a-directory: The path given on the command line is a file, not a directory. - error
usage: The command line is not valid: an unknown command, option or hook, a missing or extra argument, an option without its value or with one it does not take (such as a --registry that is not an http:// or https:// URL), or options that cannot go together (exit code 2); with --json the document holds only schemaVersion and this error. - error
project-not-found: A directory listed in projects in depcurfew.json does not exist, so nothing is written. - error
project-not-a-directory: A path listed in projects in depcurfew.json is a file, not a directory, so nothing is written. - error
project-symlink: A directory listed in projects in depcurfew.json is a symlink or lies under one, which may lead out of the repository, so nothing is written. - error
project-has-policy: A directory listed in projects has a depcurfew.json of its own, a second policy for the same files, so nothing is written. - error
project-is-repository: A directory listed in projects is a separate git repository or lies in one (it has a .git of its own, as a git submodule does), whose files belong to that repository, so nothing is written. - error
project-in-workspace: A directory listed in projects is, holds or lies in a workspace package of the repository root, which the root configuration already covers, so nothing is written. - error
home-missing: apply --user and init --user: the home directory (HOME, USERPROFILE on Windows) does not exist or is not an absolute path, so nothing is written.
Protecting the whole machine
depcurfew apply protects one project. The user-level configuration of each package manager protects every project on the machine that does not set the same settings itself, and also what runs outside a project: npx, uvx, global installs, and a new project before anyone ran apply in it.
depcurfew init --user
depcurfew apply --user --dry-run
depcurfew apply --user
depcurfew doctor --user
- The user policy is
~/.config/depcurfew/depcurfew.json(in$XDG_CONFIG_HOME/depcurfew/when that variable is set to an absolute path, in%APPDATA%\depcurfew\on Windows). It has the same format as a project policy; undertargets: "auto"it configures the package managers found inPATH. - depcurfew does not run other programs to learn their versions. It reads the version from the files next to the executable in
PATH: thepackage.jsonof a tool installed from npm (also Yarn installed as@yarnpkg/cli-dist), the.dist-infofolder of one installed from PyPI. A Yarn or pnpm shim of Corepack runs, outside a project, the version Corepack'slastKnownGood.jsonnames (in$COREPACK_HOME, else$XDG_CACHE_HOME/node/corepack, else~/.cache/node/corepack;%LOCALAPPDATA%\node\corepackon Windows), and ayarnPathin~/.yarnrc.ymlthat points to a release namedyarn-<version>.cjs, asyarn set versionnames it, gives Yarn's version;doctor --usersays where each version came from. When it cannot, it configures for the latest version it knows and says so, except for tools that stop every command on a value their version does not understand (the last column below): for those it writes nothing until you pass the version, for example--for uv@0.13.6. Bun from its official installer (~/.bun/bin/bun, which writes no version file) or from Homebrew has no version depcurfew can read, so it needs--for bun@<version>. init --userkeeps a longer release-age delay that the user files of the installed tools already set, asinitdoes in a project: a delay of 7 days in~/.npmrcmakescooldown.minimum7d, andinit --usersays which setting, value, file and line it took. Deno reads the delay in~/.npmrcin whole days and ignores any other value, soapply --userwrites it for Deno in days, rounded up.- Poetry and PDM ask the library platformdirs where their user directory is, and on macOS the answer depends on the platformdirs version in the tool's environment (the rows below). depcurfew reads that version from the
platformdirs-<version>.dist-infofolder next to the tool's own; when it cannot andXDG_CONFIG_HOMEorXDG_DATA_HOMEwould move the file,apply --userwrites where the docs say (~/Library/Application Support/...) and warns with the other file (user-file-uncertain), anddoctor --userchecks both. apply --userwrites the files of all tools or none, asapplydoes in a project: a user file it cannot read or edit (not UTF-8, a symlink, larger than 1 MiB, a file where its directory belongs) is an error that names it, such as~/.config/pip/pip.conf, with a fix for that problem, and a file that cannot be written puts back the ones already replaced.init --userandapply --userdo not create a missing home directory, and whenXDG_CONFIG_HOMEor%APPDATA%is a relative path,apply --userwrites nothing for the tools that may use it, with a warning (pip then readspip.confrelative to the directory each command runs in). Run as root (sudowithHOMEkept), they leave each file they replace to its owner, and give what they create in the home directory to the owner of the home directory.- A key
apply --userwrites into a user file can be overridden by a file the tool reads ahead of it: pnpm 10 reads~/.npmrcbefore itsrcfile.apply --userwarns about a different value there (user-setting-overridden) with the line, anddoctor --userreports it. - A tool's own project settings win over its user file for the same key. Parts of the policy that a tool reads only from project files, such as per-package allow-lists, are not written into the user file;
apply --usernames them, anddepcurfew applyin the project writes them. - A user file is edited as
applyedits a project file: a setting the policy manages that is set by hand elsewhere in the file is commented out and comes back when the policy stops managing it (YAML files get the policy's value instead). That includes an exception list the policy leaves empty, the command sections ofpip.confsuch as[install], and the[pip]table ofuv.toml. Install-script allow-list entries that the policy does not allow are kept and reported; a longer delay that the policy shortens is reported. doctor --userreports a user file that does not match the user policy, values in user files that weaken every project, and environment variables that make a tool read another file, no file or other values, such as a relativeXDG_CONFIG_HOME. It reads values as each tool does: a switch that allows every install script is on for every value the tool does not read as off (dangerously-allow-all-builds=1in~/.npmrcfor pnpm 10), and a merge key (<<: *defaults) counts in pnpm'sconfig.yaml. A missing home directory, or a user file it cannot read, is an error rather than a clean report, and an installed tool too old for settings the user policy needs (npm 10 has no release-age delay and no install-script allow-list) is a warning that says what stays unprotected and which version to update to.- Tools that record the release-age delay in their lockfile may lock differently on a machine whose user file sets one than on a machine without it; set the same delay in the project with
depcurfew applyso that every machine and CI agree.
| Tool | User file | Settings apply --user writes (versions that read them there) | Needs the exact version |
|---|---|---|---|
| npm | Linux, macOS: ~/.npmrcWindows: %HOME%\.npmrc (when it is an absolute path), else ~\.npmrc | min-release-age (11.10.0+), min-release-age-exclude (11.17.0+), strict-allow-scripts (11.16.0+), allow-scripts (11.16.0+), allow-git (11.10.0+), allow-remote (11.15.0+), allow-file (11.15.0+), allow-directory (11.15.0+) | no |
| pnpm | Linux: $XDG_CONFIG_HOME/, else ~/.macOS: $XDG_CONFIG_HOME/, else ~/Windows: %XDG_CONFIG_HOME%\pnpm\rc, else %LOCALAPPDATA%\pnpm\config\rc, else ~\.11.0.0+: Linux: $XDG_CONFIG_HOME/, else ~/.macOS: $XDG_CONFIG_HOME/, else ~/Windows: %XDG_CONFIG_HOME%\pnpm\config., else %LOCALAPPDATA%\pnpm\config\config., else ~\. | minimum-release-age (10.16.0–11.0.0), minimum-release-age-exclude (10.16.0–11.0.0), only-built-dependencies (10.0.0–11.0.0), strict-dep-builds (10.3.0–11.0.0), block-exotic-subdeps (10.26.0–11.0.0), minimumReleaseAge (11.0.0+), minimumReleaseAgeExclude (11.0.0+), minimumReleaseAgeStrict (11.0.0+), strictDepBuilds (11.0.7+), blockExoticSubdeps (11.0.0–12.0.0) | yes |
| Yarn (Berry) | Linux, macOS, Windows: ~/.yarnrc.yml | npmMinimalAgeGate (4.10.2+), npmPreapprovedPackages (4.10.0+), enableScripts (4.0.0+), approvedGitRepositories (4.14.0+) | yes |
| Bun | Linux, macOS, Windows: $XDG_CONFIG_HOME/. (when it is an absolute path), else ~/.bunfig.toml | install. (1.3.0+), install. (1.3.0+) | yes |
| Deno | 1.45.0+: Linux, macOS, Windows: ~/.npmrc | min-release-age (2.8.0+) | no |
| uv | Linux, macOS: $XDG_CONFIG_HOME/ (when it is an absolute path), else ~/.Windows: %APPDATA%\uv\uv., else ~\AppData\Roaming\uv\uv. | exclude-newer (0.9.17+), exclude-newer-package (0.9.25+), no-build (0.2.12+), no-build-package (0.2.12+), no-binary-package (0.2.12+) | yes |
| pip | Linux: $XDG_CONFIG_HOME/, else ~/.macOS: ~/ (when the folder exists), else ~/.Windows: %APPDATA%\pip\pip., else ~\AppData\Roaming\pip\pip.26.2.0+: Linux: $XDG_CONFIG_HOME/, else ~/.macOS: $XDG_DATA_HOME/ (when the folder exists), else ~/ (when the folder exists and $XDG_DATA_HOME is not set), else ~/.Windows: %APPDATA%\pip\pip., else ~\AppData\Roaming\pip\pip. | uploaded-prior-to (26.1.0+), only-binary (7.1.0+), no-binary (7.1.0+) | yes |
| Poetry | 2.0.0+: Linux: $XDG_CONFIG_HOME/, else ~/.macOS: ~/Windows: %APPDATA%\pypoetry\config., else ~\AppData\Roaming\pypoetry\config.macOS with platformdirs before 4.6.0: ~/macOS with platformdirs 4.6.0–4.9.6: $XDG_CONFIG_HOME/, else $XDG_DATA_HOME/, else ~/macOS with platformdirs 4.9.6–4.11.8: $XDG_CONFIG_HOME/, else ~/macOS with platformdirs 4.11.8+: $XDG_CONFIG_HOME/ (when it is an absolute path), else ~/ | solver. (2.4.0+), solver. (2.4.0+), installer. (2.0.0+), installer. (1.2.0+) | no |
| PDM | Linux: $XDG_CONFIG_HOME/, else ~/.macOS: ~/Windows: ~\AppData\Local\pdm\pdm\config.macOS with platformdirs before 4.6.0: ~/macOS with platformdirs 4.6.0–4.9.6: $XDG_CONFIG_HOME/, else $XDG_DATA_HOME/, else ~/macOS with platformdirs 4.9.6–4.11.8: $XDG_CONFIG_HOME/, else ~/macOS with platformdirs 4.11.8+: $XDG_CONFIG_HOME/ (when it is an absolute path), else ~/ | strategy. (2.27.0+) | yes |
| Pipenv | none | none | no |
doctor --user rules
user-policy-invalid
error · The user policy does not follow the schema or cannot be read.
Without a valid user policy (~/.config/depcurfew/depcurfew.json), doctor --user cannot compare the user-level configuration with it, so only the checks that need no user policy run, as when there is none.
Fix: Fix what the message lists, or write a new user policy with depcurfew init --user --force.
user-config-unreadable
error · A user-level configuration file cannot be read, parsed or edited, or the home directory does not exist, so depcurfew cannot check it.
The package manager may still read the file (or stop on it), and its settings may weaken every project on the machine; doctor --user fails closed and reports an error instead of passing silently. Without a home directory the user files of most tools cannot be found at all.
Fix: Do what the fix of the finding says: fix invalid syntax, give read permission, replace a symlink apply --user does not write through, make a file smaller than 1 MiB, or set HOME to an existing directory.
user-config-drift
warning · needs depcurfew.json · A user-level configuration file does not match the user policy.
depcurfew apply --user writes the user policy (~/.config/depcurfew/depcurfew.json) into the user-level configuration of each installed tool, which protects every project on the machine that does not set the same settings itself, and npx, uvx and global installs. A file that no longer matches leaves that protection weaker than the policy.
Fix: Run depcurfew apply --user.
user-config-weakens
warning · A user-level configuration file has a value that weakens every project that does not set it itself.
Settings in the user-level configuration apply to every project that does not set the same key, and some apply even then (lists that tools merge across files). A value that turns the release-age delay off, exempts packages or allows install scripts or git dependencies there weakens all of them at once, and so does a file the tool reads ahead of the user file.
Fix: Remove the value from the file, or set it in the policy and run depcurfew apply --user.
user-environment-overrides
warning · An environment variable moves, skips or overrides the user-level configuration or the policy.
Package managers read settings from environment variables before their configuration files, and some variables make them read another file or none. A variable set in a shell profile or a CI runner can switch the protection off without any file showing it.
Fix: Unset the variable, or set it to a value the policy allows.
Supported tools
What depcurfew configures and checks for each tool, from its knowledge base, where every value links to the official documentation or changelog it was verified against.
| Tool | Release-age delay | Exceptions | Install-script allow-list (Python: packages built from source; Composer: plugins) | Dependency sources | Verified |
|---|---|---|---|---|---|
| npm | min-release-age in .npmrc (days), since 11.10.0 | min-release-age-exclude, since 11.17.0 | allowScripts in package.json | git: allow-gitremote tarball: allow-remotelocal tarball: allow-filelocal directory: allow-directory | 2026-10-28 – 2026-11-02 |
| pnpm | minimumReleaseAge in pnpm-workspace. (minutes), since 10.16.0 | minimumReleaseAgeExclude, since 10.16.0 | allowBuilds in pnpm-workspace. | git: blockExoticSubdepsremote tarball: blockExoticSubdeps | 2026-10-28 – 2026-11-02 |
| Yarn (Berry) | npmMinimalAgeGate in .yarnrc.yml (minutes), since 4.10.2 | npmPreapprovedPackages, since 4.10.0 | dependenciesMeta. in package.json | git: approvedGitRepositories | 2026-10-28 – 2026-11-02 |
| Bun | install. in bunfig.toml (seconds), since 1.3.0 | install., since 1.3.0 | trustedDependencies in package.json | none | 2026-10-28 – 2026-11-02 |
| Deno | minimumDependencyAge in deno.json, .npmrc (minutes), since 2.5.5 / 2.6.0 ⚠️ | minimumDependencyAge., since 2.5.5 | allowScripts in deno.json | none | 2026-10-28 – 2026-11-02 |
| uv | exclude-newer in uv.toml, pyproject.toml (duration string), since 0.9.17 | exclude-newer-package, since 0.9.25 | no-binary-package in pyproject.toml | none | 2026-10-30 – 2026-11-02 |
| pip | uploaded-prior-to on the command line or in the environment (no project file) (days), since 26.1.0 | none | --no-binary in requirements.txt | none | 2026-10-30 – 2026-11-02 |
| Poetry | solver. in poetry.toml (days), since 2.4.0 | solver., since 2.4.0 | installer. in poetry.toml | none | 2026-10-30 – 2026-11-02 |
| PDM | exclude-newer in pyproject.toml (duration string), since 2.26.9 | exclude-newer-override, since 2.29.1 | no-binary in pyproject.toml | none | 2026-10-30 – 2026-11-02 |
| Pipenv | cool-down-period in Pipfile (duration string), since 2026.6.2 | none | none | none | 2026-10-30 – 2026-11-02 |
| Composer | config. in composer.json (duration string), since 2.11.0 | config., since 2.11.0 | config. in composer.json | none | 2026-11-02 |
| Renovate | minimumReleaseAge in renovate.json, renovate.jsonc … (duration string), since 35.46.0 | packageRules: matchPackageNames + minimumReleaseAge: null, since 35.46.0 ❓ | none | n/a | 2026-10-28 – 2026-11-02 |
| Dependabot | cooldown in ., . (days), since 2025 07 01 | cooldown.exclude, since 2025 07 01 ❓ | none | n/a | 2026-10-28 – 2026-11-02 |
⚠️ official sources disagree (depcurfew takes the safer reading) · ❓ official sources are incomplete. Every value with its source: docs/knowledge.md.
Yarn Classic (1.x) is detected but not supported.
Where Node.js and Python come from. doctor also reads the files that choose the runtime: .nvmrc, .node-version, engines.node, Volta, .tool-versions, devEngines.runtime, actions/setup-node in workflows (with node-version-file and matrix values), and Dockerfiles (Dockerfile, *.Dockerfile, Dockerfile.<name> and Containerfile in the root, in direct subdirectories without a manifest of their own and in workspace packages, with FROM node:<tag> or FROM python:<tag>), and the image of a job container (container: node:20, read like a FROM). An unpinned npm is the one that comes with that Node.js: Node.js 22 comes with npm 10, which ignores the release-age setting without a warning. With installScripts.default: "deny", apply writes strict-allow-scripts=true when any of these places runs npm 11.16 to 11.x (the pinned npm, an npm a CI job or Dockerfile installs, or the npm of a declared Node.js), also when another place runs npm 12, and says what that means for npm 12; doctor offers the setting with "which depcurfew apply writes" only where apply does. manager-version-too-old reports such a runtime where its npm (or the pip of the official python image) lacks a setting in use, unless the CI job or the build stage installs another one first (npm install -g npm@<version>, pip install pip==<version>), which is then checked the same way: a range (npm@^11.10.0) by its lowest version, and a version depcurfew cannot read (npm@latest, npm@${NPM_VERSION}) counts as an npm that runs every dependency install script. A place whose installs are all npm ci needs no release-age setting, because npm ci installs exactly the versions of package-lock.json; one whose installs run with scripts off (--ignore-scripts, npm_config_ignore_scripts=true in env:, ENV or on the command) needs no script blocking and counts for no npm that runs scripts. When the npm a fix names does not run on the Node.js of the place (npm 11 needs Node.js ^20.17.0 || >=22.9.0), the fix says to move to such a Node.js first. A pip install in a CI job or in a stage of the python image needs PIP_UPLOADED_PRIOR_TO (in env:, ENV or on the command) or --uploaded-prior-to with at least the days of the policy, because pip reads no configuration from the project; a job or stage that sets PIP_CONFIG_FILE, runs pip config set or writes a pip.conf is not checked. A stage whose image tag does not tell its Node.js or Python (a digest alone, an unknown tag) gives a note with a fix. runtime-end-of-life reports Node.js lines past their end of life. corepack-unavailable reports a packageManager or devEngines.packageManager pin of Yarn or npm, which only Corepack reads, where a declared Node.js is 25 or later: Node.js 25.0.0 stopped distributing Corepack, so the pin has no effect there until Corepack is installed (npm install -g corepack, then corepack enable, or corepack enable npm for npm). pnpm 10 and later switches to its pinned version itself and npm checks devEngines.packageManager itself, so those pins are not reported, and neither is a CI job or Dockerfile stage that installs or runs Corepack, or installs the pinned npm release itself (npm install -g npm@<version>), before it installs with the pinned tool; one that installs another npm release first gets the warning with that release named. depcurfew explain lists what it found.
Python projects
depcurfew configures uv, Poetry, PDM, Pipenv and pip from the same depcurfew.json, and the bots for Python packages: Dependabot pip and uv entries and Renovate.
- Detection. A Python tool counts as used when its lockfile is there, when
pyproject.tomlhas its table, or when its own configuration file or a CI setup step shows it;requirements.txtmeans pip.depcurfew explainlists what it found. - Package names. Python packages on the policy's package lists (the cooldown exceptions,
installScripts.allowandinstallScripts.deny) take the prefixpypi:, for example"pypi:docopt". Names without a prefix stay npm names, so an exception for an npm package never applies to a Python package of the same name. - Install scripts are builds. A wheel installs without running package code; a source distribution, a git repository or a local directory is built, which runs its build code.
installScripts.default: "deny"forbids building, and the packages ininstallScripts.alloware the exceptions. None of the three tools can only allow a build, so these packages are always built from source, even where a wheel exists;applysays so, anddoctorlists the packages in the lockfile that would be built. Packages ininstallScripts.denymust not be built:doctorreports one that a tool would still build (Poetry, PDM, pip and Pipenv build git, local and URL dependencies whatever their settings say), and one without wheels that cannot be installed while building is blocked. - pip reads no project configuration.
applywrites the build options intorequirements.txt. The release-age delay cannot live in the project, soapplyprints the environment variable to set where pip runs.doctoralso checks the files a requirements file includes with-rand-c, and reports an include it cannot read. - Lock again after
apply.uv.lockrecords the release-age delay, and PDM checkspdm.lockagainst all of its resolution settings, the delay and the build ban included. Afterapplychanges them,uv sync --lockedanduv lock --checkfail until you runuv lock, andpdm lock --checkandpdm install --checkuntil you runpdm lock;applysays so (lockfile-out-of-date), and so doesdoctor(the rulelockfile-out-of-date) while the lockfile is not locked again: it compares whatuv.lockrecords with the settings as uv does, and the hash inpdm.lockwith the one PDM computes frompyproject.toml.poetry.lockandPipfile.lockdo not depend on the settingsapplywrites. - PDM and Pipenv cannot pin their own version.
applyconfigures for the latest version depcurfew knows and names that version (version-not-pinned). Pipenv takes the delay in whole days only (rounded up), has no exceptions and no project setting that forbids building;applysays what to set where Pipenv runs instead.doctorreadspdm.lockandPipfile.lock, and reports settings that turn the delay or the build ban off without a word: Pipenv's pure-Python resolver and PDM's uv mode. - Hatch has no settings of its own. Its environments install with uv or pip, so a Hatch project is covered by the uv configuration (add
"uv"totargetswhen its environments use the uv installer) or by the environment variables of pip. - Running depcurfew. depcurfew is a Node.js program. In a project without
package.json, run a pinned version withnpx --yes depcurfew@<version>; in CI the GitHub Action needs nothing else.
Update bots
apply gives Renovate and Dependabot the policy's delay, rounded up to their unit and at least as long as the delay of every package manager after rounding, so that they never propose a version the package managers would refuse. It edits only bot configuration files that exist and adds no entries to them.
- Dependabot gets
cooldownin eachupdatesentry of an ecosystem depcurfew configures (npm,bun,deno,pip,uv,composer), with the policy's exceptions for that registry underexclude. - GitHub Actions. When the repository has workflows in
.github/workflowsandtargetsis"auto"or names the bot, the actions they run wait as long as the packages. Thegithub-actionsentries of Dependabot get the samedefault-days(GitHub Actions reads nosemver-*-days), and theirexcludestays as written, because the policy's exceptions name packages, not actions. The delay at the top level of the Renovate configuration covers itsgithub-actionsmanager too.doctor(bot-cooldown-mismatch) compares the bot's delay for the actions withcooldown.minimum. - Renovate gets the delay at the top level of its configuration and a
packageRulesentry for the exceptions.applyanddoctorread the otherpackageRulesentries the way Renovate matches them, also bymatchManagersandmatchCategories:npmcovers the packages of npm, pnpm and Yarn,bunanddenothose of Bun and Deno,pep621those of uv and PDM,poetry,pip_requirementsandpipenvthose of Poetry, pip and Pipenv,composerthose of Composer, andgithub-actionsthe actions of the workflows; the categories arejs,python,phpandci. Globs,/regex/,!and a single string instead of a list work as in Renovate. A rule that shortens the delay of a package manager in use (or of the actions) givesbot-rule-shortens-delayinapplyandbot-cooldown-mismatchindoctor, and one that turns it off givesbroad-exclusion. For the actions, a rule withmatchFileNamesthat matches every workflow file of the repository (read as Renovate reads it:*within one directory,**across them, any case, or/regex/) and a rule withmatchDepTypesthat matchesaction, the depType Renovate gives an action of a workflow step, count too; one that matches only some workflow files, or only reusable workflows (workflow), leaves the other actions their delay and does not count. A rule with a glob matcher depcurfew cannot read (braces, character classes,?, or extglobs such as@(a|b)and+(…)) does not count, anddoctorgives a note when it may set a shorter delay; so does a rule with a matcher depcurfew does not evaluate (matchBaseBranches,matchJsonata,matchRepositories,matchCurrentAge, andmatchFileNamesormatchDepTypesfor the packages of a package manager), unless a package matcher already limits it to some packages. When rules give the update types different delays,doctorreports each one that is shorter, not only the shortest. - Numbers in
dependabot.ymlare read as Dependabot's YAML library, Ruby's Psych, reads them, which follows YAML 1.1:default-days: 010is 8 days (octal),1_0and1,0are 10,1:30is 5400, and0o12is text.doctorcompares the number Dependabot reads with the policy and says how it read it ("default-days: 010 in .github/dependabot.yml, which Dependabot reads as 8"), andapplywrites the policy's number in place of one Dependabot reads otherwise. - YAML aliases. Dependabot does not accept YAML aliases (
*name) in.github/dependabot.yml(dependabot-core loads the file without them), sodoctordoes not read a delay or an exception through one:bot-cooldown-mismatchgives a note at the first alias instead of a result,applywrites thecooldownof an entry out in place of an alias and warns that Dependabot reads nothing from the file (bot-config-unreadable), andinitnotes it, since it does not keep a longer delay from such a file incooldown.minimum. GitHub Actions workflows may use aliases:doctorand detection read theenvmaps, jobs and steps of a workflow through them, at most 100 aliases per file. - How old an action is comes from git. Renovate takes the commit date of a tag, the tagger date of an annotated tag, or the date of a GitHub release of that version when that is newer, and holds an update without a date; Dependabot takes the date of the GitHub release, then of the tag, then of the commit, and lets an update without a date through. Whoever makes a commit or a tag sets its git date, so the delay protects actions less than packages from a registry, which records when a version was published.
Composer projects
depcurfew configures Composer (PHP packages from Packagist) from the same depcurfew.json, and the bots for Composer packages: Dependabot composer entries and Renovate.
- Detection.
composer.jsonorcomposer.lockin the directory means Composer. Only the rootcomposer.jsoncounts: Composer readsconfigandrepositoriesfrom it alone. Composer readscomposer.jsonas strict JSON, soapplydoes not edit one with a comment or a trailing comma (cannot-edit, with the line). - Package names. Composer packages on the policy's package lists take the prefix
composer:, for example"composer:monolog/monolog"or"composer:acme/*". Composer names are lowercasevendor/name; a name without a prefix stays an npm name. Composer reads the keys ofallow-pluginsandignorewithout regard to case, and its*there matches/too, so depcurfew compares them the same way:"Symfony/Flex": trueis the plugincomposer:symfony/flex. - The release-age delay needs Composer 2.11.0.
applywrites the delay of the cooldown policy in whole days, rounded up ("36h"becomes"2 days"); a period that is already as long in another form (259200seconds,"72 hours") stays as it is. Composer reads"policy": trueand"cooldown": trueas{}, soapplywrites its object there; below"policy": false,"cooldown": falseor another value that is no object it writes nothing (cooldown-not-written), andexplainshows that Composer cannot apply the delay. Composer counts the delay frompublished-time, the moment Packagist received the version, which the package author cannot set. The delay applies whencomposer updateorcomposer requirepicks versions;composer installinstalls whatcomposer.locklocks, so review lockfile changes as before. Older Composer ignores the setting without a word, and Composer has no setting that pins its own version:doctorreports acomposer.lockwritten by a Composer older than 2.11 (itsplugin-api-version) and otherwise gives a note (manager-version-too-old). - Exceptions.
applywrites thecomposer:exceptions of the policy as theignorelist, adds them to a list that is there, or adds them to anignoreobject with a reason that namesdepcurfew.json. That reason marks an entry asapply's: once the policy drops the name from its exceptions,applyremoves it. Any other entry that exempts a package the policy does not stays, also a name of a list an earlierapplywrote, since a list has no room for a mark:applywarns (exclude-entry-outside-policy) anddoctorreports it (config-drift, a warning) with the line, until you remove it and runcomposer update --lock. - Lock again after
apply. Composer 2.11 hashesconfig.policyintocomposer.lock, so afterapplychanges it,composer installwarns that the lock file is out of date andcomposer validatefails;applysays so (lockfile-out-of-date), only in the run that changes the setting: a laterapplyanddoctordo not compute the hash ofcomposer.lockand do not repeat it. Runcomposer update --lock, which writes the new hash without updating packages, and commitcomposer.lock. Dependabot writescomposer.lockwith Composer 2.10.3, whose hash leavesconfig.policyout, so the same happens on each of its composer pull requests onceconfig.policyis there: runcomposer update --lockon the pull request's branch before merging (doctorsays so in its note on each Dependabotcomposerentry). - Install scripts are plugins. Composer runs the scripts of the root
composer.jsononly; the code of dependencies that runs during Composer commands is plugins (typecomposer-plugin, and the legacycomposer-installer, in any case). WithinstallScripts.default: "deny",applywrites the plugins ofinstallScripts.allowintoallow-pluginsastrueand those ofinstallScripts.denyasfalse(patterns as they are, since Composer takes them). An unlisted plugin makes Composer ask, or fail without a terminal (one that declaresextra.plugin-optionalis skipped there).applyreplaces"allow-plugins": truewith the list,{}when the policy lists no plugins (allow-all-replaced), and never changes an entry it did not write: an earlier key that blocks a plugin the policy allows, such as a final"*": false, givesdeny-conflicts-with-filewith the fix to put the plugin before it.doctorlists the plugins incomposer.lockthatallow-pluginslets run but the policy does not allow, and those it does not list. Acomposer.lockwithoutplugin-api-version(or with one below 2.2.0) next to an empty or missingallow-pluginsmakes Composer 2.2 to 2.9 run every plugin without asking, sodoctorreports each plugin the policy does not allow as an error;composer update --lockrecordsplugin-api-version.applydoes not add a catch-all"*": falsefor this: Composer takes the first key that matches, and the pluginsapplyadds go at the end, where such a key would hide them. - No switch for dependency sources. Composer has no setting that forbids VCS, path, artifact or package repositories (
source-not-enforceable), but only the rootcomposer.jsondeclares them.doctorreports the packages ofcomposer.lockthat come from such a repository, and a repository of a denied kind whose packages it cannot find in the lockfile. A package ofcomposer.lockwithout adist, or with adistwithout a type, is installed from itssource, a VCS checkout, whatevercomposer.jsondeclares, so it counts as a VCS package, and so does one thatconfig.preferred-installofcomposer.jsonmakes Composer check out:"source", a pattern object whose first matching key gives anything but"dist"or"auto", and"auto"for a dev version (dev-main,1.x-dev). Composer's default"dist"takes the archive, also of a dev version. Such packages are one error per entry ofconfig.preferred-install, at its line incomposer.json, naming the packages with their lines incomposer.lock, and the fix is incomposer.json(remove the entry or set it to"dist"), orsources.git: "allow";initnotes them (composer-source-checkouts). An archive fromapi.github.com,gitlab.com,bitbucket.orgorcodeberg.orgnext to a source on another host than its own (github.comforapi.github.com; hosts are compared as URLs compare them, sogithub.com:443isgithub.com, andwww.github.comisgithub.com, as Composer's GitHub driver reads it) gives a warning while the policy denies VCS sources: Packagist.org never writes such an entry, and Composer installs from that source with--prefer-source, withpreferred-installset inCOMPOSER_HOME/config.json, and before 2.10.0 when the archive cannot be downloaded (2.10 does that only withsource-fallback, which 2.11 removed). An archive from a host that neither the archives of Packagist.org (api.github.com,gitlab.com,bitbucket.org,codeberg.org) nor a repository ofcomposer.jsonuse is a source depcurfew does not recognize: one warning per host while the policy denies a source, naming its packages with their lines. A Composer repository ofcomposer.jsonmay keep its archives on another host than its own; depcurfew knows that of WPackagist (https://wpackagist.orgservesdownloads.wordpress.org), and for another one, such aspackages.drupal.orgwith its archives onftp.drupal.org, the warning names the declared repositories and says how to check that one of them serves those archives, sincecomposer.lockdoes not say which repository a package comes from.doctor --osvlooks up every release ofcomposer.lock, also one from such a host and one Composer checks out of its source, but not a package of a VCS repository ofcomposer.json, whose code is not the release on Packagist. - Settings that turn protections off. The rule
composer-config-weakensreports"policy": false, acooldownorblockturned off, malware blocking turned off or limited to updates, a repository with"require-published-time": falseor"filter": false,"allow-plugins": true(or a key that matches every plugin,"*"or"*/*") while the policy denies plugins,"secure-http": false,"disable-tls": true, andCOMPOSER_POLICY=0,COMPOSER_NO_BLOCKINGorCOMPOSER_NO_SECURITY_BLOCKINGset to1,true,on,falseoroff(any other value makes Composer stop with an error), a shorterCOMPOSER_POLICY_COOLDOWN_PERIOD,COMPOSER_POLICY_COOLDOWN_BLOCK=0orCOMPOSER_POLICY_MALWARE_BLOCK=0in theenvof a CI workflow. - Bots.
applygives Dependabotcomposerentries the cooldown and thecomposer:exceptions, and Renovate a rule for them limited to thepackagistdatasource. Dependabot counts its composer cooldown from the release time the package author writes and updatescomposer.lockwith a Composer without the cooldown, sodoctorgives each such entry a note; Renovate counts frompublished-time. - Checking before an install.
depcurfew check composer:vendor/name(alsocomposer:vendor/name:^3.9, ascomposer requiretakes it), the Claude Code hook (composer require,composer updateof named packages,composer global require,composer create-project) andcheck_packagesofdepcurfew mcplook Composer packages up on Packagist: see Guarding installs. - Not covered.
doctorreads the Composer variables of theenvmaps of a workflow, not options and variables in arunstep (composer update --no-cooldown,--no-blocking,COMPOSER_POLICY=0 composer update).COMPOSER_HOME/config.jsonandCOMPOSER_HOME/composer.json(Composer's configuration for the whole machine and its global project) are not read or written, so a repository, aminimum-stabilityor apreferred-installthey set is not seen bydoctor,checkor the hook; the hook asks about a command that setsCOMPOSER_HOMEitself, and aboutcomposer global updateof a package named without a constraint, whose constraint is inCOMPOSER_HOME/composer.json. Command-line options in CI steps, such as--prefer-source, are not read either.
doctor rules
Findings have a level: an error makes depcurfew doctor exit with 1, a warning does too when doctor.failOn in depcurfew.json or --fail-on is warning (the policy of depcurfew init sets it; without it a warning does not), and a note never does. When warnings make doctor fail, the line under the counts says where that was set, with how to fail on errors only, and --json has failOn (error or warning), failOnFrom (option, policy or default) and strict (true when warnings fail). Things doctor could not check (a binary lockfile, a lockfile format without install-script data, a missing policy) are listed as notes after the findings, not as findings: each line starts with note and ends with its code (the rule that could not check something, or a code such as policy-missing, or script-not-followed for a CI run step or a Dockerfile RUN that changes the directory more than 256 times or to a path longer than 4096 characters, or sets more than 1024 variables, after which depcurfew does not follow the directory or the variables), with what to do on the next line when there is something to do, and the line of counts counts them as notes. In --json each entry of notices has level (note), code, location and file when it concerns one file (location.line too when it concerns one line, such as the step or instruction of script-not-followed, which SARIF uses for the region), message, and fix when there is one; summary counts the findings only. Findings point at the line, and for a syntax error also the column (location.column), where the problem is, also a problem of depcurfew.json, which gets the line of the field it names. Below them, doctor says what it checked: the package managers and bots it found with the version it checked against, the files it read and every check with what it checks and its result, passed, the problems it found, skipped when it needs a policy (needs a valid depcurfew.json when the file is there but invalid), or not applicable when it does not concern the project, such as the Python checks without a Python tool or the bot checks without an update bot (checked in --json, where the result is passed, problems, skipped or not-applicable).
invalid-policy
error · depcurfew.json does not follow the schema or cannot be read.
Without a valid policy, the rules that compare the repository with depcurfew.json cannot run, so doctor runs only the rules that need no policy, as when there is no policy file.
Fix: Fix what the message lists; an editor that reads the $schema of depcurfew.json shows the problems as you type.
invalid-project
error · needs depcurfew.json · A directory listed in projects in depcurfew.json does not exist, is not a directory, is a symlink, has a depcurfew.json of its own or overlaps a workspace package of the root.
depcurfew checks and configures each listed directory as a project of its own. A directory that is missing or misspelled is never checked, so its package managers keep whatever configuration they have; one with a depcurfew.json of its own would follow two policies, and a workspace package of the root is configured with the root already. depcurfew apply writes nothing while one of them is listed.
Fix: Correct or remove the entry in projects, or remove the depcurfew.json of the project.
depcurfew-outdated
warning · The project installs a newer depcurfew than the one running.
A global install or an old cached copy may not know settings, rules or tool versions that the version pinned in the project knows, so it can miss problems or report fixed ones. The project copy is read from node_modules or package-lock.json, in the project and in the tools directory .claude/depcurfew that the README recommends for a repository without package.json in its root.
Fix: Run the project's copy of depcurfew (install the project's dependencies first when only package-lock.json pins it), or update the one you run.
manager-version-too-old
error · A declared package manager version, or the npm or pip that comes with a declared Node.js or Python version, is older than a setting the repository relies on.
Package managers ignore settings they do not know yet; npm 10 does not even warn. When packageManager, devEngines, Volta, .tool-versions, a CI step or a Dockerfile pins an older version, the setting is silently not applied there. The same holds for the npm that comes with the Node.js a CI job, a Dockerfile stage or a version file (.nvmrc, .node-version) chooses, and for the pip of a python image: Node.js 22 comes with npm 10, which has no min-release-age. Composer cannot pin its own version: a composer.lock whose plugin-api-version shows that a Composer older than 2.11 wrote it gives a warning, and otherwise a note says that the cooldown needs Composer 2.11.
Fix: Raise the declared version to at least the version that introduced the setting, everywhere it is declared, or install a newer npm or pip before installing packages.
runtime-end-of-life
warning · A declared Node.js version is past its end of life.
After its end of life a Node.js release line gets no more releases, security fixes included. A version file, CI step or Dockerfile that only allows such lines keeps running an unpatched Node.js, and an older npm with it.
Fix: Move to a Node.js line that is still supported, in every place that declares the version.
corepack-unavailable
warning · packageManager or devEngines.packageManager pins Yarn or npm, but a declared Node.js is 25 or later, which no longer comes with Corepack.
Corepack reads packageManager (and devEngines.packageManager) and runs the pinned Yarn, or npm after corepack enable npm. Node.js 25.0.0 stopped distributing Corepack, so where a version file, CI job or Dockerfile chooses Node.js 25 or later, the pin has no effect unless Corepack is installed separately: whatever Yarn or npm is there runs, or none. pnpm 10 and later switches to its pinned version itself, npm checks devEngines.packageManager itself, and every Yarn from 1.18 runs a release committed under yarnPath in .yarnrc.yml, so those pins are not reported; nor is a CI job or Dockerfile stage that installs or runs Corepack, or installs the pinned npm release itself, before it installs with the pinned tool.
Fix: Install Corepack where that Node.js runs (npm install -g corepack, then corepack enable), before the pinned tool runs.
setting-ignored-by-version
error · A setting or file is ignored by the package manager version in use.
Package managers drop old settings and files in new major versions and only accept new forms from some version on. The configuration stays in the repository and looks effective while the version in use ignores it.
Fix: Move the setting to the place and form this version reads.
npmrc-overrides-policy
error · An .npmrc file changes a security setting in a way the policy does not see, or is ignored.
Package managers read .npmrc from different places with different priorities. npm ignores the .npmrc of workspace packages and the keys below a [section] header, and a before date in .npmrc takes the place of min-release-age; pnpm 10 applies a root .npmrc value whenever pnpm-workspace.yaml does not set the same setting, which can weaken the policy, and dangerously-allow-all-builds there runs every build script while pnpm-workspace.yaml has no build settings of its own.
Fix: Keep security settings out of .npmrc files that are ignored or that override the policy; set them through depcurfew.json and depcurfew apply.
pnpm-shared-workspace-lockfile
warning · pnpm 10 without a shared lockfile lets a package .npmrc change the release-age delay.
With sharedWorkspaceLockfile: false, pnpm 10 installs every workspace project separately with that project's own .npmrc layered over the workspace settings (read from the pnpm 10 source code), so a setting such as minimum-release-age=0 in a package .npmrc applies to that package.
Fix: Remove security settings from package .npmrc files, or keep the default shared lockfile.
python-config-ignored
error · A Python tool ignores a setting where it is written.
uv reads uv.toml instead of [tool.uv] when both exist, and in a workspace only the root; pip reads no configuration from the project and rejects options it does not know in requirements files, so a CI job or a Dockerfile stage of the python image installs without the release-age delay unless it sets PIP_UPLOADED_PRIOR_TO or passes --uploaded-prior-to; PDM in uv mode installs without only-binary; Pipenv with resolver_backend = "pure-python" locks without cool-down-period. A setting in such a place looks effective but is not, and an exclude-newer date freezes dependencies instead of delaying new versions.
Fix: Move the setting to the file the tool reads.
composer-config-weakens
error · composer.json or the env of a CI workflow turns a Composer protection off: the cooldown, malware blocking, allow-plugins, secure-http or TLS.
Composer 2.11 withholds new versions with config.policy.cooldown and runs only the plugins config.allow-plugins allows. "policy": false, "cooldown": false or "block": false under it, a repository with "require-published-time": false (the cooldown then counts from the time the package author wrote) or "filter": false (no malware list), "allow-plugins": true (or a "*" or "*/*" key) and "secure-http": false or "disable-tls": true turn such a protection off while the configuration still looks right; so do COMPOSER_POLICY=0, COMPOSER_NO_BLOCKING or COMPOSER_NO_SECURITY_BLOCKING set to 1, true, on, false or off, COMPOSER_POLICY_COOLDOWN_PERIOD with a shorter period and COMPOSER_POLICY_COOLDOWN_BLOCK=0 in the env of a CI workflow. Settings of the cooldown are reported when depcurfew.json sets one, allow-plugins when it denies dependency code; malware blocking and filter give warnings, because depcurfew.json does not manage them. COMPOSER_HOME/config.json on a machine is not read.
Fix: Remove the setting or the variable, or limit it to the repository or the run that needs it.
config-drift
error · needs depcurfew.json · A configuration file does not match what depcurfew apply writes for depcurfew.json.
A generated setting was edited by hand, or depcurfew.json changed without running depcurfew apply, so the tools enforce something other than the policy. Entries in a tool's own install-script lists that apply does not change are reported as well: one that lets a package of installScripts.deny run its scripts is an error, like the differences depcurfew apply fixes, and one that lets a package the policy does not name run them, or that blocks a package the policy allows, is a warning. So is an entry of config.policy.cooldown.ignore in composer.json that exempts a package cooldown.exclude does not, which apply keeps unless it wrote the entry itself.
Fix: Run depcurfew apply (depcurfew apply --dry-run shows the difference), or change depcurfew.json if the edit was intended.
bot-cooldown-mismatch
error · An update bot waits less than the package manager, or for the actions of workflows less than the policy, before proposing a new version.
The bot then opens pull requests with versions the package manager would not pick yet: they fail where the manager re-checks the lockfile or bring in younger versions, and security updates that skip the bot delay fail too. A Dependabot cooldown with an include list delays only the dependencies it names; a Renovate packageRules entry can shorten the delay for a registry, manager, category, update type or the files and depType of actions, each such entry is reported, and one with a matcher depcurfew cannot read or does not evaluate gets a note. For the actions of workflows the bot must wait cooldown.minimum. An unreadable bot file, a JSON5 Renovate file or a Dependabot file with a YAML alias gets a note per package manager that waits. With projects, a bot file inside a project gets a note (bots read only the root's), and a project the root's Dependabot configuration leaves out while it updates others gets a warning. A Dependabot composer entry gets a note: it counts from the release time the author sets.
Fix: Give the bot a delay at least as long as the package manager's, and for the actions of workflows as long as cooldown.minimum (depcurfew apply does this).
broad-exclusion
error · An exception list matches every package, or a per-scope setting shortens the delay, which turns the protection off.
A release-age exception such as "*" (or a policy allow-list entry such as "@*/*") looks like a narrow exception but exempts every package, so the delay or the install-script block never applies. The same goes for a Renovate packageRules entry that turns minimumReleaseAge off for a whole datasource, Renovate manager (matchManagers) or category (matchCategories) in use, for major, minor or patch updates of every package, or with only negative patterns, which match every package they do not name, and, when depcurfew.json sets a cooldown for Renovate and the repository has workflows, for the github-actions manager, every workflow file (matchFileNames) or the depType action (matchDepTypes). In Yarn 4.17 and later, npmScopes.<scope>.npmMinimalAgeGate replaces the delay for one scope, so a shorter value there exempts that scope.
Fix: Replace the pattern with the packages or scopes that really need the exception.
lockfile-forbidden-source
error · needs depcurfew.json · The lockfile installs a package from a source depcurfew.json denies.
A git repository, a tarball URL or a local path bypasses the registry and its checks (release age, provenance, integrity from the registry). The lockfile shows what installs really fetch, whatever the package manager settings say. For Composer, whose lockfile does not name the repository of a package, packages of a path or artifact repository are told by their dist, those of a VCS or package repository by the repositories of composer.json; a package without a usable dist, or one that config.preferred-install makes Composer check out, is a VCS checkout. Warnings: archives from a host that neither the archives of Packagist.org (GitHub, GitLab, Bitbucket, Codeberg) nor a repository of composer.json use (with the archive host depcurfew knows for one, such as downloads.wordpress.org for WPackagist), one per host, such an archive next to a source on another host, and a repository of a denied kind that no package of composer.lock comes from.
Fix: Replace the dependency with a registry version, or allow that source in depcurfew.json.
lockfile-install-scripts
error · needs depcurfew.json · The lockfile has packages with install scripts, or Python packages built from source, that depcurfew.json does not allow.
Install scripts run arbitrary code during installation, and so do building a Python package from source (a source distribution, a git repository or a local directory) and loading a Composer plugin. Where the package manager runs them by default, or its own allow-list lets them run, they run code the policy does not allow; where it blocks them, a package may not work. A package in installScripts.deny works without its scripts, so it is reported only while the package manager in use still runs them (or builds it from source), or when it cannot be installed (a Python package without wheels, esbuild without optional dependencies). npm, Bun (1.4.4+) and Deno (lockfile v5) record install scripts in the lockfile and composer.lock records plugins; uv.lock, poetry.lock, pdm.lock and pylock.toml show which Python packages have no wheel. pip requirements files and Pipfile.lock list hashes only, so the check does not apply to their packages from an index, only to requirements from a repository, a URL or a local path.
Fix: Remove or replace the package, or review it and add it to installScripts.allow in depcurfew.json, or to installScripts.deny when it works without its scripts (Python packages with the prefix pypi:, Composer plugins with composer:). For a package in installScripts.deny, make the package manager block its scripts (depcurfew apply records the denial where the tool can). For packages whose scripts depcurfew knows (esbuild, fsevents, lefthook), from a registry, the finding names the fix: installScripts.deny, and for lefthook running lefthook install after each clone, as its documentation says for projects that do not use its postinstall script.
lockfile-out-of-date
warning · A uv.lock or pdm.lock was locked with other settings than the project has now.
uv records the release-age delay (exclude-newer and exclude-newer-package) in uv.lock, and PDM checks pdm.lock against a hash of the dependencies, the sources and the [tool.pdm.resolution] table, the delay and the build ban included. After a change, by depcurfew apply or by hand, uv sync --locked, uv lock --check, pdm lock --check and pdm install --check fail until the lockfile is locked again, and the lockfile still holds what was resolved under the old settings.
Fix: Lock again (uv lock or pdm lock) and commit the lockfile.
known-malicious
error · With --osv: a lockfile locks a package version that OSV.dev lists as malicious.
With --osv, doctor sends the name and version of every package the lockfiles it reads lock (npm, pnpm, Yarn, Bun and Deno lockfiles, uv.lock, poetry.lock, pdm.lock, Pipfile.lock, pylock.toml, requirements pinned with == and the releases in composer.lock) to api.osv.dev in batch queries, and reports each version that has a record of the OpenSSF malicious-packages database there (an id starting with MAL-). Such a version was published to steal or damage, and installing from the lockfile brings it back. It looks up what the tool installs: for npm the registry tarball of resolved, for pnpm the one of resolution.tarball (each path read as the registry reads it), for Yarn the version of resolution; an entry whose name or version says otherwise gets a warning, as does a registry entry without an exact version. Only reported malware is found, and the database admits false positives. Without --osv nothing is sent and the rule does not run; when OSV.dev cannot be reached it gives a warning instead of passing.
Fix: Remove or replace the package version and lock again; if it was installed anywhere, read the record on OSV.dev and treat that machine as exposed.
unreadable-config
error · A configuration file or lockfile cannot be read, parsed or edited, so depcurfew cannot check it.
A package manager or bot may still read the file and its settings may weaken the policy; depcurfew fails closed and reports an error instead of passing silently.
Fix: Fix what the message describes: invalid syntax, missing read permission, a symlink, a file over the size limit or a style depcurfew apply cannot edit.
GitHub Action
The action runs depcurfew doctor, prints the report to the job log, shows each finding as an annotation on its file and in a table in the job summary, each with how to fix it, uploads the SARIF report to code scanning and fails the job when doctor finds an error, or also a warning when doctor.failOn in depcurfew.json is warning (the input fail-on is empty by default and follows it) or the input fail-on is warning (as depcurfew doctor --fail-on warning); fail-on: error fails on errors only, whatever the policy says, and the depcurfew doctor check fails or passes the same way. The log and the job summary also say what was checked: the tools found, the files read and every check with its result (passed, the problems it found, skipped because it needs depcurfew.json, or not applicable to the project). A clean run adds a notice with the same list ("No problems found in 7 files for npm 11.21.1 (package.json). 14 checks: 9 passed, 5 not applicable."), so it is not mistaken for one that did nothing. Code scanning lists only alerts, so its entry stays empty when there is nothing to report; next to it, in the Checks tab of a pull request, the action adds the check "depcurfew doctor" with the result, the table of checks and the findings as annotations on their lines (at most 100, errors first; a finding about a whole file is shown on its first line). It needs checks: write, which the example below grants; without it the step only says so, as a warning in the log. The lines the action writes about its own run start with depcurfew: and their level (error, warning or note); its notes and warnings are also annotations of the same level (unless annotations is false), and its errors are the error annotation of the failing step. A workflow that runs on both push and pull_request checks the same commit twice: the second run updates the entry of the first one instead of adding another, unless both run at the same moment, since GitHub has no way to look for an entry and add one in a single step. Annotations cannot be removed from an entry, so when the first entry has annotations other than the second run's findings, the second run adds a new entry and marks the first one as replaced (neutral, with a title that says so), rather than leave findings that no longer hold under the new result. It runs from its own checkout, with nothing to install, and sets up Node.js 22 only when the runner has an older version.
Pin it to a full commit SHA, like every action in your workflows; the example pins the commit of a release, and the comment names its tag. Pin a release at least as new as the depcurfew your project uses elsewhere (in package.json, the hook or npx): an older Action reports policy fields it does not know as an invalid policy, such as doctor and projects, which are new in 1.3.0 and which depcurfew init 1.3.0 writes, and names it does not know, such as composer: names (new in 1.4.0); it does not check Composer projects before 1.4.0 and ignores inputs it does not have, such as osv (new in 1.4.0). The example pins the release that was current when this README was written; when you install a newer depcurfew, pin the commit of its tag (git ls-remote https://github.com/DevXord/depcurfew refs/tags/v<version>). If the repository cannot use code scanning, set upload-sarif: false; the findings stay in the job log. osv: true adds the rule known-malicious, as depcurfew doctor --osv does: the runner then sends the locked package versions to api.osv.dev (see Known malicious versions).
name: depcurfew
on:
push:
pull_request:
jobs:
doctor:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
security-events: write
# The depcurfew doctor check with the result in the Checks tab:
checks: write
# Only needed in private repositories:
actions: read
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: DevXord/depcurfew@20fcc21389f308180a2f9a3a404a486f08705c15 # v1.4.0
| Input | Default | Description |
|---|---|---|
directory | . | Directory to check, relative to the workspace. |
sarif-file | depcurfew.sarif | Where to write the SARIF report, relative to the workspace; a path outside it works too. |
upload-sarif | true | Upload the SARIF report to GitHub code scanning. The job needs the security-events write permission. |
category | depcurfew | Code scanning category of the upload, to tell several depcurfew runs in one workflow apart. |
fail-on | empty | Which findings fail the step and the depcurfew doctor check. Empty (the default) follows doctor.failOn in depcurfew.json, which depcurfew init sets to warning, and fails on errors only without it; error or warning overrides the policy, and warning makes warnings fail them too, as depcurfew doctor --fail-on warning does. Notes never do. Any other value fails the step. |
fail-on-error | true | Deprecated, use fail-on. false keeps the step from failing on findings at all, whatever fail-on says, and makes the depcurfew doctor check neutral instead of failed. |
annotations | true | Show the findings as annotations on the files, each with how to fix it, or one notice when there are none, which lists every check and its result. The notes and warnings of the action about the run itself (a directory checked with the root that lists it under projects, a depcurfew doctor check that could not be added) are annotations too, of the same level as in the log. GitHub shows a limited number per step; the log lists every finding. Turn off when code scanning already shows them. |
job-summary | true | Add a table of the findings, each with how to fix it, and what was checked (tools, files read, every check with its result) to the job summary. |
check-run | true | Add a check named "depcurfew doctor" to the commit, with the result, every check and the findings as annotations on the files (at most 100, errors first), shown next to the code scanning entry in the Checks tab of a pull request. A later run on the same commit, such as push and pull_request of one commit, updates it instead of adding a second one. The job needs the checks write permission; without it the step only says so, as a warning in the log. |
github-token | ${{ github.token }} | Token for the depcurfew doctor check; the workflow token by default. |
osv | false | true also looks every locked package version up in OSV.dev and reports a known malicious version as an error (the rule known-malicious, as depcurfew doctor --osv does); it sends the package names and versions to api.osv.dev. false (the default) sends nothing. A version OSV.dev did not answer for gives a warning. Any other value fails the step. |
| Output | Description |
|---|---|
errors | Number of findings with level error. |
warnings | Number of findings with level warning. |
notes | Number of findings with level note. |
sarif-file | Path of the SARIF report, relative to the workspace when it is inside it, otherwise absolute; empty when doctor could not run or the report could not be written. |
Other CI systems
The action works only on GitHub. On GitLab CI or any other CI, install the project's dependencies without running their install scripts, so nothing from the lockfile runs before doctor has checked it, then run the depcurfew version the lockfile pins; the job fails when doctor finds an error, or a warning when doctor.failOn is warning (exit code 1):
depcurfew:
image: node:22
script:
- npm ci --ignore-scripts
- npx --no depcurfew doctor
--no makes npx fail instead of downloading the latest depcurfew when the project does not have it; in CI npx otherwise downloads without asking. With pnpm, Yarn or Bun, use their frozen-lockfile install with install scripts turned off instead of npm ci --ignore-scripts.
depcurfew doctor --json and --sarif write the findings for other tools; set FORCE_COLOR=1 if the CI log shows colors.
What depcurfew does not do
depcurfew makes the package managers' own protections work and checks a package's age, name, install scripts and source before it is installed. It has no malware database of its own and does not scan package contents, so by default a malicious version older than the cooldown passes. With --osv (check, the Claude Code hook, depcurfew mcp, doctor and the input osv of the Action) it asks OSV.dev about the versions it checks and blocks those with a record of the OpenSSF malicious-packages database, but that catches only malware someone has found and reported, and the database admits false positives: see Known malicious versions. Keep npm audit or an equivalent for known vulnerabilities, which --osv does not report. depcurfew also does not run or sandbox anything: the protection comes from the configuration the package managers read.
Guarding installs
The configuration apply writes is what really protects a project: the package managers themselves wait out the release-age delay and refuse install scripts and sources the policy denies. depcurfew check adds a check before something is installed, which matters most for AI coding agents that add packages on their own:
npx depcurfew check zod@4 left-pad
npx depcurfew check pypi:requests "pypi:django>=5.2,<6"
npx depcurfew check composer:monolog/monolog "composer:symfony/console:^7.4"
For each package it looks up the npm registry (the one --registry or registry= in .npmrc names, else the public one; any other is named in a note), PyPI for names with the pypi: prefix, or Packagist for names with the composer: prefix, and reports:
- a name the registry does not know, which is how made-up package names show up;
- a version younger than the cooldown in
depcurfew.json, with the newest version that is old enough; - install scripts the policy does not allow; for a Python package, a version without wheels, which is built from source and so runs its build code; for a Composer package, a plugin (type
composer-pluginor the legacycomposer-installer, in any case), whose code runs during Composer commands. A package ininstallScripts.denyis not reported, because its scripts are meant to stay blocked (doctorchecks that the package manager blocks them), except a Python package without wheels, which gets a warning: it cannot be installed where building is blocked; - a name that differs by a letter or two, or only in separators, from a popular package of its registry: 5,000 npm packages with the most weekly downloads among the results of several hundred registry searches (the npm registry has no download ranking, so a popular package can be missing), and the 5,000 most downloaded PyPI projects of a recent month (there is no such list for Packagist);
- a deprecated version, or an abandoned Composer package (a warning);
- a registry or index reached over plain HTTP (a warning), unless it is on this computer (
localhost,127.0.0.0/8or::1), such as a local registry for testing; - with
--osv, a version OSV.dev lists as malicious (known-malicious, an error), as described below.
A registry that redirects to another origin (another host, port or scheme, so also from http:// to https:// on the same host), and one at a link-local address, where cloud metadata services answer, is not followed or asked: the package counts as not checked, and so does a package whose registry document lists more than 100,000 versions (or a PyPI page with more than 100,000 versions or 200,000 files, or a Packagist answer larger than 16 MiB, with more than 10,000 versions or with a version longer than 255 characters), far more than any real package, because such a document is made to slow checks down. For a name without a version or with a range, npm prefers the dist-tag tag= in .npmrc names, and so does check.
.npmrc is read where npm reads it: ~/.npmrc, and the .npmrc of npm's local prefix, the nearest directory from the checked one up with a package.json or a node_modules directory (or the workspace root that lists it). Its registries and dist-tag count only when that directory is the one with depcurfew.json or a directory in its projects (without a depcurfew.json, the checked directory): an .npmrc anywhere else could name a registry that vouches for anything. When such an .npmrc sets registry, @scope:registry or tag to another registry or dist-tag than the one depcurfew used, the packages it applies to are not checked (an error that says which file and line, what it sets and which registry depcurfew checked), and a package of that scope is not looked up at all. A value that names the registry depcurfew checked (scheme and host in any case, without credentials and trailing slashes) changes nothing, and one depcurfew cannot read, such as a variable, counts as another registry. If you trust that registry, the fix says how to have depcurfew check it: --registry <URL> for check, in the hook command in .claude/settings.json and for depcurfew mcp, or @scope:registry=<URL> in the .npmrc next to depcurfew.json. A configuration file that is not a regular file or is larger than 1 MiB, such as a named pipe or a device in place of ~/.npmrc, is skipped with a note. So are the workspaces of a package.json with more than 1000 patterns, a pattern longer than 1000 characters or patterns that take too long to match: the nearer directory stays npm's local prefix, and a registry the .npmrc next to that package.json sets counts as one depcurfew did not check. npm skips the .npmrc of a workspace package, so a registry it sets counts only where npm may read it: when depcurfew cannot be sure that npm takes the directory for a workspace (a pattern with ?, [, braces or a trailing /, a negated glob, a directory whose name starts with a dot, a root package.json npm cannot read), for an npm command line that may turn workspaces off (--no-workspaces, --workspaces=false), and for pnpm and Bun, which read .npmrc by rules of their own. When the registry depcurfew checked has no such package or version while such an .npmrc names another registry, the package is unverified (with the file and line), not reported as a made-up name.
Python packages are written as pip takes them after the prefix (pypi:name, pypi:name==1.2, pypi:name[extra]>=1.2,<2), or as pypi:name@1.2, pypi:name@^1.2 and pypi:name@latest, as uvx and poetry add take them. The version is picked as pip picks it: the newest match, pre-releases only when asked for or when nothing else matches, and yanked versions only when pinned with == (a warning). A version is as old as its newest file, because files can be added to a release later. A project PyPI quarantines (as malware, for example) is an error; an archived or deprecated one, a warning. --index looks Python packages up in another index that serves the JSON form of the simple repository API.
Composer packages are written as composer require takes them after the prefix: composer:vendor/name, or the name and a constraint separated by :, = or a space (composer:monolog/monolog:^3.9, "composer:symfony/console >=7.4 <8"), with every form of Composer's constraints (exact, ranges, -, *, ~, ^, ||, , and stability flags such as @beta). Packagist is the only repository depcurfew asks: Composer's default, https://repo.packagist.org, and its v2 metadata of tagged releases. The version is picked as Composer picks it: the highest release the constraint allows at the minimum-stability of the root composer.json (stable without it), or at the stability the constraint names (@beta, 2.0.0-RC1), and the most stable of them first when prefer-stable is true; for a package named without a constraint, the version composer require picks itself. Messages write a version as composer require takes it: composer:monolog/monolog:3.12.1. A release is as old as its published-time, when Packagist published it, which the author cannot set, as Composer 2.11 counts its cooldown; a release without one is not checked. A constraint that names a branch (dev-main, 1.x-dev), or allows dev versions (@dev, or any constraint under "minimum-stability": "dev" without prefer-stable), is not vouched for: a branch is a commit, not a release, and Composer's cooldown never holds it back. Platform packages (php, ext-json) are not looked up, because Composer does not install them. When the root composer.json (of the checked directory, or the nearest one above it up to the directory of depcurfew.json) declares a repository or turns packagist.org off, a Composer package counts as not checked (an error that names the file, the line and the repository, and with which the hook asks), because Composer asks those repositories before Packagist and the first one that has the package wins; it is then not looked up on Packagist at all, so that the name of a private package does not leave the machine. The same holds for a root composer.json that depcurfew does not read (larger than 1 MiB, or not a regular file), which Composer reads with whatever repositories it declares. Composer uses a composer.json above the directory it runs in only when it runs in a terminal and you agree, or with use-parent-dir set to true, and never with --working-dir; otherwise it stays where it is, at the stability stable. So the version of a package is checked at the minimum-stability of such a file and at stable, and the stricter answer counts; its repositories count as above.
Git repositories, tarball URLs and local paths are checked against the sources section only. Without a depcurfew.json in the directory or one of its parents up to the root of the repository, the default policy applies. When the registry cannot be reached, the package counts as failed: check exits with 1 whenever a package breaks the policy or cannot be checked. With --json it prints one document and nothing on stderr: packages with their findings (each with code, level, message and fix), notices (each with level note, a code, location when it concerns a file, message, and fix when there is something to do; the codes are policy-missing, nearer-policy, registry-not-public, registry-link-local, npm-dist-tag and config-not-read; without --json each is a note: line on stderr ending with the code, and its fix: line), and errors for a depcurfew.json that cannot be used, in which case no package is checked.
Known malicious versions (OSV)
--osv asks OSV.dev, the open database of vulnerabilities and malicious packages of the Google project google/osv.dev, about the versions depcurfew checks. It is off unless you pass it, on each command: without it nothing is sent to OSV.dev.
npx depcurfew check --osv zod@4 pypi:requests
npx depcurfew doctor --osv
check, the Claude Code hook (check --hook claude-code --osv) andcheck_packagesofdepcurfew mcp --osvlook up the version the install would pick, after the dist-tag, the range and the pre-release rules have chosen it, in one batch query per command. A version with a record of the OpenSSF malicious-packages database (an id starting withMAL-) is an error with the codeknown-malicious: it names the record and links its page, such ashttps://osv.dev/vulnerability/MAL-2026-18458, and the hook denies the command. Other OSV.dev records are vulnerabilities, not malware, and are not reported. For the hook, add"--osv"after"claude-code"in theargsof the hook command in.claude/settings.json(see below); fordepcurfew mcp, after"mcp".- When OSV.dev cannot be reached (no network, an error, a redirect, an answer too large or not in the documented shape, such as a record id outside the characters of the OSV schema or one result with more records than two of its pages), the package is
unverified:checkexits with 1 and the hook asks. No other text of an answer reaches a message: a redirect is named by the origin it points to, or as a location that is not a URL. A package is never passed because OSV.dev did not answer. doctor --osv(and the Action withosv: true) sends every package version the lockfiles it reads pin:package-lock.json,pnpm-lock.yaml,yarn.lock(Berry),bun.lock,deno.lock,uv.lock,poetry.lock,pdm.lock,Pipfile.lock,pylock.toml, requirements files pinned with==and the releases incomposer.lock(as Composer wrote them,v7.3.4or3.9.0), in the root and in every directory ofprojects. Each version goes once, in batch queries of at most 1000 (the limit of the API), at most 20,000 versions per run. The version looked up is the one the tool installs: forpackage-lock.jsonthe registry tarball ofresolved, whose name and version npm reads from its path (/cutekitten-test/-/cutekitten-test-1.0.1.tgz), forpnpm-lock.yamlthe registry tarball ofresolution.tarballwhen there is one (a relative one too, which pnpm reads against the registry), and foryarn.lockthe version ofresolution. The path is read as the registry reads it: without a query or a fragment after the file name (.tgz?x=1,.tgz#x, which npm still fetches) and with its percent-escapes decoded; a registry tarball URL from which depcurfew reads no name and version (.TGZ) counts as an entry without an exact version, never as the version field; an entry whose name or version field says otherwise (an edited lockfile) gets a warning that names both. The ruleknown-maliciousreports each malicious version once per package and lockfile, at its line; versions OSV.dev did not answer for, or beyond the 20,000, give a warning per lockfile, so the check is not counted as passed, and so do registry entries of a lockfile that give no exact version. Requirements without an exact version (requests>=2), and Composer packages locked at a branch (dev-main,1.x-dev), are named in a note. Without--osvthe rule does not run and shows as not applicable.- Withdrawn records (for example a report found to be a false positive) are left out of OSV.dev's answers. OSV.dev aims to be at most 15 minutes behind its sources, so a report a few minutes old may not be found yet.
Privacy. --osv sends the name, the ecosystem and the version of each package to api.osv.dev, the API of OSV.dev, including packages of a private registry, whose names may tell what the project uses; nothing else about the project, and no credentials. Leave it off where package names must not leave the network.
What it catches. Only malware that has been found and reported to the OpenSSF malicious-packages database, which OSV.dev serves; a new malicious release is not there until someone reports it, which is what the release-age delay of cooldown.minimum gives time for. The database says it may contain false positives: if you think a record is wrong, read it on its page and report it there.
Claude Code
depcurfew check --hook claude-code runs as a Claude Code hook before every command of the tools that run shell commands: Bash, PowerShell and Monitor. It finds the packages the command would install or download to run, checks them and answers:
- deny when a package breaks the policy, or with
--osvis a known malicious version (Claude sees the reason and can pick another package or version); - ask when depcurfew cannot vouch for the command: a look-alike name, a registry it cannot reach (with
--osvalso OSV.dev), an error of its own, or a command it cannot read, such as one with$(...)or variables in its arguments, or a shell that runs commands from a pipe (curl … | sh); also when an option or variable weakens the policy for that one command, such as a release-age delay shorter than the one indepcurfew.json(--min-release-age=0,YARN_NPM_MINIMAL_AGE_GATE=0,--no-time-gate) or install scripts turned on where it restricts them (--ignore-scripts=false,npm_config_ignore_scripts=false,bun add --trust), even innpm ci, and when the same command changes the package manager's configuration orpackage.jsonnext to an install or another command of the package manager (npm config set registry … && npm ci, also spellednpm confornpm c,npm pkg set overrides.… && npm install,poetry source add … && poetry add x,pdm config pypi.url … && pdm add x); - nothing when everything checks out, so the command goes through Claude Code's usual permission prompts.
Warnings, such as a deprecated version or a git source depcurfew does not look up, never decide: the command goes through the usual permission prompts, and the warnings reach Claude as context next to the result of the command, with the deprecation message in quotes as the publisher's text, which is data, not instructions.
It follows the command lines inside a command: $(...) and backticks, sh -c, PowerShell's -Command, cmd /c, eval, npx -c, yarn workspace <name>, python -m pip, the command uv run runs, a package manager run by npx, uvx or pipx run, wrappers such as sudo (also sudo -s and sudo -i), su -c, env (also env -S in grouped options, env -iS'…'), timeout, flock and xargs, the action of a trap, aliases defined in the command, . and source of a package manager, and an interpreter that runs a package manager's script (node /usr/bin/npm, node .yarn/releases/yarn-4.1.0.cjs, python3 -m pip.__main__, python3 pip.pyz); a script the shell expands before an install word (node $(which npm) i x) makes it ask. $'…' is decoded as bash decodes it ($'np\x6d' i x is npm i x) and $"…" is read like "…"; a command built at run time inside another command line (eval 'x$A', sh -c '${A}x'), brace expansion as the command ({npm,install,x}) and $'…' it cannot decode make it ask. An alias is expanded at most once while its text is read, as in bash, and a command whose aliases and nested command lines add up to more than depcurfew reads makes it ask. CRLF line endings are read as bash reads them, so a here-document ends where bash ends it. It reads options the way each tool does, including npm's and pnpm's shorthands and abbreviations (--reg and --regis for --registry, --no-ign for --no-ignore-scripts), abbreviated long options of pip and pipx (--extra for --extra-index-url) and short options with their value in the same word (-ihttps://…), so an option, or a variable such as PIP_INDEX_URL or UV_INDEX, that points the tool at another index makes it ask. Such a variable, and one that weakens the policy or changes the version or the directory, counts wherever the line sets it, as if it were set to the value that weakens the policy most: NAME=value is read with its value, and a value depcurfew cannot read counts as the worst one when a builtin sets the name (printf -v NAME, read NAME, mapfile and readarray, getopts … NAME, wait -p NAME, a nameref declare -n ref=NAME, zsh's and ksh's print -v, vared and integer), with NAME+=…, NAME[0]=…, for NAME in …, select, ${NAME:=…}, an assignment in arithmetic (let, (( NAME = … )), NAME++, [[ … -eq … ]], subscripts, declare -i), inside eval, an alias, a trap, sh -c or $(…), in the value of another variable (N=NAME), which the shell may use as a name, in the code of python -c, node -e or php -r, which depcurfew does not read, and with cmd's set /p and set /a; so does a builtin that may set a variable whose name the shell builds (export "$(cat .env)"). A word that only reads or mentions the name sets nothing: printenv NAME, echo, grep NAME, a commit message or Composer in a sentence, and export NAME, readonly NAME and unset NAME, which do not change a value. Names count in the case the tool reads them: in Bash on Linux and macOS COMPOSER but not Composer, npm_config_* and YARN_* in any case, pnpm_config_* all in lower or all in upper case, PIP_ followed by the rest in any case; in PowerShell, cmd and Git Bash on Windows every case counts. The hook then asks only where that value would make it ask: a variable of the registry or of the version next to an install that names packages (read npm_config_registry; npm ci goes through like npm_config_registry=… npm ci, which installs the lockfile), one that weakens the release-age delay or the install scripts when the policy sets them, and one that turns off a protection of Composer next to any command of a package manager. It checks the version the command would install: the dist-tag --tag or npm_config_tag makes npm prefer, pre-releases that --pre, --prerelease allow and the like allow, and builds from source that the command's options or variables force (wheels refused), which count as install scripts. A script piped into an interpreter (curl … | python3, irm … | iex) or read from <(curl …) makes it ask, and so does a command with more than 50 packages, of which the first 50 are checked. deno x and dx are read like npx. Packages from requirements files (pip install -r) and the project's own files (uv sync, poetry install) are not checked, as with npm install without names; depcurfew doctor checks the lockfiles. When it cannot read the hook event at all (not JSON, not an object, or a PreToolUse event of Bash, PowerShell or Monitor without a readable tool_input.command, apart from a Monitor WebSocket watch), it exits with code 2, which blocks the command in every Claude Code version, and says on stderr what it got (with the code hook-event-unreadable) and how Claude Code runs the hook.
The registry settings of Yarn (npmRegistryServer and npmScopes in .yarnrc.yml), Bun (registry and scopes in the [install] table of bunfig.toml), uv ([[tool.uv.index]] or [[index]], index-url, extra-index-url and find-links in pyproject.toml or uv.toml), Poetry ([[tool.poetry.source]]), PDM ([[tool.pdm.source]]) and Pipenv ([[source]] in Pipfile), in the working directory or a directory above it in the project, make the hook ask about the packages that tool would install from there when they name another registry or index than the one depcurfew checked, or a value it cannot read (such as ${NPM_REGISTRY}). Pass that registry to the hook with --registry (Python: --index) when you trust it; settings that add places to install from (extra-index-url, find-links, a uv index without default = true, a supplemental Poetry source, a PDM source not named pypi) always make it ask, because depcurfew checks one index. An index that serves only the packages pinned to it (uv's explicit = true with [tool.uv.sources], Poetry's explicit source with source = in a dependency, a Pipfile source other than the first with index =) makes it ask about those packages. When such a setting applies and the checked registry or index has no such package or version, the hook asks and names the setting instead of denying the install as a made-up name. The same goes for an .npmrc that npm reads there but check does not take (see above).
The hook takes its policy from the project Claude Code works on (CLAUDE_PROJECT_DIR), wherever Claude's working directory is inside it: when the project directory has a depcurfew.json (or one above it up to the root of the repository), that policy applies, and a different depcurfew.json nearer the working directory (in a nested repository, a vendored one or a package folder) is applied too, and the stricter answer counts, so it can tighten the check but never relax it; a decision says so. This holds whether or not the project is in a git repository. When the project directory has no depcurfew.json, the nearest one of the working directory applies alone, as check --dir finds it, so frontend/depcurfew.json decides for frontend. Paths are compared by where they really lead, through symlinks.
Each install is checked in the directory it runs in, not only in Claude's working directory: the hook follows cd (also quoted, absolute or after &&, ;, || or a new line), pushd, popd, cd - and zsh's chdir, PowerShell's Set-Location (cd, chdir, sl), Push-Location and Pop-Location with a written-out -Path or -LiteralPath, cmd's cd /d, and the options that move one command: npm --prefix (-C), pnpm --dir (-C), yarn --cwd, bun --cwd, uv --directory and --project (and UV_WORKING_DIR, UV_PROJECT before the command), poetry --directory (-C) and --project (-P), pdm --project (-p, PDM_PROJECT), and PIPENV_PIPFILE (the directory of that Pipfile). A change ends with its subshell ( … ), $( … ) or sh -c '…'. So cd pkgs/strict && npm install x is checked with the policy of the project and pkgs/strict/depcurfew.json as if Claude were in pkgs/strict, with the .npmrc, .yarnrc.yml, bunfig.toml and uv settings of that directory. A directory outside the project gets only the project's policy (or, when the project has none, the nearest one from the working directory), never one found there. When the hook cannot tell the directory, as with cd "$DIR", cd ~, cd alone, a pattern, a cd in a loop, a function or one branch of if, it asks about an install that names packages and says to run the install from that directory or with the path written out; a command that installs nothing, or only from the lockfile (npm ci), goes through as before. A change of directory can only tighten the check: the policies of the working directory apply to an install in another directory too, in case the change does not happen. A cd to a directory that does not exist (unless a mkdir earlier in the command creates it) or is a file, cd - after the command sets OLDPWD, and a relative cd after it sets PWD make the hook ask; cd -P, set -P and set -o physical are followed through symlinks. env -C (--chdir) and sudo -D move the install too, also inside an env -S string, which is read again as arguments as GNU env reads it, while find -execdir and an install in a trap that a later cd may move make it ask. The stack of pushd is followed as bash keeps it (pushd -n, +N and -N, popd -n, dirs -c; a popd on an empty stack stays where it is), and a directory that cd - or popd leaves must exist. The hook also asks about an install after a builtin that may set OLDPWD, PWD, CDPATH or DIRSTACK through a name the shell builds (declare "${X}PWD=…", printf -v, read, eval "$X") followed by cd -, popd or a relative cd, and after a trap on DEBUG, ERR, RETURN or a signal that changes directory, a file run with . or source, or a function whose body changes directory. The activation script of a Python virtual environment (source .venv/bin/activate, bin/activate.fish, bin/activate.csh, Scripts\Activate.ps1) keeps the directory known when its environment directory holds pyvenv.cfg and no command before it on the line may write the script. PowerShell paths with \ (cd pkgs\strict) are read with \ as a separator on every system.
Composer commands are read as Composer reads them: options before or after the command, of which only -d and --working-dir take the next word before the command name (Composer finds the command name before it knows the command, so composer --ignore-platform-req require x/y is composer require x/y), the alias r, the short names Composer accepts when only one command starts with them (composer req, but not composer re), and php composer.phar …, php /path/to/composer … and ./composer.phar …. -d and --working-dir move the command, like cd, as Composer finds them: the first word that is one of them, or starts with -d or --working-dir=, before --; a grouped -nd dir does not move Composer. The scripts of the root composer.json are commands too when the command word is not the exact name of a Composer command: a word that names a script (or one of its scripts-aliases) runs that script, as composer run-script does, which the hook does not follow (as with npm run), and an abbreviation that a script makes ambiguous (composer req next to a script required) runs nothing. Composer reads scripts-aliases only since 2.7.0, so a word that is a Composer command without them (composer r next to a script alias r, which Composer 2.2 to 2.6 run as composer require) is checked as that command. A composer.json above the directory the command runs in may not apply (see check above), so the hook reads the command word also without its scripts (composer req x in a subdirectory is composer require x there) and checks the version also at the stability stable. composer require and composer global require are checked package by package, also when the constraint is the next word (composer require vendor/name ^1.0); composer create-project checks the package of the project (with its version and --stability) and the packages of --require; composer update checks the packages it names with a constraint (vendor/name:1.2.3, --with vendor/name:1.2.3) and the ones it names without one at the constraint of the root composer.json (with its minimum-stability and prefer-stable), while a package that composer.json does not require, a pattern ("vendor/*") and a full composer update are left to Composer, as composer install, which installs composer.lock. The hook asks about --no-blocking, --no-security-blocking and COMPOSER_NO_BLOCKING or COMPOSER_NO_SECURITY_BLOCKING with any value but 0 and an empty one (they turn off every policy block of Composer, malware blocking included), COMPOSER_POLICY=0, COMPOSER_POLICY_MALWARE_BLOCK=0 and create-project --no-secure-http whatever the policy says, and about --no-cooldown, COMPOSER_POLICY_COOLDOWN_BLOCK=0 and a COMPOSER_POLICY_COOLDOWN_PERIOD shorter than cooldown.minimum; about composer config that changes a setting (--unset, --editor, also with --global or after composer global), composer repository add (and remove, set-url, enable, disable) and composer policy add-source in the same command as another Composer command; about such a change with --global (-g) or --file (-f), or after composer global, also without another Composer command, because the Composer commands that follow use COMPOSER_HOME/config.json, COMPOSER_HOME/composer.json or that file, which depcurfew does not read; and about COMPOSER=<file>, COMPOSER_HOME=<directory> and create-project --repository, which take the repositories from elsewhere, and about composer global update of a package named without a constraint, whose constraint is in COMPOSER_HOME/composer.json. Commands of composer global and create-project do not read the composer.json of the working directory, so its repositories do not count for them.
It reads commands the way Windows runs them too: executables in any case and with .exe, .cmd, .bat or .ps1 (NPM.CMD i lodash), paths with backslashes, and cmd /c or cmd /k (also //c from Git Bash), where %VAR% and !VAR! count as expansions. PowerShell commands (the PowerShell tool, powershell -c, pwsh -c and iex) are read like any other when they are plain commands: quotes, ;, |, &&, the call operator &, redirections and $env:NAME = "value" before a command. PowerShell syntax it does not read, such as variables, (...), $(...), {...}, @(...), backticks, commas, Start-Process or Set-Alias, makes it ask when the same statement names a package manager; a statement without one, such as Get-ChildItem | Where-Object { $_.Length -gt 1kb }, goes through. Variables that point a package manager at another registry or weaken its checks count wherever the line sets or names them: $env:NAME, ${env:NAME}, paths on the Env: drive, the names New-Item -Name, Rename-Item -NewName, Copy-Item and Move-Item give there, and [Environment]::SetEnvironmentVariable, with parameters read as PowerShell binds them (any case, abbreviations such as -N and -Nam, -Name:value, positions) and backtick escapes and quotes removed (Env:COMPO`SER is Env:COMPOSER). A use of the Env: drive whose variable name it cannot tell (Set-Item "Env:$name", a wildcard, splatting, a parameter it cannot bind) makes it ask when the line runs a package manager. COMPOSER counts only as such a name, not as the word composer (& 'composer' require …, Write-Output 'composer'). It also asks about an argument with &, |, <, > or " for a package manager run from PowerShell, because the .cmd shim of a package manager hands its arguments to cmd.exe again.
Install depcurfew as a development dependency of the project, then add the hook:
In .claude/settings.json of the project:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|PowerShell|Monitor",
"hooks": [
{
"type": "command",
"command": "node",
"args": [
"${CLAUDE_PROJECT_DIR}/node_modules/depcurfew/dist/cli.js",
"check",
"--hook",
"claude-code"
],
"onFailure": "block"
}
]
}
]
}
}
The matcher Bash|PowerShell|Monitor covers every tool that runs shell commands. On Windows, PowerShell is the main shell wherever the PowerShell tool is on, and without Git Bash there is no Bash tool, so a hook on Bash alone never runs there; Monitor runs a command in the background. command is node itself, which also starts on Windows, where Claude Code runs it without a shell.
onFailure: "block" needs Claude Code 2.1.295 or later; without it, a hook that fails to start, crashes or times out lets the command through.
Before depcurfew is installed. The hook runs the copy in node_modules. When that file is missing (a fresh clone before npm ci, a repository without package.json in its root, a new cloud session), node still starts, cannot load it and exits with code 1 and a stack trace, and with onFailure: "block" that failure blocks every command of these tools, including the npm ci that would install depcurfew; without it, every command runs unchecked. The blocked call shows the hook command, failed; blocking because onFailure is "block" and node's Error: Cannot find module '…/node_modules/depcurfew/dist/cli.js': run the install (npm ci --ignore-scripts in the project, or in .claude/depcurfew for the tools directory below) from a terminal, then try again. So install depcurfew before the hook is needed:
- On your machine: run
npm cionce after cloning, in a terminal, before you start Claude Code. - In every session: a
SessionStarthook installs the locked copy when it is missing (with pnpm, Yarn or Bun, use their install from the lockfile instead ofnpm ci). It runs when a session starts or resumes, and Claude's first response waits for it, so the guard is in place before the first command.--ignore-scriptskeeps the install from running dependency scripts before the guard is there. Add it next toPreToolUsein the same.claude/settings.json:
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "cd \"$CLAUDE_PROJECT_DIR\" && { test -f node_modules/depcurfew/dist/cli.js || npm ci --ignore-scripts; }"
}
]
}
]
}
}
- A repository without
package.jsonin its root (several projects side by side): keep depcurfew in a small tools directory, for example.claude/depcurfew/package.jsonwith depcurfew pinned exactly and itspackage-lock.jsoncommitted, install it there (cd .claude/depcurfew && npm ci --ignore-scripts, also in theSessionStarthook), and point the hook at${CLAUDE_PROJECT_DIR}/.claude/depcurfew/node_modules/depcurfew/dist/cli.js. - Cloud sessions (Claude Code on the web): the repository's
.claude/settings.jsonhooks run there, user-level ones do not. The environment's setup script runs before Claude Code starts, so it can install depcurfew too; theSessionStarthook above covers a cached environment as well. - Locked out already: install depcurfew from a terminal outside Claude Code, or remove the hook from
.claude/settings.jsonthere; Claude cannot do it for you while the hook blocks its commands, which is the point ofblock.
Commands the guard reads (an add subcommand of any npm package manager, such as install or i, is checked for all of them; Python commands are matched word for word, and python -m pip counts as pip):
| Package manager | Add packages | Download and run | Starter kits |
|---|---|---|---|
| npm | npm install, npm add, npm i, npm in, npm ins, npm inst, npm insta, npm instal, npm isnt, npm isnta, npm isntal, npm isntall, npm install-test, npm it | npx, npm exec, npm x | npm init, npm create, npm innit |
| pnpm | pnpm add | pnx, pnpx, pnpm dlx | pnpm create |
| Yarn (Berry) | yarn add | yarn dlx | yarn create |
| Bun | bun add | bunx, bun x | bun create |
| Deno | deno add, deno install | deno x, dx, denox, dnx | none |
| uv | uv add, uv pip install, uv tool install | uvx, uv tool run, uv run | none |
| pip | pip install, pip3 install | none | none |
| pipx | pipx install | pipx run | none |
| Poetry | poetry add | none | none |
| PDM | pdm add | none | none |
| Pipenv | pipenv install | none | none |
| Composer | composer require, composer r, composer global require, composer global r | none | composer create-project |
The hook reads commands the way a shell splits them, but it is a safety net, not a sandbox: a command can install packages in ways it does not recognize (a script, another tool). Keep the package manager configuration from depcurfew apply in place; it is what enforces the policy for every install.
MCP server
depcurfew mcp runs the same check as a Model Context Protocol server over stdio, so that an AI agent in Claude Desktop, VS Code or any other client that starts a local stdio server can ask before it installs a package. It offers two read-only tools:
check_packagestakespackages, a list of 1 to 50 specs asdepcurfew checktakes them (zod@4,pypi:requests,composer:monolog/monolog), and an optionaldirectory. It returns the document ofcheck --json(schemaVersion,policy,packageswith their findings,notices,errors) as structured content and as JSON text. A package that breaks the policy is a successful check with error findings; the call itself is an error (isError) only when nothing could be checked, such as adepcurfew.jsonthat cannot be used, a directory that does not exist or a wrong argument, with a message that says what to do.explain_policytakes an optionaldirectoryand returns whichdepcurfew.jsonapplies (nulland the default policy ofdepcurfew initwhen there is none), the release-age delay and its exceptions, the install-script lists, the allowed sources, and the registry and indexcheck_packagesasks, with the scopes.npmrcassigns to other registries as a list ofscopeandregistry,composerRepository, the Packagist URL it asks about Composer packages, andosv, the OSV.dev endpoint it asks with--osv. It reads local files only.
The policies are those the hook applies, with directory as the working directory: the depcurfew.json of the project directory (or the nearest one above it up to the root of the repository), and a different one nearer directory too, the stricter answer counting, so that a depcurfew.json the user did not write, such as one in a package in .venv/…/site-packages, a vendored clone or a file an agent wrote, can tighten the check but not relax it; a notice names it. Without a depcurfew.json in the project directory, the nearest one from directory applies. Paths are shown relative to the project directory, and a directory must exist inside it. The registries come from .npmrc as for check: an .npmrc that npm reads in directory but that does not belong to the policy makes the packages it applies to unverified, and so does a root composer.json there that declares other repositories for Composer packages.
What the agent gets is the same as in the hook: install scripts are named without their content, text from a registry or the repository (keys too) is shown as visible text and cut to 400 characters, with control characters, bidirectional controls, line and paragraph separators and invisible characters (zero-width characters, Unicode tag characters, variation selectors, Hangul fillers and the blank braille pattern) written as escapes, and the publisher's text, such as a deprecation message, in quotes; the tool descriptions say that it is data, not instructions. A fix that would change depcurfew.json says to ask the user instead, because only the user changes the policy, a fix about the registry or index (one that cannot be reached, plain HTTP, a registry setting depcurfew does not take) says to tell the user, because the agent cannot choose another one, and a fix that sets something in .npmrc says to ask the user to set it; without a depcurfew.json, a notice says that the default policy applies and to tell the user if the project should have its own. The registry and the index are options of the server (--registry, --index, and .npmrc as for check), never arguments of a call, so an agent cannot point the check at a registry that vouches for anything. The project directory is --dir, else CLAUDE_PROJECT_DIR, else the directory the client starts the server in. The server ends when the client closes its stdin or sends SIGINT or SIGTERM, and writes nothing but protocol messages to stdout.
Install depcurfew as a development dependency of the project, then add the server. Each configuration starts node itself with the path of the installed script, which works without a shell on Windows too:
The server speaks the initialize handshake of the protocol revisions 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05 and the stateless revision 2026-07-28, without the handshake, for clients that use it.
Claude Code. Add the server to .mcp.json of the project, which everyone working on it shares (Claude Code asks before it starts a server from that file):
claude mcp add --scope project --transport stdio depcurfew -- node '${CLAUDE_PROJECT_DIR:-.}/node_modules/depcurfew/dist/cli.js' mcp
or write .mcp.json yourself:
{
"mcpServers": {
"depcurfew": {
"command": "node",
"args": [
"${CLAUDE_PROJECT_DIR:-.}/node_modules/depcurfew/dist/cli.js",
"mcp"
]
}
}
}
Claude Code sets CLAUDE_PROJECT_DIR to the project root for the server, and depcurfew mcp takes it as the project directory. Claude Code itself does not have the variable, so in args it needs the default ${CLAUDE_PROJECT_DIR:-.} (in cmd.exe, leave out the single quotes).
Claude Desktop. In claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/claude_desktop_config.json, Windows: %APPDATA%\Claude\claude_desktop_config.json, Linux: ~/.config/Claude/claude_desktop_config.json), with absolute paths and --dir, because Claude Desktop starts servers in no particular directory:
{
"mcpServers": {
"depcurfew": {
"command": "node",
"args": [
"/path/to/project/node_modules/depcurfew/dist/cli.js",
"mcp",
"--dir",
"/path/to/project"
]
}
}
}
VS Code. In .vscode/mcp.json of the workspace:
{
"servers": {
"depcurfew": {
"type": "stdio",
"command": "node",
"args": [
"${workspaceFolder}/node_modules/depcurfew/dist/cli.js",
"mcp"
],
"cwd": "${workspaceFolder}"
}
}
}
code --add-mcp '{"name":"depcurfew","type":"stdio","command":"node","args":["/path/to/project/node_modules/depcurfew/dist/cli.js","mcp","--dir","/path/to/project"]}' adds the server for one project to your user profile instead.
Other clients that start a stdio server take the same command and arguments; give them absolute paths and --dir when they do not start the server in the project directory.
With --osv in the server's arguments ("args": [".../cli.js", "mcp", "--osv"]), check_packages also looks the versions up in OSV.dev, as check --osv does, and explain_policy names the endpoint in lookups.osv (null without it). An agent cannot turn it on or off: it is an option of the server, not an argument of a call.
The server is advice to an agent that asks, not a guard: an agent can install packages without calling it. The package manager configuration from depcurfew apply is what enforces the policy; in Claude Code the hook above also checks every install command.
Network and privacy
Only check, the Claude Code hook and the check_packages tool of depcurfew mcp use the network, and doctor and the GitHub Action only with --osv (input osv: true), for OSV.dev alone. For each package the first three request one URL with the package name in it: from the public npm registry (registry.npmjs.org), PyPI (pypi.org) or Packagist (repo.packagist.org), or from the registry that the project's or your .npmrc sets, --registry or --index. They send no credentials (tokens in .npmrc are not read, and a user name and password written into a registry URL are removed before the request and in every message) and nothing else about the project, and depcurfew has no telemetry. A private package whose scope has no registry in an .npmrc that npm reads is looked up in the public registry, so its name reaches that registry; set @scope:registry in .npmrc or pass --registry first. A package whose scope has a registry in an .npmrc that check does not take, or in .yarnrc.yml or bunfig.toml, is not looked up, and neither is a Composer package when the root composer.json declares another repository or turns packagist.org off. With --osv they also send the name, ecosystem and version of each package to api.osv.dev in a batch query, and doctor --osv those of every locked package version, private packages included (see Known malicious versions); without --osv nothing goes to OSV.dev. init, explain, apply and doctor without --osv work without network access. The GitHub Action checks the repository without it too, unless the input osv is true; afterwards it sends the result (the counts, the table of checks and findings, the files read) to the GitHub API of the run (GITHUB_API_URL) with the workflow token to create the depcurfew doctor check (input check-run), and the SARIF upload to code scanning is a separate step that runs GitHub's own action. An editor that reads the $schema of depcurfew.json may download the schema from unpkg.com.
Library
The same functions are available from the package: detectProject, planApply and applyProject, runDoctor with toSarif (and runDoctorWithOsv, doctor --osv), checkSpecs with scanCommandLine, queryOsv, and the policy parser and converters. Their types ship in dist/index.d.ts, and src/index.ts lists every export. The package is an ES module only: use import; require('depcurfew') works on Node.js 22.12 and later, which load ES modules with require().
Contributing
Facts about package managers live only in src/knowledge/, each with the official URL it was verified against and the date. A correction is welcome with a link to the official documentation or changelog. npm test builds the project and runs every test without network access. CONTRIBUTING.md has the setup, the commands and the rules for changes, and the issue forms ask for what a report needs; vulnerabilities go through SECURITY.md.
npm run site builds the documentation site into dist-site/ from this README, the knowledge base and schema.json, so its pages say what the README says; it uses relative links only, needs no JavaScript and loads nothing from other hosts. npm run site -- --versions also reads the release tags with git and adds the README, knowledge base, changes and schema of every earlier release from 1.1.0 on under /<version>/, each page with a banner that names its version, and a Versions page. The site is published at depcurfew.github.io by the repository depcurfew/depcurfew.github.io, which builds it from the latest release of this repository once a day (its files are in site/org-repo/) and serves the schema at schema.json and schema/v1.json.
License
Trademarks
npm is a registered trademark of npm, Inc. "Python" and "PyPI" are trademarks of the Python Software Foundation. pnpm, Yarn, Bun, Deno, uv, pip, Poetry, PDM, Pipenv, Composer, Packagist, Renovate, Dependabot, GitHub, Claude Code and the other names in this document belong to their owners. depcurfew is an independent project and is not affiliated with or endorsed by any of them.