depcurfew v1.6.1
On this page

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, or workspace-search-limit when the workspace patterns of one file, such as **, lead through more than 20000 directories, after which depcurfew stops looking for workspace packages; a directory reached again through a symlink, such as one that points back to the root, is searched once and is one package, and the root is never a workspace package of itself), 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).

Without depcurfew.json (and without --user), the rules that need a policy (marked "needs depcurfew.json" below) compare the repository with the policy depcurfew init would write there: the default policy, with the longer release-age delay and the projects init would keep, and the packages it would put into installScripts.deny. Every finding of theirs is a note, whatever its usual level, so it never makes doctor fail, whatever --fail-on or --strict say, and doctor.failOn of the default policy does not apply. The message names "the default policy of depcurfew init", and the fix runs depcurfew init and then depcurfew apply (depcurfew apply --dry-run shows the changes first). The text shows these notes after the other findings, under "Compared with the default policy (no depcurfew.json):", and the line of counts counts them as notes. config-drift gives one note per file instead of one per difference: a package manager that waits less than cooldown.minimum now, with where its delay comes from (a setting, or the default of its version), and the settings apply would set or remove, by name and value as the tool reads the file (a setting of every entry of a list, such as the update entries of a bot, is named once, with [] after the list); a file whose settings already hold gives none, also when they were written by hand outside depcurfew's block. A note names at most 6 settings and counts the rest. Of one rule in one file at most 5 notes are shown, and one more says how many are left out. Two rules that run without a policy as well, manager-version-too-old and unreadable-config, run against it once more: a pinned package manager too old for a setting the default policy needs, and a lockfile that only the rules that need a policy read and that cannot be parsed, are notes like the others. Their findings without a policy keep their level and are not repeated, and a version problem that a setting apply writes for the repository would settle is left out, since config-drift names that setting. The list of what was checked shows these rules with their result and "(compared with the default policy)" instead of skipped. In --json each such finding has "policy": "default", the top level has policySource ("file" for depcurfew.json, "default" for this comparison, null when the rules that need a policy did not run, as with an invalid depcurfew.json), and the entry of each such rule in checked.rules has "policy": "default"; SARIF has them as results with level note, the same message and properties.policy "default". The notice policy-missing says which policy was used. Of the projects init would list, the first 20 are compared: each one is detected and checked by every rule that needs a policy, so a repository of thousands of them would take seconds. With more, the notice projects-not-compared after it says how many were left out and names the first five; doctor checks them all once depcurfew init has listed them in depcurfew.json. known-malicious looks up the lockfiles of the compared projects too, in the local OSV.dev data or with --osv, and a version listed there is an error that fails doctor, as one in the root lockfile is: a known malicious locked version is a fact of the lockfile, not a difference from a policy. So is what lockfile-tampered reports, which is an error in those projects as well. An invalid depcurfew.json is reported as before (invalid-policy), and only the rules that need no policy run. The GitHub Action does not make this comparison: without depcurfew.json it runs only the rules that need no policy, as before.

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 (without one: a note against the default policy) · 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 · without depcurfew.json also a note against the default policy · 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; npm 10 does not even warn. When packageManager, devEngines, Volta, .tool-versions, mise.toml, a CI step or a Dockerfile (pipx install poetry==X, the uv image) pins an older version, a setting is not applied there, and so for the npm of the Node.js a CI job, Dockerfile stage or version file chooses and the pip of a python image: Node.js 22 comes with npm 10, which has no min-release-age. npm install -g npm@<range> counts as the newest release depcurfew knows. Composer pins no version: plugin-api-version in composer.lock older than 2.11 gives a warning, otherwise a note. Bundler runs the version of BUNDLED WITH in Gemfile.lock (cooldown: in a Gemfile needs 4.0.14 once it exists), the bundler input of ruby/setup-ruby or gem install bundler -v, and every Yarn from 1.18 the release yarnPath names. Cargo runs the Rust of rust-toolchain.toml or rust-toolchain: a release below 1.100.0 is an error, a channel X.Y a warning, beta, nightly and dated channels a note.

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. The version in use of Yarn is the release yarnPath in .yarnrc.yml names, which every Yarn from 1.18 runs instead of itself, so a packageManager pin of another release has no effect (a warning).

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.

pnpm-config-unverifiable

warning · pnpm loads a pnpmfile or a config-dependency plugin that can change every setting, takes a security setting from a config dependency, or stops at include in pnpm-workspace.yaml.

The updateConfig hook of a pnpmfile (.pnpmfile.mjs, .pnpmfile.cjs, or the files the pnpmfile and globalPnpmfile settings name) and of the pnpmfile pnpm loads by itself from a config dependency named pnpm-plugin-*, @*/pnpm-plugin-* or @pnpm/plugin-* can change every setting after pnpm has read pnpm-workspace.yaml, so the delay and the build settings depcurfew checks may not hold; ignoreScripts does not stop them, ignorePnpmfile does. A plugin the knowledge base knows to set a shorter release-age delay than depcurfew.json is an error. From pnpm 12.15, include in pnpm-workspace.yaml can give a setting to a config dependency, whose values depcurfew cannot read, or to a file outside the directory; and an include pnpm rejects (ERR_PNPM_INVALID_WORKSPACE_INCLUDE, ERR_PNPM_DUPLICATE_SETTING, ERR_PNPM_SETTING_NOT_DELEGATED) makes every pnpm command stop.

Fix: Read what the pnpmfile or plugin changes; set pnpmfile: [] when the project needs no pnpmfile, remove plugins it does not need, keep security settings in pnpm-workspace.yaml or a local file of include, and fix what pnpm rejects in include.

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 (a pip.conf counts only where PIP_CONFIG_FILE names it) 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 (or its older name resolver) set to "pure-python" or "uv" 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.

ci-weakens-policy

error · needs depcurfew.json (without one: a note against the default policy) · A CI workflow or a Dockerfile turns off or weakens, for the commands it runs, a protection depcurfew apply wrote for npm, pnpm, Yarn, Bun, Deno, uv, Poetry or PDM.

Package managers take settings from the command line and the environment over their configuration files, so a workflow or a Dockerfile can undo what depcurfew apply wrote for every install it runs: NPM_CONFIG_MIN_RELEASE_AGE=0, npm_config_strict_allow_scripts=false, npm_config_allow_git=all, pnpm_config_minimum_release_age=0, YARN_ENABLE_SCRIPTS=true or UV_EXCLUDE_NEWER in the env of the workflow, a job, its container or a step, exported or set before a command in a run step, written to $GITHUB_ENV, or set with ENV or ARG in a Dockerfile; and npm --min-release-age=0 or --allow-git=all, pnpm --config.dangerously-allow-all-builds=true, uv lock --exclude-newer or pdm lock --exclude-newer on a command there. A shorter delay and install scripts or sources the policy denies are errors; a value with $ or ${{ }}, or a delay depcurfew cannot compare (a date), gives a warning. pip has python-config-ignored, Composer composer-config-weakens and Bundler bundler-config-weakens.

Fix: Remove the variable or the option, or give it a value that keeps what depcurfew.json requires; let a single package through with cooldown.exclude, installScripts.allow or sources in depcurfew.json and depcurfew apply instead.

composer-config-weakens

error · composer.json, a CI workflow or a Dockerfile 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", "cooldown" or "block" under it set to false, a repository with "require-published-time": false or "filter": false, "allow-plugins": true (or a "*" or "*/*" key), "secure-http": false and "disable-tls": true turn such a protection off while the configuration looks right; so do COMPOSER_POLICY=0, COMPOSER_NO_BLOCKING or COMPOSER_NO_SECURITY_BLOCKING set to 1, true, on, false or off, a shorter COMPOSER_POLICY_COOLDOWN_PERIOD and COMPOSER_POLICY_COOLDOWN_BLOCK=0 in a CI workflow (env, run step, $GITHUB_ENV) or a Dockerfile (ENV, ARG, RUN), composer config policy.cooldown false and the like run there, and --no-cooldown or --no-blocking on a composer command there. Cooldown settings count when depcurfew.json sets a cooldown, allow-plugins when it denies dependency code; malware blocking and filter give warnings. COMPOSER_HOME/config.json is not read.

Fix: Remove the setting or the variable, or limit it to the repository or the run that needs it.

bundler-config-weakens

error · needs depcurfew.json (without one: a note against the default policy) · .bundle/config, the env of a CI workflow or a bundle command in a CI job or a Dockerfile turns the cooldown of Bundler off or makes it shorter than the policy.

Bundler 4.0.13 and later hold back gem versions younger than the cooldown: of the Gemfile source, but the cooldown setting and the --cooldown option take its place for every source. BUNDLE_COOLDOWN with fewer days than depcurfew.json, or 0, in .bundle/config (read line by line as Bundler reads it, not as YAML: quotes stay unless they enclose the whole value, and # starts a comment), in a CI workflow (env, $GITHUB_ENV, before a bundle command) or a Dockerfile (ENV, ARG), bundle config set cooldown N in a run step or a RUN instruction, and a shorter --cooldown on bundle install, update, lock, add or cache (also bundle i, bundle package and bundle _VERSION_ install) turn the delay off or shorten it while the Gemfile still looks configured. Bundler reads the value with to_i: one that does not start with a digit counts as 0, and 1e3 as 1. A value with a ${{ }} expression gives a warning.

Fix: Remove the setting, the variable or the option, or give it at least the days of the policy; take a newer version of one gem by hand with bundle update GEM --cooldown 0 after reviewing it.

config-drift

error · needs depcurfew.json (without one: a note against the default policy) · 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 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 too: one that lets a package of installScripts.deny run its scripts is an error; one that lets a package the policy does not name run them, blocks a package the policy allows, or is not read by the version in use yet (Deno before 2.6.0) is a warning, as is an entry of config.policy.cooldown.ignore in composer.json that exempts a package cooldown.exclude does not, unless apply wrote it. What apply cannot write is an error where it says: the cooldown of Bundler in a Gemfile (a source with a block, a modifier or in other code, cooldown: twice, a URL from ENV, no rubygems.org source, a Gemfile depcurfew cannot read as Ruby does; a warning if that call waits as long as the policy), and a --no-binary or --only-binary=:none: of pip it cannot comment out.

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 and the dependencies of the ecosystems updated by bots only 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 is reported, and one with a matcher depcurfew cannot read gets a note. The actions of workflows and the dependencies of Cargo, Go, Maven, Gradle and NuGet must wait cooldown.minimum; without a bot, the latter get a note. An unreadable bot file (JSON5, 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 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 and the ecosystems updated by bots only as long as cooldown.minimum (depcurfew apply does this).

broad-exclusion

error · An exception list matches every package, or a per-scope setting or a Renovate rule exempts packages cooldown.exclude does not, which turns the protection off.

A release-age exception such as "*" (or a policy allow-list entry such as "@*/*") looks narrow 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, manager (matchManagers) or category (matchCategories) in use, for major, minor or patch updates of every package, or with only negative patterns; with a cooldown for Renovate, also for the actions of the workflows (the github-actions manager, every workflow file, the depType action) and for Cargo, Go modules, Maven, Gradle or NuGet in the repository. A Renovate rule that gives npm, Python or Composer packages of some names a shorter delay than cooldown.minimum is an exception cooldown.exclude must make too, and a rule depcurfew apply wrote for the names of one registry must be limited to its datasource. In Yarn 4.17 and later, a shorter npmScopes.<scope>.npmMinimalAgeGate exempts that scope.

Fix: Replace the pattern with the packages or scopes that really need the exception.

lockfile-forbidden-source

error · needs depcurfew.json (without one: a note against the default policy) · 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 (without one: a note against the default policy) · 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 or the local OSV.dev data of depcurfew osv update: a lockfile locks a package version that OSV.dev lists as malicious.

Doctor looks up the exact 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) and reports each version that has a record of the OpenSSF malicious-packages database (an id starting with MAL-): with --osv in api.osv.dev (which then gets the names and versions), else offline in the local OSV.dev data of depcurfew osv update. It looks up what the tool installs (for npm and Yarn Classic the tarball of resolved, for pnpm that of resolution.tarball, for Yarn the resolution); an entry that says otherwise gets a warning, as does a registry entry without an exact version. Local data older than 7 days gives a note; older than 30 days, or in a directory others can write, a warning; OSV.dev out of reach gives a warning. Only reported malware is found, and the database admits false positives. With neither --osv nor local data, a note says how to get them.

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.

lockfile-tampered

error · A package of composer.lock, or a repository of composer.json, has a transport option that Composer calls as a PHP function while downloading (GHSA-jhfp-4ppm-crmm, fixed in Composer 2.11.2).

Composer 2.10.0-RC2 to 2.11.1 take prevent_url_access_callable from the transport-options of a package in composer.lock and, when it names a PHP function, call it with the URL of each download of the package, so an edited composer.lock can run a command during composer install (GHSA-jhfp-4ppm-crmm). Composer 2.7.3 to 2.11.1 do the same with prevent_ip_access_callable and the IP address of the server. Composer 2.11.2 no longer takes them from composer.lock. composer.json can set them too: in the options of a Composer or VCS repository, which Composer uses for its downloads and copies into composer.lock for the packages a Composer repository serves from its own URL, and in the transport-options of a package a package repository defines, which it copies there too. The rule checks the packages and packages-dev of composer.lock and the repositories of composer.json whatever the Composer version, which depcurfew cannot tell, and leaves other transport options, such as symlink of a path repository, alone.

Fix: Use Composer 2.11.2 or later before any composer install or update, then remove the transport option from composer.lock and composer.json, or restore them from before the change, and find out who added it.

unreadable-config

error · without depcurfew.json also a note against the default policy · 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.