depcurfew 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # depcurfew
2
2
 
3
- One supply-chain security policy for npm, pnpm, Yarn, Bun, Deno, uv, pip, Poetry, PDM, Pipenv, Renovate and Dependabot, and a doctor that finds configuration that silently does nothing.
3
+ 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.
4
4
 
5
5
  ## Why
6
6
 
@@ -91,10 +91,10 @@ Every section is optional: a section that is absent means depcurfew does not man
91
91
  | `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. |
92
92
  | `cooldown.exclude` | | `[]` | Packages that skip the release-age delay, e.g. your own scope. |
93
93
  | `cooldown.securityUpdates` | `"bypass"`, `"delay"` | `"bypass"` | Whether security updates skip the delay ("bypass") or wait like other updates ("delay"), where the tool can tell them apart. |
94
- | `installScripts` | | | Code that dependencies run when they are installed: lifecycle scripts in npm, building from source in Python. |
95
- | `installScripts.default` | `"allow"`, `"deny"` | | Whether dependency lifecycle scripts (preinstall, install, postinstall) run by default; for Python, whether packages may be built from source. |
96
- | `installScripts.allow` | | `[]` | Packages whose lifecycle scripts may run when the default is "deny"; Python packages listed here are always built from source. |
97
- | `installScripts.deny` | | `[]` | 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. 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. |
94
+ | `installScripts` | | | Code that dependencies run when they are installed: lifecycle scripts in npm, building from source in Python, plugins in Composer. |
95
+ | `installScripts.default` | `"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. |
96
+ | `installScripts.allow` | | `[]` | 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. |
97
+ | `installScripts.deny` | | `[]` | 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. |
98
98
  | `sources` | | | Where dependencies may come from besides the registry. An absent field is not managed. |
99
99
  | `sources.git` | `"allow"`, `"deny"` | | Dependencies installed from git repositories. |
100
100
  | `sources.remoteTarball` | `"allow"`, `"deny"` | | Dependencies installed from tarball URLs outside the registry. |
@@ -123,6 +123,8 @@ List those directories under `projects`, relative to the repository root with `/
123
123
 
124
124
  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`).
125
125
 
126
+ 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.
127
+
126
128
  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.
127
129
 
128
130
  `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.
@@ -136,10 +138,10 @@ The package installs two names for the same command: `depcurfew` and the short `
136
138
  | `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. |
137
139
  | `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. |
138
140
  | `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. |
139
- | `depcurfew doctor [directory] [--json \| --sarif] [--fail-on error\|warning]` | 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`). |
140
- | `depcurfew check <package...> [--json] [--dir <directory>] [--registry <url>] [--index <url>]` | Checks packages against the policy before you install them: see [Guarding installs](#guarding-installs). |
141
- | `depcurfew check --hook claude-code` | The same check as a Claude Code hook that blocks install commands breaking the policy. |
142
- | `depcurfew mcp [--dir <directory>] [--registry <url>] [--index <url>]` | 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](#mcp-server). |
141
+ | `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](#known-malicious-versions-osv). |
142
+ | `depcurfew check <package...> [--json] [--dir <directory>] [--registry <url>] [--index <url>] [--osv]` | Checks packages against the policy before you install them: see [Guarding installs](#guarding-installs). |
143
+ | `depcurfew check --hook claude-code [--osv]` | The same check as a Claude Code hook that blocks install commands breaking the policy. |
144
+ | `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](#mcp-server). |
143
145
  | `depcurfew init --user [--force] [--json]` | Writes the user policy, which `--user` uses: see [Protecting the whole machine](#protecting-the-whole-machine). |
144
146
  | `depcurfew apply --user [--dry-run] [--json] [--for <tool@version>]` | Writes the user policy into the user-level configuration of the installed package managers. |
145
147
  | `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. |
@@ -177,6 +179,8 @@ How `apply` edits files:
177
179
 
178
180
  `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.
179
181
 
182
+ 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`).
183
+
180
184
  <!-- BEGIN GENERATED: init-codes -->
181
185
  Warnings:
182
186
 
@@ -188,6 +192,12 @@ Notes:
188
192
  - `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.
189
193
  - `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.
190
194
  - `tool-only-in-ci`: A tool appears only in a CI setup step, so depcurfew apply does not configure it under targets "auto".
195
+ - `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.
196
+ - `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.
197
+ - `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.
198
+ - `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.
199
+ - `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.
200
+ - `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.
191
201
  <!-- END GENERATED: init-codes -->
192
202
 
193
203
  ### apply warnings and errors
@@ -218,12 +228,15 @@ Warnings:
218
228
  - `cooldown-environment-only`: pip reads no project configuration, so the release-age delay has to be set in the environment where pip runs.
219
229
  - `exclude-pattern-unsupported`: A cooldown exception uses a wildcard the tool does not support, so it is not written.
220
230
  - `exclude-unsupported`: The tool has no setting for cooldown exceptions, so the release-age delay applies to every package.
231
+ - `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.
232
+ - `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.
221
233
  - `allow-pattern-unsupported`: An installScripts.allow entry is a pattern, but the tool takes package names only, so it is not written.
222
234
  - `deny-pattern-unsupported`: An installScripts.deny entry is a pattern, but the tool records denials by package name only, so it is not written.
223
235
  - `allow-all-unsupported`: The tool has no setting that allows every install script, so installScripts "allow" is not written.
224
- - `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, because the policy denies install scripts.
236
+ - `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.
225
237
  - `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.
226
238
  - `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.
239
+ - `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.
227
240
  - `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.
228
241
  - `built-in-list-replaced`: Bun's built-in list of trusted packages is replaced by the policy's allow-list.
229
242
  - `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.
@@ -238,13 +251,14 @@ Warnings:
238
251
  - `build-switch-install-only`: Poetry applies its build switch when installing; poetry lock may still build source distributions.
239
252
  - `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.
240
253
  - `requirements-file-missing`: There is no requirements file to write pip options into.
241
- - `manifest-missing`: A setting belongs in a project manifest (package.json, deno.json, pyproject.toml, Pipfile) that does not exist, and apply does not create manifests.
254
+ - `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.
242
255
  - `lockfile-out-of-date`: The lockfile records, or is checked against, a setting apply changed, so it has to be locked again.
243
256
  - `file-ignored-by-git`: A file apply writes matches a .gitignore rule, so the change may never reach the repository.
244
257
  - `bot-config-missing`: A bot has no configuration file, and depcurfew does not create one.
245
258
  - `bot-config-not-edited`: A bot configuration file is in a place or format depcurfew does not edit; a snippet shows what to add.
246
259
  - `bot-entries-missing`: The Dependabot configuration has no entries for the package managers in use.
247
- - `bot-rule-shortens-delay`: A Renovate packageRules entry apply does not manage gives every package of a registry, or some update types, a shorter release-age delay than the one apply writes; Renovate uses it, and apply does not change it.
260
+ - `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.
261
+ - `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.
248
262
  - `security-delay-unsupported`: Dependabot cannot delay security updates, so securityUpdates "delay" is not applied.
249
263
 
250
264
  Errors (nothing is written):
@@ -353,27 +367,28 @@ Fix: Unset the variable, or set it to a value the policy allows.
353
367
  What depcurfew configures and checks for each tool, from its [knowledge base](https://github.com/DevXord/depcurfew/blob/main/docs/knowledge.md), where every value links to the official documentation or changelog it was verified against.
354
368
 
355
369
  <!-- BEGIN GENERATED: tools -->
356
- | Tool | Release-age delay | Exceptions | Install-script allow-list (Python: packages built from source) | Dependency sources | Verified |
370
+ | Tool | Release-age delay | Exceptions | Install-script allow-list (Python: packages built from source; Composer: plugins) | Dependency sources | Verified |
357
371
  | --- | --- | --- | --- | --- | --- |
358
- | 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-git`<br>remote tarball: `allow-remote`<br>local tarball: `allow-file`<br>local directory: `allow-directory` | 2026-10-28 – 2026-11-01 |
359
- | pnpm | `minimumReleaseAge` in `pnpm-workspace.yaml` (minutes), since 10.16.0 | `minimumReleaseAgeExclude`, since 10.16.0 | `allowBuilds` in `pnpm-workspace.yaml` | git: `blockExoticSubdeps`<br>remote tarball: `blockExoticSubdeps` | 2026-10-28 – 2026-11-01 |
360
- | Yarn (Berry) | `npmMinimalAgeGate` in `.yarnrc.yml` (minutes), since 4.10.2 | `npmPreapprovedPackages`, since 4.10.0 | `dependenciesMeta.<package>.built` in `package.json` | git: `approvedGitRepositories` | 2026-10-28 – 2026-11-01 |
361
- | Bun | `install.minimumReleaseAge` in `bunfig.toml` (seconds), since 1.3.0 | `install.minimumReleaseAgeExcludes`, since 1.3.0 | `trustedDependencies` in `package.json` | none | 2026-10-28 – 2026-11-01 |
362
- | Deno | `minimumDependencyAge` in `deno.json`, `.npmrc` (minutes), since 2.5.5 / 2.6.0 ⚠️ | `minimumDependencyAge.exclude`, since 2.5.5 | `allowScripts` in `deno.json` | none | 2026-10-28 – 2026-11-01 |
363
- | 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-01 |
364
- | 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-01 |
365
- | Poetry | `solver.min-release-age` in `poetry.toml` (days), since 2.4.0 | `solver.min-release-age-exclude`, since 2.4.0 | `installer.no-binary` in `poetry.toml` | none | 2026-10-30 – 2026-11-01 |
366
- | 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-01 |
367
- | Pipenv | `cool-down-period` in `Pipfile` (duration string), since 2026.6.2 | none | none | none | 2026-10-30 – 2026-11-01 |
368
- | 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-01 |
369
- | Dependabot | `cooldown` in `.github/dependabot.yml`, `.github/dependabot.yaml` (days), since 2025 07 01 | `cooldown.exclude`, since 2025 07 01 ❓ | none | n/a | 2026-10-28 – 2026-11-01 |
372
+ | 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-git`<br>remote tarball: `allow-remote`<br>local tarball: `allow-file`<br>local directory: `allow-directory` | 2026-10-28 – 2026-11-02 |
373
+ | pnpm | `minimumReleaseAge` in `pnpm-workspace.yaml` (minutes), since 10.16.0 | `minimumReleaseAgeExclude`, since 10.16.0 | `allowBuilds` in `pnpm-workspace.yaml` | git: `blockExoticSubdeps`<br>remote tarball: `blockExoticSubdeps` | 2026-10-28 – 2026-11-02 |
374
+ | Yarn (Berry) | `npmMinimalAgeGate` in `.yarnrc.yml` (minutes), since 4.10.2 | `npmPreapprovedPackages`, since 4.10.0 | `dependenciesMeta.<package>.built` in `package.json` | git: `approvedGitRepositories` | 2026-10-28 – 2026-11-02 |
375
+ | Bun | `install.minimumReleaseAge` in `bunfig.toml` (seconds), since 1.3.0 | `install.minimumReleaseAgeExcludes`, since 1.3.0 | `trustedDependencies` in `package.json` | none | 2026-10-28 – 2026-11-02 |
376
+ | Deno | `minimumDependencyAge` in `deno.json`, `.npmrc` (minutes), since 2.5.5 / 2.6.0 ⚠️ | `minimumDependencyAge.exclude`, since 2.5.5 | `allowScripts` in `deno.json` | none | 2026-10-28 – 2026-11-02 |
377
+ | 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 |
378
+ | 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 |
379
+ | Poetry | `solver.min-release-age` in `poetry.toml` (days), since 2.4.0 | `solver.min-release-age-exclude`, since 2.4.0 | `installer.no-binary` in `poetry.toml` | none | 2026-10-30 – 2026-11-02 |
380
+ | 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 |
381
+ | Pipenv | `cool-down-period` in `Pipfile` (duration string), since 2026.6.2 | none | none | none | 2026-10-30 – 2026-11-02 |
382
+ | Composer | `config.policy.cooldown.period` in `composer.json` (duration string), since 2.11.0 | `config.policy.cooldown.ignore`, since 2.11.0 | `config.allow-plugins` in `composer.json` | none | 2026-11-02 |
383
+ | 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 |
384
+ | Dependabot | `cooldown` in `.github/dependabot.yml`, `.github/dependabot.yaml` (days), since 2025 07 01 | `cooldown.exclude`, since 2025 07 01 ❓ | none | n/a | 2026-10-28 – 2026-11-02 |
370
385
 
371
386
  ⚠️ official sources disagree (depcurfew takes the safer reading) · ❓ official sources are incomplete. Every value with its source: [docs/knowledge.md](https://github.com/DevXord/depcurfew/blob/main/docs/knowledge.md).
372
387
  <!-- END GENERATED: tools -->
373
388
 
374
389
  Yarn Classic (1.x) is detected but not supported.
375
390
 
376
- **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. `depcurfew explain` lists what it found.
391
+ **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.
377
392
 
378
393
  ### Python projects
379
394
 
@@ -388,6 +403,33 @@ depcurfew configures uv, Poetry, PDM, Pipenv and pip from the same `depcurfew.js
388
403
  - **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"` to `targets` when its environments use the uv installer) or by the environment variables of pip.
389
404
  - **Running depcurfew.** depcurfew is a Node.js program. In a project without `package.json`, run a pinned version with `npx --yes depcurfew@<version>`; in CI the [GitHub Action](#github-action) needs nothing else.
390
405
 
406
+ ### Update bots
407
+
408
+ `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.
409
+
410
+ - **Dependabot** gets `cooldown` in each `updates` entry of an ecosystem depcurfew configures (`npm`, `bun`, `deno`, `pip`, `uv`, `composer`), with the policy's exceptions for that registry under `exclude`.
411
+ - **GitHub Actions.** When the repository has workflows in `.github/workflows` and `targets` is `"auto"` or names the bot, the actions they run wait as long as the packages. The `github-actions` entries of Dependabot get the same `default-days` (GitHub Actions reads no `semver-*-days`), and their `exclude` stays as written, because the policy's exceptions name packages, not actions. The delay at the top level of the Renovate configuration covers its `github-actions` manager too. `doctor` (`bot-cooldown-mismatch`) compares the bot's delay for the actions with `cooldown.minimum`.
412
+ - **Renovate** gets the delay at the top level of its configuration and a `packageRules` entry for the exceptions. `apply` and `doctor` read the other `packageRules` entries the way Renovate matches them, also by `matchManagers` and `matchCategories`: `npm` covers the packages of npm, pnpm and Yarn, `bun` and `deno` those of Bun and Deno, `pep621` those of uv and PDM, `poetry`, `pip_requirements` and `pipenv` those of Poetry, pip and Pipenv, `composer` those of Composer, and `github-actions` the actions of the workflows; the categories are `js`, `python`, `php` and `ci`. 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) gives `bot-rule-shortens-delay` in `apply` and `bot-cooldown-mismatch` in `doctor`, and one that turns it off gives `broad-exclusion`. For the actions, a rule with `matchFileNames` that matches every workflow file of the repository (read as Renovate reads it: `*` within one directory, `**` across them, any case, or `/regex/`) and a rule with `matchDepTypes` that matches `action`, 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, and `doctor` gives a note when it may set a shorter delay; so does a rule with a matcher depcurfew does not evaluate (`matchBaseBranches`, `matchJsonata`, `matchRepositories`, `matchCurrentAge`, and `matchFileNames` or `matchDepTypes` for the packages of a package manager), unless a package matcher already limits it to some packages. When rules give the update types different delays, `doctor` reports each one that is shorter, not only the shortest.
413
+ - **Numbers in `dependabot.yml`** are read as Dependabot's YAML library, Ruby's Psych, reads them, which follows YAML 1.1: `default-days: 010` is 8 days (octal), `1_0` and `1,0` are 10, `1:30` is 5400, and `0o12` is text. `doctor` compares 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"), and `apply` writes the policy's number in place of one Dependabot reads otherwise.
414
+ - **YAML aliases.** Dependabot does not accept YAML aliases (`*name`) in `.github/dependabot.yml` (dependabot-core loads the file without them), so `doctor` does not read a delay or an exception through one: `bot-cooldown-mismatch` gives a note at the first alias instead of a result, `apply` writes the `cooldown` of an entry out in place of an alias and warns that Dependabot reads nothing from the file (`bot-config-unreadable`), and `init` notes it, since it does not keep a longer delay from such a file in `cooldown.minimum`. GitHub Actions workflows may use aliases: `doctor` and detection read the `env` maps, jobs and steps of a workflow through them, at most 100 aliases per file.
415
+ - **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.
416
+
417
+ ### Composer projects
418
+
419
+ depcurfew configures Composer (PHP packages from Packagist) from the same `depcurfew.json`, and the bots for Composer packages: Dependabot `composer` entries and Renovate.
420
+
421
+ - **Detection.** `composer.json` or `composer.lock` in the directory means Composer. Only the root `composer.json` counts: Composer reads `config` and `repositories` from it alone. Composer reads `composer.json` as strict JSON, so `apply` does not edit one with a comment or a trailing comma (`cannot-edit`, with the line).
422
+ - **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 lowercase `vendor/name`; a name without a prefix stays an npm name. Composer reads the keys of `allow-plugins` and `ignore` without regard to case, and its `*` there matches `/` too, so depcurfew compares them the same way: `"Symfony/Flex": true` is the plugin `composer:symfony/flex`.
423
+ - **The release-age delay needs Composer 2.11.0.** `apply` writes 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 (`259200` seconds, `"72 hours"`) stays as it is. Composer reads `"policy": true` and `"cooldown": true` as `{}`, so `apply` writes its object there; below `"policy": false`, `"cooldown": false` or another value that is no object it writes nothing (`cooldown-not-written`), and `explain` shows that Composer cannot apply the delay. Composer counts the delay from `published-time`, the moment Packagist received the version, which the package author cannot set. The delay applies when `composer update` or `composer require` picks versions; `composer install` installs what `composer.lock` locks, so review lockfile changes as before. Older Composer ignores the setting without a word, and Composer has no setting that pins its own version: `doctor` reports a `composer.lock` written by a Composer older than 2.11 (its `plugin-api-version`) and otherwise gives a note (`manager-version-too-old`).
424
+ - **Exceptions.** `apply` writes the `composer:` exceptions of the policy as the `ignore` list, adds them to a list that is there, or adds them to an `ignore` object with a reason that names `depcurfew.json`. That reason marks an entry as `apply`'s: once the policy drops the name from its exceptions, `apply` removes it. Any other entry that exempts a package the policy does not stays, also a name of a list an earlier `apply` wrote, since a list has no room for a mark: `apply` warns (`exclude-entry-outside-policy`) and `doctor` reports it (`config-drift`, a warning) with the line, until you remove it and run `composer update --lock`.
425
+ - **Lock again after `apply`.** Composer 2.11 hashes `config.policy` into `composer.lock`, so after `apply` changes it, `composer install` warns that the lock file is out of date and `composer validate` fails; `apply` says so (`lockfile-out-of-date`), only in the run that changes the setting: a later `apply` and `doctor` do not compute the hash of `composer.lock` and do not repeat it. Run `composer update --lock`, which writes the new hash without updating packages, and commit `composer.lock`. Dependabot writes `composer.lock` with Composer 2.10.3, whose hash leaves `config.policy` out, so the same happens on each of its composer pull requests once `config.policy` is there: run `composer update --lock` on the pull request's branch before merging (`doctor` says so in its note on each Dependabot `composer` entry).
426
+ - **Install scripts are plugins.** Composer runs the scripts of the root `composer.json` only; the code of dependencies that runs during Composer commands is plugins (type `composer-plugin`, and the legacy `composer-installer`, in any case). With `installScripts.default: "deny"`, `apply` writes the plugins of `installScripts.allow` into `allow-plugins` as `true` and those of `installScripts.deny` as `false` (patterns as they are, since Composer takes them). An unlisted plugin makes Composer ask, or fail without a terminal (one that declares `extra.plugin-optional` is skipped there). `apply` replaces `"allow-plugins": true` with 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`, gives `deny-conflicts-with-file` with the fix to put the plugin before it. `doctor` lists the plugins in `composer.lock` that `allow-plugins` lets run but the policy does not allow, and those it does not list. A `composer.lock` without `plugin-api-version` (or with one below 2.2.0) next to an empty or missing `allow-plugins` makes Composer 2.2 to 2.9 run every plugin without asking, so `doctor` reports each plugin the policy does not allow as an error; `composer update --lock` records `plugin-api-version`. `apply` does not add a catch-all `"*": false` for this: Composer takes the first key that matches, and the plugins `apply` adds go at the end, where such a key would hide them.
427
+ - **No switch for dependency sources.** Composer has no setting that forbids VCS, path, artifact or package repositories (`source-not-enforceable`), but only the root `composer.json` declares them. `doctor` reports the packages of `composer.lock` that come from such a repository, and a repository of a denied kind whose packages it cannot find in the lockfile. A package of `composer.lock` without a `dist`, or with a `dist` without a type, is installed from its `source`, a VCS checkout, whatever `composer.json` declares, so it counts as a VCS package, and so does one that `config.preferred-install` of `composer.json` makes 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 of `config.preferred-install`, at its line in `composer.json`, naming the packages with their lines in `composer.lock`, and the fix is in `composer.json` (remove the entry or set it to `"dist"`), or `sources.git: "allow"`; `init` notes them (`composer-source-checkouts`). An archive from `api.github.com`, `gitlab.com`, `bitbucket.org` or `codeberg.org` next to a source on another host than its own (`github.com` for `api.github.com`; hosts are compared as URLs compare them, so `github.com:443` is `github.com`, and `www.github.com` is `github.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`, with `preferred-install` set in `COMPOSER_HOME/config.json`, and before 2.10.0 when the archive cannot be downloaded (2.10 does that only with `source-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 of `composer.json` use 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 of `composer.json` may keep its archives on another host than its own; depcurfew knows that of WPackagist (`https://wpackagist.org` serves `downloads.wordpress.org`), and for another one, such as `packages.drupal.org` with its archives on `ftp.drupal.org`, the warning names the declared repositories and says how to check that one of them serves those archives, since `composer.lock` does not say which repository a package comes from. `doctor --osv` looks up every release of `composer.lock`, also one from such a host and one Composer checks out of its source, but not a package of a VCS repository of `composer.json`, whose code is not the release on Packagist.
428
+ - **Settings that turn protections off.** The rule `composer-config-weakens` reports `"policy": false`, a `cooldown` or `block` turned off, malware blocking turned off or limited to updates, a repository with `"require-published-time": false` or `"filter": false`, `"allow-plugins": true` (or a key that matches every plugin, `"*"` or `"*/*"`) while the policy denies plugins, `"secure-http": false`, `"disable-tls": true`, and `COMPOSER_POLICY=0`, `COMPOSER_NO_BLOCKING` or `COMPOSER_NO_SECURITY_BLOCKING` set to `1`, `true`, `on`, `false` or `off` (any other value makes Composer stop with an error), a shorter `COMPOSER_POLICY_COOLDOWN_PERIOD`, `COMPOSER_POLICY_COOLDOWN_BLOCK=0` or `COMPOSER_POLICY_MALWARE_BLOCK=0` in the `env` of a CI workflow.
429
+ - **Bots.** `apply` gives Dependabot `composer` entries the cooldown and the `composer:` exceptions, and Renovate a rule for them limited to the `packagist` datasource. Dependabot counts its composer cooldown from the release time the package author writes and updates `composer.lock` with a Composer without the cooldown, so `doctor` gives each such entry a note; Renovate counts from `published-time`.
430
+ - **Checking before an install.** `depcurfew check composer:vendor/name` (also `composer:vendor/name:^3.9`, as `composer require` takes it), the Claude Code hook (`composer require`, `composer update` of named packages, `composer global require`, `composer create-project`) and `check_packages` of `depcurfew mcp` look Composer packages up on Packagist: see [Guarding installs](#guarding-installs).
431
+ - **Not covered.** `doctor` reads the Composer variables of the `env` maps of a workflow, not options and variables in a `run` step (`composer update --no-cooldown`, `--no-blocking`, `COMPOSER_POLICY=0 composer update`). `COMPOSER_HOME/config.json` and `COMPOSER_HOME/composer.json` (Composer's configuration for the whole machine and its global project) are not read or written, so a repository, a `minimum-stability` or a `preferred-install` they set is not seen by `doctor`, `check` or the hook; the hook asks about a command that sets `COMPOSER_HOME` itself, and about `composer global update` of a package named without a constraint, whose constraint is in `COMPOSER_HOME/composer.json`. Command-line options in CI steps, such as `--prefer-source`, are not read either.
432
+
391
433
  ## doctor rules
392
434
 
393
435
  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`).
@@ -421,7 +463,7 @@ Fix: Run the project's copy of depcurfew (install the project's dependencies fir
421
463
 
422
464
  **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.
423
465
 
424
- 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.
466
+ 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.
425
467
 
426
468
  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.
427
469
 
@@ -433,6 +475,14 @@ After its end of life a Node.js release line gets no more releases, security fix
433
475
 
434
476
  Fix: Move to a Node.js line that is still supported, in every place that declares the version.
435
477
 
478
+ ### `corepack-unavailable`
479
+
480
+ **warning** · packageManager or devEngines.packageManager pins Yarn or npm, but a declared Node.js is 25 or later, which no longer comes with Corepack.
481
+
482
+ 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.
483
+
484
+ Fix: Install Corepack where that Node.js runs (npm install -g corepack, then corepack enable), before the pinned tool runs.
485
+
436
486
  ### `setting-ignored-by-version`
437
487
 
438
488
  **error** · A setting or file is ignored by the package manager version in use.
@@ -465,27 +515,35 @@ uv reads uv.toml instead of [tool.uv] when both exist, and in a workspace only t
465
515
 
466
516
  Fix: Move the setting to the file the tool reads.
467
517
 
518
+ ### `composer-config-weakens`
519
+
520
+ **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.
521
+
522
+ 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.
523
+
524
+ Fix: Remove the setting or the variable, or limit it to the repository or the run that needs it.
525
+
468
526
  ### `config-drift`
469
527
 
470
528
  **error** · needs depcurfew.json · A configuration file does not match what depcurfew apply writes for depcurfew.json.
471
529
 
472
- 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.
530
+ 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.
473
531
 
474
532
  Fix: Run depcurfew apply (depcurfew apply --dry-run shows the difference), or change depcurfew.json if the edit was intended.
475
533
 
476
534
  ### `bot-cooldown-mismatch`
477
535
 
478
- **error** · An update bot waits less than the package manager before proposing a new version.
536
+ **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.
479
537
 
480
- The bot then opens pull requests with versions the package manager would not pick yet: they fail to install where the manager re-checks the lockfile, or they bring in a version younger than the manager allows. Security updates that skip the bot delay fail the same way where the manager re-checks the lockfile. A Dependabot cooldown with an include list delays only the dependencies it names, and a Renovate packageRules entry can shorten the delay for a whole registry or update type. A bot configuration file that depcurfew cannot read or parse, or a Renovate file in JSON5, gets a note for each package manager that waits, instead of a delay the file may not have. With projects in depcurfew.json, a bot configuration file inside a project gets a note, because the bot reads its configuration only from the repository root, and a project that the Dependabot configuration of the root leaves out while it updates other projects gets a warning.
538
+ 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.
481
539
 
482
- Fix: Give the bot a delay at least as long as the package manager's (depcurfew apply does this).
540
+ 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).
483
541
 
484
542
  ### `broad-exclusion`
485
543
 
486
544
  **error** · An exception list matches every package, or a per-scope setting shortens the delay, which turns the protection off.
487
545
 
488
- 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, for major, minor or patch updates of every package, or with only negative patterns, which match every package they do not name. In Yarn 4.17 and later, npmScopes.<scope>.npmMinimalAgeGate replaces the delay for one scope, so a shorter value there exempts that scope.
546
+ 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.
489
547
 
490
548
  Fix: Replace the pattern with the packages or scopes that really need the exception.
491
549
 
@@ -493,7 +551,7 @@ Fix: Replace the pattern with the packages or scopes that really need the except
493
551
 
494
552
  **error** · needs depcurfew.json · The lockfile installs a package from a source depcurfew.json denies.
495
553
 
496
- 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.
554
+ 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.
497
555
 
498
556
  Fix: Replace the dependency with a registry version, or allow that source in depcurfew.json.
499
557
 
@@ -501,9 +559,9 @@ Fix: Replace the dependency with a registry version, or allow that source in dep
501
559
 
502
560
  **error** · needs depcurfew.json · The lockfile has packages with install scripts, or Python packages built from source, that depcurfew.json does not allow.
503
561
 
504
- Install scripts run arbitrary code during installation, and so does building a Python package from source (a source distribution, a git repository or a local directory). Where the package manager runs them by default, or an entry in its own allow-list lets them run, these packages execute code the policy does not allow; where it blocks them, they may not work without a build step. A package in installScripts.deny was reviewed and works without its scripts, so it is reported only while the package manager still runs them (or builds it from source), or when it cannot be installed without a build. Only npm, Bun (1.4.4+) and Deno (lockfile v5) record install scripts in the lockfile; 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 (depcurfew check pypi:<name> shows whether one has wheels), only to requirements from a repository, a URL or a local path.
562
+ 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.
505
563
 
506
- 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:). For a package in installScripts.deny, make the package manager block its scripts (depcurfew apply records the denial where the tool can).
564
+ 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.
507
565
 
508
566
  ### `lockfile-out-of-date`
509
567
 
@@ -513,6 +571,14 @@ uv records the release-age delay (exclude-newer and exclude-newer-package) in uv
513
571
 
514
572
  Fix: Lock again (uv lock or pdm lock) and commit the lockfile.
515
573
 
574
+ ### `known-malicious`
575
+
576
+ **error** · With --osv: a lockfile locks a package version that OSV.dev lists as malicious.
577
+
578
+ 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.
579
+
580
+ 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.
581
+
516
582
  ### `unreadable-config`
517
583
 
518
584
  **error** · A configuration file or lockfile cannot be read, parsed or edited, so depcurfew cannot check it.
@@ -526,7 +592,7 @@ Fix: Fix what the message describes: invalid syntax, missing read permission, a
526
592
 
527
593
  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.
528
594
 
529
- 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. If the repository cannot use code scanning, set `upload-sarif: false`; the findings stay in the job log.
595
+ 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](#known-malicious-versions-osv)).
530
596
 
531
597
  <!-- BEGIN GENERATED: action-workflow -->
532
598
  ```yaml
@@ -551,7 +617,7 @@ jobs:
551
617
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
552
618
  with:
553
619
  persist-credentials: false
554
- - uses: DevXord/depcurfew@25fb6165f2d19767eb4d35c92606500e104b09f9 # v1.2.0
620
+ - uses: DevXord/depcurfew@20fcc21389f308180a2f9a3a404a486f08705c15 # v1.4.0
555
621
  ```
556
622
  <!-- END GENERATED: action-workflow -->
557
623
 
@@ -568,6 +634,7 @@ jobs:
568
634
  | `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. |
569
635
  | `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. |
570
636
  | `github-token` | `${{ github.token }}` | Token for the depcurfew doctor check; the workflow token by default. |
637
+ | `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. |
571
638
 
572
639
  | Output | Description |
573
640
  | --- | --- |
@@ -595,7 +662,7 @@ depcurfew:
595
662
 
596
663
  ## What depcurfew does not do
597
664
 
598
- depcurfew makes the package managers' own protections work and checks a package's age, name, install scripts and source before it is installed. It does not know which packages are malicious: it has no malware database and does not scan package contents, so a malicious version older than the cooldown passes. Use it together with a tool that blocks known malware at install time, and keep `npm audit` or an equivalent for known vulnerabilities. It also does not run or sandbox anything: the protection comes from the configuration the package managers read.
665
+ 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](#known-malicious-versions-osv). 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.
599
666
 
600
667
  ## Guarding installs
601
668
 
@@ -604,36 +671,58 @@ The configuration `apply` writes is what really protects a project: the package
604
671
  ```bash
605
672
  npx depcurfew check zod@4 left-pad
606
673
  npx depcurfew check pypi:requests "pypi:django>=5.2,<6"
674
+ npx depcurfew check composer:monolog/monolog "composer:symfony/console:^7.4"
607
675
  ```
608
676
 
609
- 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), or PyPI for names with the `pypi:` prefix, and reports:
677
+ 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:
610
678
 
611
679
  - a name the registry does not know, which is how made-up package names show up;
612
680
  - a version younger than the cooldown in `depcurfew.json`, with the newest version that is old enough;
613
- - 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. A package in `installScripts.deny` is not reported, because its scripts are meant to stay blocked (`doctor` checks that the package manager blocks them), except a Python package without wheels, which gets a warning: it cannot be installed where building is blocked;
614
- - 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;
615
- - a deprecated version (a warning);
616
- - a registry or index reached over plain HTTP (a warning), unless it is on this computer (`localhost`, `127.0.0.0/8` or `::1`), such as a local registry for testing.
681
+ - 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-plugin` or the legacy `composer-installer`, in any case), whose code runs during Composer commands. A package in `installScripts.deny` is not reported, because its scripts are meant to stay blocked (`doctor` checks that the package manager blocks them), except a Python package without wheels, which gets a warning: it cannot be installed where building is blocked;
682
+ - 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);
683
+ - a deprecated version, or an abandoned Composer package (a warning);
684
+ - a registry or index reached over plain HTTP (a warning), unless it is on this computer (`localhost`, `127.0.0.0/8` or `::1`), such as a local registry for testing;
685
+ - with `--osv`, a version OSV.dev lists as malicious (`known-malicious`, an error), as described below.
617
686
 
618
- 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), 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`.
687
+ 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`.
619
688
 
620
689
  `.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.
621
690
 
622
691
  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.
623
692
 
693
+ 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.
694
+
624
695
  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.
625
696
 
697
+ ### Known malicious versions (OSV)
698
+
699
+ `--osv` asks [OSV.dev](https://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.
700
+
701
+ ```bash
702
+ npx depcurfew check --osv zod@4 pypi:requests
703
+ npx depcurfew doctor --osv
704
+ ```
705
+
706
+ - `check`, the Claude Code hook (`check --hook claude-code --osv`) and `check_packages` of `depcurfew mcp --osv` look 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 with `MAL-`) is an error with the code `known-malicious`: it names the record and links its page, such as `https://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 the `args` of the hook command in `.claude/settings.json` (see below); for `depcurfew mcp`, after `"mcp"`.
707
+ - 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`: `check` exits 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.
708
+ - `doctor --osv` (and the Action with `osv: 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 in `composer.lock` (as Composer wrote them, `v7.3.4` or `3.9.0`), in the root and in every directory of `projects`. 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: for `package-lock.json` the registry tarball of `resolved`, whose name and version npm reads from its path (`/cutekitten-test/-/cutekitten-test-1.0.1.tgz`), for `pnpm-lock.yaml` the registry tarball of `resolution.tarball` when there is one (a relative one too, which pnpm reads against the registry), and for `yarn.lock` the version of `resolution`. 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 rule `known-malicious` reports 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 `--osv` the rule does not run and shows as not applicable.
709
+ - 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.
710
+
711
+ **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.
712
+
713
+ **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.
714
+
626
715
  ### Claude Code
627
716
 
628
717
  `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:
629
718
 
630
- - **deny** when a package breaks the policy (Claude sees the reason and can pick another package or version);
631
- - **ask** when depcurfew cannot vouch for the command: a look-alike name, a registry it cannot reach, 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 in `depcurfew.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 in `npm ci`, and when the same command changes the package manager's configuration or `package.json` next to an install or another command of the package manager (`npm config set registry … && npm ci`, also spelled `npm conf` or `npm c`, `npm pkg set overrides.… && npm install`);
719
+ - **deny** when a package breaks the policy, or with `--osv` is a known malicious version (Claude sees the reason and can pick another package or version);
720
+ - **ask** when depcurfew cannot vouch for the command: a look-alike name, a registry it cannot reach (with `--osv` also 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 in `depcurfew.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 in `npm ci`, and when the same command changes the package manager's configuration or `package.json` next to an install or another command of the package manager (`npm config set registry … && npm ci`, also spelled `npm conf` or `npm c`, `npm pkg set overrides.… && npm install`, `poetry source add … && poetry add x`, `pdm config pypi.url … && pdm add x`);
632
721
  - nothing when everything checks out, so the command goes through Claude Code's usual permission prompts.
633
722
 
634
723
  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.
635
724
 
636
- 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. 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.
725
+ 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.
637
726
 
638
727
  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).
639
728
 
@@ -641,7 +730,9 @@ The hook takes its policy from the project Claude Code works on (`CLAUDE_PROJECT
641
730
 
642
731
  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.
643
732
 
644
- 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. 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.
733
+ 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.
734
+
735
+ 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.
645
736
 
646
737
  Install depcurfew as a development dependency of the project, then add the hook:
647
738
 
@@ -719,6 +810,7 @@ Commands the guard reads (an add subcommand of any npm package manager, such as
719
810
  | Poetry | `poetry add` | none | none |
720
811
  | PDM | `pdm add` | none | none |
721
812
  | Pipenv | `pipenv install` | none | none |
813
+ | Composer | `composer require`, `composer r`, `composer global require`, `composer global r` | none | `composer create-project` |
722
814
  <!-- END GENERATED: claude-code-hook -->
723
815
 
724
816
  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.
@@ -727,10 +819,10 @@ The hook reads commands the way a shell splits them, but it is a safety net, not
727
819
 
728
820
  `depcurfew mcp` runs the same check as a [Model Context Protocol](https://modelcontextprotocol.io) 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:
729
821
 
730
- - `check_packages` takes `packages`, a list of 1 to 50 specs as `depcurfew check` takes them (`zod@4`, `pypi:requests`), and an optional `directory`. It returns the document of `check --json` (`schemaVersion`, `policy`, `packages` with 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 a `depcurfew.json` that cannot be used, a directory that does not exist or a wrong argument, with a message that says what to do.
731
- - `explain_policy` takes an optional `directory` and returns which `depcurfew.json` applies (`null` and the default policy of `depcurfew init` when there is none), the release-age delay and its exceptions, the install-script lists, the allowed sources, and the registry and index `check_packages` asks, with the scopes `.npmrc` assigns to other registries as a list of `scope` and `registry`. It reads local files only.
822
+ - `check_packages` takes `packages`, a list of 1 to 50 specs as `depcurfew check` takes them (`zod@4`, `pypi:requests`, `composer:monolog/monolog`), and an optional `directory`. It returns the document of `check --json` (`schemaVersion`, `policy`, `packages` with 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 a `depcurfew.json` that cannot be used, a directory that does not exist or a wrong argument, with a message that says what to do.
823
+ - `explain_policy` takes an optional `directory` and returns which `depcurfew.json` applies (`null` and the default policy of `depcurfew init` when there is none), the release-age delay and its exceptions, the install-script lists, the allowed sources, and the registry and index `check_packages` asks, with the scopes `.npmrc` assigns to other registries as a list of `scope` and `registry`, `composerRepository`, the Packagist URL it asks about Composer packages, and `osv`, the OSV.dev endpoint it asks with `--osv`. It reads local files only.
732
824
 
733
- 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.
825
+ 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.
734
826
 
735
827
  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.
736
828
 
@@ -804,19 +896,23 @@ Claude Code sets `CLAUDE_PROJECT_DIR` to the project root for the server, and `d
804
896
 
805
897
  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.
806
898
 
899
+ 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.
900
+
807
901
  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.
808
902
 
809
903
  ## Network and privacy
810
904
 
811
- Only `check`, the Claude Code hook and the `check_packages` tool of `depcurfew mcp` use the network. For each package they request one URL with the package name in it: from the public npm registry (`registry.npmjs.org`) or PyPI (`pypi.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. `init`, `explain`, `apply` and `doctor` work without network access. The GitHub Action checks the repository without it too; 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.
905
+ 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](#known-malicious-versions-osv)); 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.
812
906
 
813
907
  ## Library
814
908
 
815
- The same functions are available from the package: `detectProject`, `planApply` and `applyProject`, `runDoctor` with `toSarif`, `checkSpecs` with `scanCommandLine`, and the policy parser and converters. Their types ship in `dist/index.d.ts`, and [`src/index.ts`](https://github.com/DevXord/depcurfew/blob/main/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()`.
909
+ 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`](https://github.com/DevXord/depcurfew/blob/main/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()`.
816
910
 
817
911
  ## Contributing
818
912
 
819
- Facts about package managers live only in [`src/knowledge/`](https://github.com/DevXord/depcurfew/tree/main/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.
913
+ Facts about package managers live only in [`src/knowledge/`](https://github.com/DevXord/depcurfew/tree/main/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](https://github.com/DevXord/depcurfew/blob/main/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](https://github.com/DevXord/depcurfew/blob/main/SECURITY.md).
914
+
915
+ `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](https://depcurfew.github.io/) by the repository [depcurfew/depcurfew.github.io](https://github.com/depcurfew/depcurfew.github.io), which builds it from the latest release of this repository once a day (its files are in [`site/org-repo/`](https://github.com/DevXord/depcurfew/tree/main/site/org-repo)) and serves the schema at [`schema.json`](https://depcurfew.github.io/schema.json) and [`schema/v1.json`](https://depcurfew.github.io/schema/v1.json).
820
916
 
821
917
  ## License
822
918
 
@@ -824,4 +920,4 @@ Facts about package managers live only in [`src/knowledge/`](https://github.com/
824
920
 
825
921
  ## Trademarks
826
922
 
827
- 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, 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.
923
+ 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.