depcurfew 1.1.0 → 1.3.0
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 +389 -58
- package/dist/cli.js +43 -6
- package/dist/index.d.ts +1226 -135
- package/dist/index.js +3 -1
- package/dist/io-eBMDYKV0.js +52 -0
- package/dist/run-DYkP4HLQ.js +46946 -0
- package/package.json +5 -2
- package/schema.json +41 -0
- package/dist/run-CvdVkFPn.js +0 -28211
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ depcurfew keeps one policy file, `depcurfew.json`, writes it into the configurat
|
|
|
14
14
|
2. `npx depcurfew explain` shows what it detected and what the policy means for each tool.
|
|
15
15
|
3. `npx depcurfew apply --dry-run` prints the changes as a diff, without writing anything.
|
|
16
16
|
4. `npx depcurfew apply` writes them. Run it again whenever `depcurfew.json` changes; a second run with the same policy changes nothing.
|
|
17
|
-
5. `npx depcurfew doctor` reports configuration that silently does not work and drift from the policy, and exits with 1 when it finds an error.
|
|
17
|
+
5. `npx depcurfew doctor` reports configuration that silently does not work and drift from the policy, and exits with 1 when it finds an error or, with the `"doctor": { "failOn": "warning" }` that `init` writes, a warning.
|
|
18
18
|
6. Add the [GitHub Action](#github-action) so that doctor runs on every push and reports to code scanning.
|
|
19
19
|
|
|
20
20
|
The default policy:
|
|
@@ -31,7 +31,8 @@ The default policy:
|
|
|
31
31
|
},
|
|
32
32
|
"installScripts": {
|
|
33
33
|
"default": "deny",
|
|
34
|
-
"allow": []
|
|
34
|
+
"allow": [],
|
|
35
|
+
"deny": []
|
|
35
36
|
},
|
|
36
37
|
"sources": {
|
|
37
38
|
"git": "deny",
|
|
@@ -39,22 +40,27 @@ The default policy:
|
|
|
39
40
|
"file": "allow",
|
|
40
41
|
"directory": "allow"
|
|
41
42
|
},
|
|
42
|
-
"targets": "auto"
|
|
43
|
+
"targets": "auto",
|
|
44
|
+
"doctor": {
|
|
45
|
+
"failOn": "warning"
|
|
46
|
+
}
|
|
43
47
|
}
|
|
44
48
|
```
|
|
45
49
|
<!-- END GENERATED: default-policy -->
|
|
46
50
|
|
|
47
51
|
## Installation
|
|
48
52
|
|
|
49
|
-
depcurfew
|
|
53
|
+
depcurfew runs on Node.js 18.3.0 or later, so it also works where a project or a coding agent still has an older Node.js (CI runs every command on the oldest version it supports), and has a single runtime dependency, [`yaml`](https://github.com/eemeli/yaml). Node.js 18 and 20 no longer get security fixes from the Node.js project; prefer a supported line where you can. Working on depcurfew itself needs Node.js 22.18 or later.
|
|
50
54
|
|
|
51
55
|
Add it to the project as a development dependency, so the team, CI and coding agents all run the version pinned in the lockfile. Once `apply` has written the project's release-age delay, later updates of depcurfew go through that delay as well:
|
|
52
56
|
|
|
53
57
|
```bash
|
|
54
|
-
npm install --save-dev depcurfew
|
|
58
|
+
npm install --save-dev --save-exact depcurfew
|
|
55
59
|
npx dcf doctor
|
|
56
60
|
```
|
|
57
61
|
|
|
62
|
+
Pin the exact version (`--save-exact`, or `save-exact=true` in `.npmrc`) and update it on purpose: depcurfew is a young project with frequent releases, and a range would let a new release in without review.
|
|
63
|
+
|
|
58
64
|
Who needs it installed:
|
|
59
65
|
|
|
60
66
|
- **Developers working on the project:** nothing to do. The protection does not depend on depcurfew being installed: it lives in the package manager configuration that `apply` writes into the repository (`.npmrc`, `package.json`, `pnpm-workspace.yaml`, `.yarnrc.yml`, `bunfig.toml`, `deno.json`, `pyproject.toml` or `uv.toml`, `poetry.toml`, `Pipfile`, `requirements.txt` and the bot configuration), and the package managers read it on their own. pip's release-age delay is the exception: pip reads it only from the environment where it runs, and `apply` says what to set there. The project install brings depcurfew along as a development dependency anyway.
|
|
@@ -88,70 +94,142 @@ Every section is optional: a section that is absent means depcurfew does not man
|
|
|
88
94
|
| `installScripts` | | | Code that dependencies run when they are installed: lifecycle scripts in npm, building from source in Python. |
|
|
89
95
|
| `installScripts.default` | `"allow"`, `"deny"` | | Whether dependency lifecycle scripts (preinstall, install, postinstall) run by default; for Python, whether packages may be built from source. |
|
|
90
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. |
|
|
91
98
|
| `sources` | | | Where dependencies may come from besides the registry. An absent field is not managed. |
|
|
92
99
|
| `sources.git` | `"allow"`, `"deny"` | | Dependencies installed from git repositories. |
|
|
93
100
|
| `sources.remoteTarball` | `"allow"`, `"deny"` | | Dependencies installed from tarball URLs outside the registry. |
|
|
94
101
|
| `sources.file` | `"allow"`, `"deny"` | | Dependencies installed from local tarball files. |
|
|
95
102
|
| `sources.directory` | `"allow"`, `"deny"` | | Dependencies installed from local directories. |
|
|
96
103
|
| `targets` | | `"auto"` | Tools to configure: "auto" detects them in the repository, or list them explicitly. |
|
|
104
|
+
| `projects` | | `[]` | Directories of independent projects in this repository, each with its own manifest and lockfile (e.g. frontend and backend). depcurfew configures each one with this policy as a project of its own, and configures Renovate and Dependabot once, from the repository root, for all of them. The repository root is always configured as well. A project must not be inside another one, must not be a workspace package of the root and must not have a depcurfew.json of its own. |
|
|
105
|
+
| `doctor` | | | How depcurfew doctor and the GitHub Action treat findings. Without this section only errors fail, as before; depcurfew init writes "failOn": "warning". |
|
|
106
|
+
| `doctor.failOn` | `"error"`, `"warning"` | | Which findings make depcurfew doctor exit with 1 and fail the GitHub Action: "error" (only errors, also when left out) or "warning" (warnings too). Notes never fail. doctor --fail-on and the input fail-on of the Action override it. |
|
|
97
107
|
<!-- END GENERATED: policy-fields -->
|
|
98
108
|
|
|
109
|
+
`doctor.failOn` decides which findings make `depcurfew doctor` exit with 1 and fail the GitHub Action: `"error"` only errors, `"warning"` warnings too; notes, which say what doctor could not check, never fail. `depcurfew init` writes `"warning"`, so a new repository stays free of warnings. A policy without the `doctor` section fails on errors only, as before, so an existing `depcurfew.json` keeps its exit codes. `doctor --fail-on error` or `--fail-on warning` and the input `fail-on` of the Action override it for one run; an invalid `depcurfew.json` counts as `"error"` (it fails doctor anyway, as `invalid-policy`). The user policy takes the same section for `doctor --user`.
|
|
110
|
+
|
|
111
|
+
### Several projects in one repository
|
|
112
|
+
|
|
113
|
+
A repository can hold independent projects side by side, each in its own directory with its own manifest and lockfile, often with nothing in the root but the bot configuration:
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
.github/dependabot.yml updates entries for /frontend and /backend
|
|
117
|
+
depcurfew.json "projects": ["frontend", "backend"]
|
|
118
|
+
frontend/ package.json, package-lock.json
|
|
119
|
+
backend/ pyproject.toml, uv.lock
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
List those directories under `projects`, relative to the repository root with `/` between directories (no patterns, no `..`, no `/` at either end). `apply`, `doctor` and `explain` then treat each listed directory as a project of its own with the policy of the root: its own `.npmrc`, lockfiles and workspaces, as if the command ran there. The root is configured as before. Renovate and Dependabot are configured once, from the repository root, where they read their configuration, and wait at least as long as the package manager of any project (in the example, npm rounds `36h` up to 2 days, so every Dependabot entry gets `default-days: 2`). Every path in the output is relative to the root (`frontend/.npmrc`), and all files of all projects are written or none. A `renovate.json` or `.github/dependabot.yml` inside a project is never read by the bot, so `doctor` gives a note at it instead of comparing its delay (and says so when the root has no configuration of that bot at all), and "What was checked" marks it as not read; a project that the Dependabot configuration of the root leaves out while it updates other projects gets a warning.
|
|
123
|
+
|
|
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
|
+
|
|
126
|
+
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
|
+
|
|
128
|
+
`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.
|
|
129
|
+
|
|
99
130
|
## Commands
|
|
100
131
|
|
|
101
132
|
The package installs two names for the same command: `depcurfew` and the short `dcf`, so `dcf doctor` works like `depcurfew doctor`.
|
|
102
133
|
|
|
103
134
|
| Command | What it does |
|
|
104
135
|
| --- | --- |
|
|
105
|
-
| `depcurfew init [directory] [--force]` | Writes `depcurfew.json` with the default policy (keeping a longer release-age delay the repository already sets); `--force` overwrites an existing one. |
|
|
106
|
-
| `depcurfew explain [directory] [--json]` | Describes the detected package managers, versions, bots and workspaces, and the policy
|
|
136
|
+
| `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
|
+
| `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. |
|
|
107
138
|
| `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. |
|
|
108
|
-
| `depcurfew doctor [directory] [--json \| --sarif]` | Finds configuration that silently does not work and drift from the policy; `--sarif` prints SARIF 2.1.0 for code scanning. |
|
|
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`). |
|
|
109
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). |
|
|
110
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). |
|
|
143
|
+
| `depcurfew init --user [--force] [--json]` | Writes the user policy, which `--user` uses: see [Protecting the whole machine](#protecting-the-whole-machine). |
|
|
144
|
+
| `depcurfew apply --user [--dry-run] [--json] [--for <tool@version>]` | Writes the user policy into the user-level configuration of the installed package managers. |
|
|
145
|
+
| `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. |
|
|
111
146
|
|
|
112
|
-
Without a directory, `explain`, `apply` and `doctor` work on the nearest directory with `depcurfew.json`, from the current directory up to the root of the repository (the directory with `.git`), and say so when that is a parent directory (outside a git repository only the current directory counts, and on Linux and macOS a parent directory counts only when it belongs to you and others cannot write to it, because a shared parent such as `/tmp` may hold someone else's `depcurfew.json`; directories inside `node_modules` are skipped), so `doctor` run in a subdirectory does not pass for a project without a policy. A directory given on the command line is used as it is. `check` looks for `depcurfew.json` the same way, starting in `--dir`.
|
|
147
|
+
Without a directory, `explain`, `apply` and `doctor` work on the nearest directory with `depcurfew.json`, from the current directory up to the root of the repository (the directory with `.git`), and say so when that is a parent directory (outside a git repository only the current directory counts, and on Linux and macOS a parent directory counts only when it belongs to you and others cannot write to it, because a shared parent such as `/tmp` may hold someone else's `depcurfew.json`; directories inside `node_modules` are skipped), so `doctor` run in a subdirectory does not pass for a project without a policy. A directory given on the command line is used as it is, unless the nearest `depcurfew.json` above it lists it (or a directory it lies in) under `projects`: then the command works on that root, which checks the directory as one of its projects, as when it runs in that directory, and says so (the GitHub Action does the same for its `directory` input). `check` looks for `depcurfew.json` the same way, starting in `--dir`.
|
|
113
148
|
|
|
114
|
-
Exit codes: `0` success (warnings included), `1` an error (an invalid or unreadable policy, a file that cannot be edited, a doctor finding with level error), `2` a usage error (such as an unknown option or a directory that does not exist). `depcurfew <command> --help` describes a command with examples and its exit codes.
|
|
149
|
+
Exit codes: `0` success (warnings included, except for `doctor` when warnings fail it), `1` an error (an invalid or unreadable policy, a directory in `projects` that cannot be used, a file that cannot be edited or written, a doctor finding with level error, or also one with level warning when `doctor.failOn` in `depcurfew.json` (with `--user` the user policy) or `--fail-on` is `warning`, which `--strict` means too; notes never fail), `2` a usage error (such as an unknown option or a directory that does not exist). `depcurfew <command> --help` describes a command with examples and its exit codes.
|
|
115
150
|
|
|
116
151
|
Every finding and warning says what to do about it. When a command cannot start, it says why with a code (`directory-not-found`, `not-a-directory`, `policy-missing`, `policy-invalid`, `policy-unreadable`, and for `init` also `policy-exists` and `cannot-write`) and what to do; with `--json` that is a document with `errors` on stdout, and `doctor --sarif` prints a run marked as not successful. Text for people is colored on a terminal; `NO_COLOR=1` turns colors off and `FORCE_COLOR=1` turns them on, for example in a CI log that shows them. Control characters in names and values from the repository or a registry are shown as text (ESC as `\x1b`, or as `\u001b` inside a quoted value such as a package name in the problems of `depcurfew.json`), so a crafted package or file name cannot change the terminal. `--json`, `--sarif` and a diff written to a file stay plain, for programs and AI agents.
|
|
117
152
|
|
|
118
|
-
The `--json` documents of all commands share these fields: `schemaVersion` (1), `policy` (its status in `apply` and `doctor`, the path of the `depcurfew.json` used in `check`, its values in `explain`), `notices` (
|
|
153
|
+
The `--json` documents of all commands share these fields: `schemaVersion` (1), `policy` (its status in `apply` and `doctor`, the path of the `depcurfew.json` used in `check`, its values in `explain`, the file written in `init`), `notices` (each with `level` `note`, a `code`, `location` when it concerns a file, `message`, and `fix` when there is something to do; `policy-from-parent` says that `doctor`, `apply` or `explain` uses the `depcurfew.json` of a parent directory and works on that directory) and `errors`, the reasons a command could not start, each with `code`, `level`, `location.file`, `message` and `fix`, and `issues` for `policy-invalid` (`home-missing` for a home directory that is not an absolute path, also an empty `HOME`, names the variable instead of a file: `location` is `{"file": "environment", "variable": "HOME"}`, `USERPROFILE` on Windows, as in `doctor --user`). A usage error with `--json` (an unknown option, `check` without a package, `--json` with `--sarif`) prints a document with only `schemaVersion` and `errors`, with the code `usage` and no location, and exits with 2; without `--json` it is written on stderr like the other errors, as `error: <message> (usage)` and a `fix:` line. The warnings of `explain` have `code`, `level`, `location` (when they concern one file), `message`, `files` and `fix`. Paths are relative to the directory the command works on (a directory that does not exist is named as given). A `depcurfew.json` that cannot be used is an error with the same code, message and fix in `apply`, `check` and `explain`, which also keeps it in `policy`; `doctor` reports it as the finding `invalid-policy`, so that it reaches code scanning too, and counts it in `summary.error`. Within a `schemaVersion` fields and values are only added; a field is removed or changes its meaning only with a new `schemaVersion`.
|
|
154
|
+
|
|
155
|
+
`explain` shows, for each tool in the policy section (and for each directory listed in `projects`), what `installScripts` and `sources` mean there, from the same plan `apply` makes: the settings `apply` writes with their files, or the warning `apply` gives when the tool cannot enforce a part, with its code and what to do. The text has one line per tool and part, `nothing to write` when `apply` writes nothing there and has nothing to say, and `not managed` when the policy has no such section:
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
installScripts deny, allow esbuild:
|
|
159
|
+
<tool>: <setting> and <setting> in <file>, <setting> in <file>; warning: <message> (<code>); fix: <what to do>
|
|
160
|
+
sources git deny:
|
|
161
|
+
<tool>: nothing written; warning: <tool> has no setting for git dependencies, so "deny" cannot be enforced (source-not-enforceable); fix: <what to do>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
In `explain --json`, `policy.installScripts` holds `default`, `allow`, `deny` and `tools`, `policy.sources` the sources the policy sets and `tools`, and each entry of `projects` has `installScripts` and `sources` with `tools` (each `null` when the policy has no such section). Each entry of `tools` has `tool`, `settings` (`setting`, `file`, and `table` for a TOML table) and `warnings`, the warnings of `apply --json` about that part for the tool. A setting that `apply` writes only to correct a value (`allow-all-replaced`) is listed only while the file still needs that correction.
|
|
119
165
|
|
|
120
166
|
How `apply` edits files:
|
|
121
167
|
|
|
122
168
|
- It keeps comments, formatting, line endings and key order. In line-based files (INI, TOML) it writes a block between `# >>> depcurfew managed block` and `# <<< depcurfew managed block` markers and comments out older copies of the settings it manages (they come back when the policy stops managing them); in YAML it marks the keys it writes with `# managed by depcurfew`; JSON files keep their comments and indentation.
|
|
123
|
-
- It
|
|
169
|
+
- It reads files the way the tool does. In YAML files of a tool whose loader applies merge keys (`<<: *defaults` in `pnpm-workspace.yaml` and pnpm's `config.yaml`), a setting a merge key brings in counts, and `apply` writes its value into the map itself, which wins over merged keys; `.yarnrc.yml` and `dependabot.yml` are read without merging. A switch that allows every install script counts as on for every value the tool does not read as off: pnpm runs every build script for `dangerouslyAllowAllBuilds: "false"` (a string) or `yes`, and npm and pnpm 10 for `=1` or `=yes` in `.npmrc`.
|
|
170
|
+
- It never removes settings it does not manage. Allow-lists are only added to; exception lists follow the policy, except the entries Renovate adds to pnpm's exception list for security updates, which stay with their comment. An entry in a tool's own install-script list that contradicts `installScripts` (it lets a package of `installScripts.deny` run its scripts, or blocks one of `installScripts.allow`) stays as it is, with the warning `deny-conflicts-with-file`, and `apply` records no denial for that package.
|
|
124
171
|
- When it replaces a longer release-age delay someone set by hand with the policy's shorter one, it warns (`cooldown-shortened`) and names the `cooldown.minimum` that keeps it.
|
|
125
172
|
- If any file cannot be edited, nothing is written. If a file cannot be written, the files already written are put back.
|
|
126
173
|
- It does not create bot configuration files or project manifests (`package.json`, `deno.json`, `pyproject.toml`, `Pipfile`, `requirements.txt`); it creates a package manager's configuration file when it is missing. A package manager seen only in a CI setup step, with nothing of it in the repository, is not configured under `targets: "auto"`.
|
|
127
174
|
- It warns when a file it writes is ignored by git (`.gitignore` or `.git/info/exclude`), because the change would then stay on one machine.
|
|
128
175
|
|
|
176
|
+
### init warnings and notes
|
|
177
|
+
|
|
178
|
+
`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
|
+
|
|
180
|
+
<!-- BEGIN GENERATED: init-codes -->
|
|
181
|
+
Warnings:
|
|
182
|
+
|
|
183
|
+
- `project-left-out`: A direct subdirectory with a manifest or lockfile is left out of projects, because depcurfew apply would refuse it (it has a depcurfew.json of its own, or it is a separate git repository).
|
|
184
|
+
|
|
185
|
+
Notes:
|
|
186
|
+
|
|
187
|
+
- `projects-listed`: The repository root has no manifest or lockfile, but direct subdirectories do, so init lists them under projects in depcurfew.json.
|
|
188
|
+
- `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
|
+
- `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
|
+
- `tool-only-in-ci`: A tool appears only in a CI setup step, so depcurfew apply does not configure it under targets "auto".
|
|
191
|
+
<!-- END GENERATED: init-codes -->
|
|
192
|
+
|
|
129
193
|
### apply warnings and errors
|
|
130
194
|
|
|
131
|
-
Each warning and error of `apply` has a code. The text output shows it in parentheses after the message, with what to do on the next line, like a doctor finding. `depcurfew apply --json` (also with `--dry-run`) prints one JSON document and nothing on stderr: `changes` (each file with its action and unified diff), `warnings` and `errors` (each with `code`, `level`, `message` and `fix`; `tool` when it concerns one tool; `location` when it concerns one file, with `file` and, when known, `line`; for an invalid policy also `issues`), `snippets` to add by hand, `notices` (such as
|
|
195
|
+
Each warning and error of `apply` has a code. The text output shows it in parentheses after the message, with what to do on the next line, like a doctor finding. `depcurfew apply --json` (also with `--dry-run`) prints one JSON document and nothing on stderr: `changes` (each file with its action and unified diff), `warnings` and `errors` (each with `code`, `level`, `message` and `fix`; `tool` when it concerns one tool; `location` when it concerns one file, with `file` and, when known, `line`; for an invalid policy also `issues`), `snippets` to add by hand, `notices` (such as `policy-from-parent`) and `written`. A run that writes nothing because of an error lists no changes and exits with 1. Diffs, on the terminal, in a pipe and in `changes`, show the values of secrets as `<redacted>` (`_authToken`, `_auth`, `_password` and the client key `key` in `.npmrc`, keys ending in `token` or `password`, the user and password of a URL), so that a diff can be shared or logged; the files keep them. The text diff also shows control and bidirectional characters from the files as text (`\x1b`, ``), so that a value cannot drive the terminal or a CI log; `changes` keeps them, escaped by JSON.
|
|
132
196
|
|
|
133
197
|
<!-- BEGIN GENERATED: apply-codes -->
|
|
134
198
|
Warnings:
|
|
135
199
|
|
|
136
200
|
- `nothing-to-configure`: depcurfew found no supported package manager or bot, so nothing is configured.
|
|
201
|
+
- `environment-overrides`: apply --user: an environment variable makes a tool read another user file or none, or overrides a value apply writes there.
|
|
202
|
+
- `depcurfew-outdated`: The project installs a newer depcurfew than the one running, which may not know settings that version writes.
|
|
137
203
|
- `file-unreadable`: A file that shows which tools a project uses cannot be read or parsed (too large, a symlink out of the repository, no read permission, invalid), so depcurfew may configure fewer tools than the project uses.
|
|
138
|
-
- `tool-not-supported`: A detected tool is not supported (Yarn Classic), so nothing is written for it.
|
|
204
|
+
- `tool-not-supported`: A detected or installed tool is not supported (Yarn Classic), so nothing is written for it.
|
|
205
|
+
- `target-not-found`: With projects, a package manager listed under targets is configured in each directory (the root or a project) that uses it, and none uses it, so nothing is written for it.
|
|
139
206
|
- `tool-only-in-ci`: A tool appears only in a CI setup step, with no manifest, lockfile or configuration in the repository, so targets "auto" does not configure it.
|
|
140
207
|
- `version-not-pinned`: No exact version of a package manager is pinned, so depcurfew configures for the latest version it knows.
|
|
141
|
-
- `version-too-old`: The pinned version of a tool lacks a setting the policy needs, so that setting is not written.
|
|
208
|
+
- `version-too-old`: The pinned version of a tool (with apply --user, the installed one) lacks a setting the policy needs, so that setting is not written.
|
|
209
|
+
- `version-unknown`: apply --user: the installed version of a tool cannot be read from its files, and a value it does not understand would stop every command, so nothing is written for it.
|
|
210
|
+
- `no-user-config`: apply --user: the tool has no user-level configuration file, so nothing is written for it.
|
|
211
|
+
- `user-setting-project-only`: apply --user: the tool reads a part of the policy only from project files, so it is not written into the user file.
|
|
212
|
+
- `user-directory-relative`: apply --user: the environment variable that names the directory of the user file (XDG_CONFIG_HOME, APPDATA) is a relative path, so the tool looks in another place from each directory, or where it looks then is not known; nothing is written for it.
|
|
213
|
+
- `user-file-uncertain`: apply --user: a library the tool calls decides where its user file is (platformdirs for Poetry and PDM on macOS), and its version cannot be read from files, so apply --user writes where the tool's docs say and names the other files the tool may read.
|
|
214
|
+
- `user-setting-overridden`: apply --user: a file the tool reads ahead of its user file (~/.npmrc for pnpm 10) sets a key apply writes there to another value, so the value written has no effect.
|
|
215
|
+
- `version-not-used`: apply --user: --for gives the version of a tool that apply --user does not configure (not in PATH with targets "auto", or not listed under targets).
|
|
142
216
|
- `cooldown-not-written`: The release-age delay cannot be written for a tool.
|
|
143
217
|
- `cooldown-shortened`: A release-age delay set outside depcurfew's markers (in JSON files also one an earlier apply wrote) is longer than the policy, and apply replaces it with the shorter one.
|
|
144
218
|
- `cooldown-environment-only`: pip reads no project configuration, so the release-age delay has to be set in the environment where pip runs.
|
|
145
219
|
- `exclude-pattern-unsupported`: A cooldown exception uses a wildcard the tool does not support, so it is not written.
|
|
146
220
|
- `exclude-unsupported`: The tool has no setting for cooldown exceptions, so the release-age delay applies to every package.
|
|
147
221
|
- `allow-pattern-unsupported`: An installScripts.allow entry is a pattern, but the tool takes package names only, so it is not written.
|
|
222
|
+
- `deny-pattern-unsupported`: An installScripts.deny entry is a pattern, but the tool records denials by package name only, so it is not written.
|
|
148
223
|
- `allow-all-unsupported`: The tool has no setting that allows every install script, so installScripts "allow" is not written.
|
|
149
|
-
- `allow-all-replaced`: A setting that allowed every install script was
|
|
150
|
-
- `allow-list-entry-outside-policy`: An install-script allow-list in the repository lets a package run scripts the policy does not allow; apply does not remove it.
|
|
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.
|
|
225
|
+
- `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
|
+
- `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.
|
|
227
|
+
- `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.
|
|
151
228
|
- `built-in-list-replaced`: Bun's built-in list of trusted packages is replaced by the policy's allow-list.
|
|
152
|
-
- `scripts-not-enforceable`: The pinned npm or pnpm runs every install script and has no allow-list, so "deny" cannot be enforced.
|
|
153
|
-
- `
|
|
154
|
-
- `
|
|
229
|
+
- `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.
|
|
230
|
+
- `strict-allow-scripts-for-npm-in-use`: An npm the repository runs (the pinned one, one a CI job or Dockerfile installs, or one a declared Node.js comes with, for a project also through the root version files and root Dockerfiles that build it) is 11.16 to 11.x and runs install scripts the allow-list does not cover unless strict-allow-scripts is set, so apply writes strict-allow-scripts=true, which makes npm 12 stop the install at such a script; not given when that npm is the pinned one and no place runs npm 12.
|
|
231
|
+
- `before-overrides-cooldown`: before= in .npmrc takes the place of the release-age delay apply writes, or makes npm stop with an error next to it (apply --user then does not write the delay).
|
|
232
|
+
- `exact-version-gap`: The pinned (with apply --user, the installed) pnpm skips the release-age delay for packages requested by exact version.
|
|
155
233
|
- `source-not-enforceable`: The tool has no setting for a dependency source the policy denies; doctor checks the lockfile instead.
|
|
156
234
|
- `source-switch-indirect-only`: The pnpm setting for git and tarball sources stops them only for indirect dependencies.
|
|
157
235
|
- `source-switch-combined`: One pnpm setting covers git and tarball sources, so denying one denies both.
|
|
@@ -166,6 +244,7 @@ Warnings:
|
|
|
166
244
|
- `bot-config-missing`: A bot has no configuration file, and depcurfew does not create one.
|
|
167
245
|
- `bot-config-not-edited`: A bot configuration file is in a place or format depcurfew does not edit; a snippet shows what to add.
|
|
168
246
|
- `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.
|
|
169
248
|
- `security-delay-unsupported`: Dependabot cannot delay security updates, so securityUpdates "delay" is not applied.
|
|
170
249
|
|
|
171
250
|
Errors (nothing is written):
|
|
@@ -178,8 +257,97 @@ Errors (nothing is written):
|
|
|
178
257
|
- `policy-exists`: depcurfew init found a depcurfew.json and writes it only with --force.
|
|
179
258
|
- `directory-not-found`: The directory given on the command line does not exist or cannot be opened.
|
|
180
259
|
- `not-a-directory`: The path given on the command line is a file, not a directory.
|
|
260
|
+
- `usage`: The command line is not valid: an unknown command, option or hook, a missing or extra argument, an option without its value or with one it does not take (such as a --registry that is not an http:// or https:// URL), or options that cannot go together (exit code 2); with --json the document holds only schemaVersion and this error.
|
|
261
|
+
- `project-not-found`: A directory listed in projects in depcurfew.json does not exist, so nothing is written.
|
|
262
|
+
- `project-not-a-directory`: A path listed in projects in depcurfew.json is a file, not a directory, so nothing is written.
|
|
263
|
+
- `project-symlink`: A directory listed in projects in depcurfew.json is a symlink or lies under one, which may lead out of the repository, so nothing is written.
|
|
264
|
+
- `project-has-policy`: A directory listed in projects has a depcurfew.json of its own, a second policy for the same files, so nothing is written.
|
|
265
|
+
- `project-is-repository`: A directory listed in projects is a separate git repository or lies in one (it has a .git of its own, as a git submodule does), whose files belong to that repository, so nothing is written.
|
|
266
|
+
- `project-in-workspace`: A directory listed in projects is, holds or lies in a workspace package of the repository root, which the root configuration already covers, so nothing is written.
|
|
267
|
+
- `home-missing`: apply --user and init --user: the home directory (HOME, USERPROFILE on Windows) does not exist or is not an absolute path, so nothing is written.
|
|
181
268
|
<!-- END GENERATED: apply-codes -->
|
|
182
269
|
|
|
270
|
+
## Protecting the whole machine
|
|
271
|
+
|
|
272
|
+
`depcurfew apply` protects one project. The user-level configuration of each package manager protects every project on the machine that does not set the same settings itself, and also what runs outside a project: `npx`, `uvx`, global installs, and a new project before anyone ran `apply` in it.
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
depcurfew init --user
|
|
276
|
+
depcurfew apply --user --dry-run
|
|
277
|
+
depcurfew apply --user
|
|
278
|
+
depcurfew doctor --user
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
- The user policy is `~/.config/depcurfew/depcurfew.json` (in `$XDG_CONFIG_HOME/depcurfew/` when that variable is set to an absolute path, in `%APPDATA%\depcurfew\` on Windows). It has the same format as a project policy; under `targets: "auto"` it configures the package managers found in `PATH`.
|
|
282
|
+
- depcurfew does not run other programs to learn their versions. It reads the version from the files next to the executable in `PATH`: the `package.json` of a tool installed from npm (also Yarn installed as `@yarnpkg/cli-dist`), the `.dist-info` folder of one installed from PyPI. A Yarn or pnpm shim of Corepack runs, outside a project, the version Corepack's `lastKnownGood.json` names (in `$COREPACK_HOME`, else `$XDG_CACHE_HOME/node/corepack`, else `~/.cache/node/corepack`; `%LOCALAPPDATA%\node\corepack` on Windows), and a `yarnPath` in `~/.yarnrc.yml` that points to a release named `yarn-<version>.cjs`, as `yarn set version` names it, gives Yarn's version; `doctor --user` says where each version came from. When it cannot, it configures for the latest version it knows and says so, except for tools that stop every command on a value their version does not understand (the last column below): for those it writes nothing until you pass the version, for example `--for uv@0.13.6`. Bun from its official installer (`~/.bun/bin/bun`, which writes no version file) or from Homebrew has no version depcurfew can read, so it needs `--for bun@<version>`.
|
|
283
|
+
- `init --user` keeps a longer release-age delay that the user files of the installed tools already set, as `init` does in a project: a delay of 7 days in `~/.npmrc` makes `cooldown.minimum` `7d`, and `init --user` says which setting, value, file and line it took. Deno reads the delay in `~/.npmrc` in whole days and ignores any other value, so `apply --user` writes it for Deno in days, rounded up.
|
|
284
|
+
- Poetry and PDM ask the library platformdirs where their user directory is, and on macOS the answer depends on the platformdirs version in the tool's environment (the rows below). depcurfew reads that version from the `platformdirs-<version>.dist-info` folder next to the tool's own; when it cannot and `XDG_CONFIG_HOME` or `XDG_DATA_HOME` would move the file, `apply --user` writes where the docs say (`~/Library/Application Support/...`) and warns with the other file (`user-file-uncertain`), and `doctor --user` checks both.
|
|
285
|
+
- `apply --user` writes the files of all tools or none, as `apply` does in a project: a user file it cannot read or edit (not UTF-8, a symlink, larger than 1 MiB, a file where its directory belongs) is an error that names it, such as `~/.config/pip/pip.conf`, with a fix for that problem, and a file that cannot be written puts back the ones already replaced. `init --user` and `apply --user` do not create a missing home directory, and when `XDG_CONFIG_HOME` or `%APPDATA%` is a relative path, `apply --user` writes nothing for the tools that may use it, with a warning (pip then reads `pip.conf` relative to the directory each command runs in). Run as root (`sudo` with `HOME` kept), they leave each file they replace to its owner, and give what they create in the home directory to the owner of the home directory.
|
|
286
|
+
- A key `apply --user` writes into a user file can be overridden by a file the tool reads ahead of it: pnpm 10 reads `~/.npmrc` before its `rc` file. `apply --user` warns about a different value there (`user-setting-overridden`) with the line, and `doctor --user` reports it.
|
|
287
|
+
- A tool's own project settings win over its user file for the same key. Parts of the policy that a tool reads only from project files, such as per-package allow-lists, are not written into the user file; `apply --user` names them, and `depcurfew apply` in the project writes them.
|
|
288
|
+
- A user file is edited as `apply` edits a project file: a setting the policy manages that is set by hand elsewhere in the file is commented out and comes back when the policy stops managing it (YAML files get the policy's value instead). That includes an exception list the policy leaves empty, the command sections of `pip.conf` such as `[install]`, and the `[pip]` table of `uv.toml`. Install-script allow-list entries that the policy does not allow are kept and reported; a longer delay that the policy shortens is reported.
|
|
289
|
+
- `doctor --user` reports a user file that does not match the user policy, values in user files that weaken every project, and environment variables that make a tool read another file, no file or other values, such as a relative `XDG_CONFIG_HOME`. It reads values as each tool does: a switch that allows every install script is on for every value the tool does not read as off (`dangerously-allow-all-builds=1` in `~/.npmrc` for pnpm 10), and a merge key (`<<: *defaults`) counts in pnpm's `config.yaml`. A missing home directory, or a user file it cannot read, is an error rather than a clean report, and an installed tool too old for settings the user policy needs (npm 10 has no release-age delay and no install-script allow-list) is a warning that says what stays unprotected and which version to update to.
|
|
290
|
+
- Tools that record the release-age delay in their lockfile may lock differently on a machine whose user file sets one than on a machine without it; set the same delay in the project with `depcurfew apply` so that every machine and CI agree.
|
|
291
|
+
|
|
292
|
+
<!-- BEGIN GENERATED: user-config -->
|
|
293
|
+
| Tool | User file | Settings `apply --user` writes (versions that read them there) | Needs the exact version |
|
|
294
|
+
| --- | --- | --- | --- |
|
|
295
|
+
| npm | Linux, macOS: `~/.npmrc`<br>Windows: `%HOME%\.npmrc` (when it is an absolute path), else `~\.npmrc` | `min-release-age` (11.10.0+), `min-release-age-exclude` (11.17.0+), `strict-allow-scripts` (11.16.0+), `allow-scripts` (11.16.0+), `allow-git` (11.10.0+), `allow-remote` (11.15.0+), `allow-file` (11.15.0+), `allow-directory` (11.15.0+) | no |
|
|
296
|
+
| pnpm | Linux: `$XDG_CONFIG_HOME/pnpm/rc`, else `~/.config/pnpm/rc`<br>macOS: `$XDG_CONFIG_HOME/pnpm/rc`, else `~/Library/Preferences/pnpm/rc`<br>Windows: `%XDG_CONFIG_HOME%\pnpm\rc`, else `%LOCALAPPDATA%\pnpm\config\rc`, else `~\.config\pnpm\rc`<br>11.0.0+: Linux: `$XDG_CONFIG_HOME/pnpm/config.yaml`, else `~/.config/pnpm/config.yaml`<br>macOS: `$XDG_CONFIG_HOME/pnpm/config.yaml`, else `~/Library/Preferences/pnpm/config.yaml`<br>Windows: `%XDG_CONFIG_HOME%\pnpm\config.yaml`, else `%LOCALAPPDATA%\pnpm\config\config.yaml`, else `~\.config\pnpm\config.yaml` | `minimum-release-age` (10.16.0–11.0.0), `minimum-release-age-exclude` (10.16.0–11.0.0), `only-built-dependencies` (10.0.0–11.0.0), `strict-dep-builds` (10.3.0–11.0.0), `block-exotic-subdeps` (10.26.0–11.0.0), `minimumReleaseAge` (11.0.0+), `minimumReleaseAgeExclude` (11.0.0+), `minimumReleaseAgeStrict` (11.0.0+), `strictDepBuilds` (11.0.7+), `blockExoticSubdeps` (11.0.0–12.0.0) | yes |
|
|
297
|
+
| Yarn (Berry) | Linux, macOS, Windows: `~/.yarnrc.yml` | `npmMinimalAgeGate` (4.10.2+), `npmPreapprovedPackages` (4.10.0+), `enableScripts` (4.0.0+), `approvedGitRepositories` (4.14.0+) | yes |
|
|
298
|
+
| Bun | Linux, macOS, Windows: `$XDG_CONFIG_HOME/.bunfig.toml` (when it is an absolute path), else `~/.bunfig.toml` | `install.minimumReleaseAge` (1.3.0+), `install.minimumReleaseAgeExcludes` (1.3.0+) | yes |
|
|
299
|
+
| Deno | 1.45.0+: Linux, macOS, Windows: `~/.npmrc` | `min-release-age` (2.8.0+) | no |
|
|
300
|
+
| uv | Linux, macOS: `$XDG_CONFIG_HOME/uv/uv.toml` (when it is an absolute path), else `~/.config/uv/uv.toml`<br>Windows: `%APPDATA%\uv\uv.toml`, else `~\AppData\Roaming\uv\uv.toml` | `exclude-newer` (0.9.17+), `exclude-newer-package` (0.9.25+), `no-build` (0.2.12+), `no-build-package` (0.2.12+), `no-binary-package` (0.2.12+) | yes |
|
|
301
|
+
| pip | Linux: `$XDG_CONFIG_HOME/pip/pip.conf`, else `~/.config/pip/pip.conf`<br>macOS: `~/Library/Application Support/pip/pip.conf` (when the folder exists), else `~/.config/pip/pip.conf`<br>Windows: `%APPDATA%\pip\pip.ini`, else `~\AppData\Roaming\pip\pip.ini`<br>26.2.0+: Linux: `$XDG_CONFIG_HOME/pip/pip.conf`, else `~/.config/pip/pip.conf`<br>macOS: `$XDG_DATA_HOME/pip/pip.conf` (when the folder exists), else `~/Library/Application Support/pip/pip.conf` (when the folder exists and $XDG_DATA_HOME is not set), else `~/.config/pip/pip.conf`<br>Windows: `%APPDATA%\pip\pip.ini`, else `~\AppData\Roaming\pip\pip.ini` | `uploaded-prior-to` (26.1.0+), `only-binary` (7.1.0+), `no-binary` (7.1.0+) | yes |
|
|
302
|
+
| Poetry | 2.0.0+: Linux: `$XDG_CONFIG_HOME/pypoetry/config.toml`, else `~/.config/pypoetry/config.toml`<br>macOS: `~/Library/Application Support/pypoetry/config.toml`<br>Windows: `%APPDATA%\pypoetry\config.toml`, else `~\AppData\Roaming\pypoetry\config.toml`<br>macOS with platformdirs before 4.6.0: `~/Library/Application Support/pypoetry/config.toml`<br>macOS with platformdirs 4.6.0–4.9.6: `$XDG_CONFIG_HOME/pypoetry/config.toml`, else `$XDG_DATA_HOME/pypoetry/config.toml`, else `~/Library/Application Support/pypoetry/config.toml`<br>macOS with platformdirs 4.9.6–4.11.8: `$XDG_CONFIG_HOME/pypoetry/config.toml`, else `~/Library/Application Support/pypoetry/config.toml`<br>macOS with platformdirs 4.11.8+: `$XDG_CONFIG_HOME/pypoetry/config.toml` (when it is an absolute path), else `~/Library/Application Support/pypoetry/config.toml` | `solver.min-release-age` (2.4.0+), `solver.min-release-age-exclude` (2.4.0+), `installer.only-binary` (2.0.0+), `installer.no-binary` (1.2.0+) | no |
|
|
303
|
+
| PDM | Linux: `$XDG_CONFIG_HOME/pdm/config.toml`, else `~/.config/pdm/config.toml`<br>macOS: `~/Library/Application Support/pdm/config.toml`<br>Windows: `~\AppData\Local\pdm\pdm\config.toml`<br>macOS with platformdirs before 4.6.0: `~/Library/Application Support/pdm/config.toml`<br>macOS with platformdirs 4.6.0–4.9.6: `$XDG_CONFIG_HOME/pdm/config.toml`, else `$XDG_DATA_HOME/pdm/config.toml`, else `~/Library/Application Support/pdm/config.toml`<br>macOS with platformdirs 4.9.6–4.11.8: `$XDG_CONFIG_HOME/pdm/config.toml`, else `~/Library/Application Support/pdm/config.toml`<br>macOS with platformdirs 4.11.8+: `$XDG_CONFIG_HOME/pdm/config.toml` (when it is an absolute path), else `~/Library/Application Support/pdm/config.toml` | `strategy.exclude-newer` (2.27.0+) | yes |
|
|
304
|
+
| Pipenv | none | none | no |
|
|
305
|
+
<!-- END GENERATED: user-config -->
|
|
306
|
+
|
|
307
|
+
### doctor --user rules
|
|
308
|
+
|
|
309
|
+
<!-- BEGIN GENERATED: user-rules -->
|
|
310
|
+
### `user-policy-invalid`
|
|
311
|
+
|
|
312
|
+
**error** · The user policy does not follow the schema or cannot be read.
|
|
313
|
+
|
|
314
|
+
Without a valid user policy (~/.config/depcurfew/depcurfew.json), doctor --user cannot compare the user-level configuration with it, so only the checks that need no user policy run, as when there is none.
|
|
315
|
+
|
|
316
|
+
Fix: Fix what the message lists, or write a new user policy with depcurfew init --user --force.
|
|
317
|
+
|
|
318
|
+
### `user-config-unreadable`
|
|
319
|
+
|
|
320
|
+
**error** · A user-level configuration file cannot be read, parsed or edited, or the home directory does not exist, so depcurfew cannot check it.
|
|
321
|
+
|
|
322
|
+
The package manager may still read the file (or stop on it), and its settings may weaken every project on the machine; doctor --user fails closed and reports an error instead of passing silently. Without a home directory the user files of most tools cannot be found at all.
|
|
323
|
+
|
|
324
|
+
Fix: Do what the fix of the finding says: fix invalid syntax, give read permission, replace a symlink apply --user does not write through, make a file smaller than 1 MiB, or set HOME to an existing directory.
|
|
325
|
+
|
|
326
|
+
### `user-config-drift`
|
|
327
|
+
|
|
328
|
+
**warning** · needs depcurfew.json · A user-level configuration file does not match the user policy.
|
|
329
|
+
|
|
330
|
+
depcurfew apply --user writes the user policy (~/.config/depcurfew/depcurfew.json) into the user-level configuration of each installed tool, which protects every project on the machine that does not set the same settings itself, and npx, uvx and global installs. A file that no longer matches leaves that protection weaker than the policy.
|
|
331
|
+
|
|
332
|
+
Fix: Run depcurfew apply --user.
|
|
333
|
+
|
|
334
|
+
### `user-config-weakens`
|
|
335
|
+
|
|
336
|
+
**warning** · A user-level configuration file has a value that weakens every project that does not set it itself.
|
|
337
|
+
|
|
338
|
+
Settings in the user-level configuration apply to every project that does not set the same key, and some apply even then (lists that tools merge across files). A value that turns the release-age delay off, exempts packages or allows install scripts or git dependencies there weakens all of them at once, and so does a file the tool reads ahead of the user file.
|
|
339
|
+
|
|
340
|
+
Fix: Remove the value from the file, or set it in the policy and run depcurfew apply --user.
|
|
341
|
+
|
|
342
|
+
### `user-environment-overrides`
|
|
343
|
+
|
|
344
|
+
**warning** · An environment variable moves, skips or overrides the user-level configuration or the policy.
|
|
345
|
+
|
|
346
|
+
Package managers read settings from environment variables before their configuration files, and some variables make them read another file or none. A variable set in a shell profile or a CI runner can switch the protection off without any file showing it.
|
|
347
|
+
|
|
348
|
+
Fix: Unset the variable, or set it to a value the policy allows.
|
|
349
|
+
<!-- END GENERATED: user-rules -->
|
|
350
|
+
|
|
183
351
|
## Supported tools
|
|
184
352
|
|
|
185
353
|
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.
|
|
@@ -187,40 +355,42 @@ What depcurfew configures and checks for each tool, from its [knowledge base](ht
|
|
|
187
355
|
<!-- BEGIN GENERATED: tools -->
|
|
188
356
|
| Tool | Release-age delay | Exceptions | Install-script allow-list (Python: packages built from source) | Dependency sources | Verified |
|
|
189
357
|
| --- | --- | --- | --- | --- | --- |
|
|
190
|
-
| 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-
|
|
191
|
-
| 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-
|
|
192
|
-
| 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-
|
|
193
|
-
| 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-
|
|
194
|
-
| 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-
|
|
195
|
-
| 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 |
|
|
196
|
-
| 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-
|
|
197
|
-
| 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 |
|
|
198
|
-
| 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-
|
|
199
|
-
| Pipenv | `cool-down-period` in `Pipfile` (duration string), since 2026.6.2 | none | none | none | 2026-10-30 |
|
|
200
|
-
| 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-
|
|
201
|
-
| 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-
|
|
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 |
|
|
202
370
|
|
|
203
371
|
⚠️ 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).
|
|
204
372
|
<!-- END GENERATED: tools -->
|
|
205
373
|
|
|
206
374
|
Yarn Classic (1.x) is detected but not supported.
|
|
207
375
|
|
|
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.
|
|
377
|
+
|
|
208
378
|
### Python projects
|
|
209
379
|
|
|
210
380
|
depcurfew configures uv, Poetry, PDM, Pipenv and pip from the same `depcurfew.json`, and the bots for Python packages: Dependabot `pip` and `uv` entries and Renovate.
|
|
211
381
|
|
|
212
382
|
- **Detection.** A Python tool counts as used when its lockfile is there, when `pyproject.toml` has its table, or when its own configuration file or a CI setup step shows it; `requirements.txt` means pip. `depcurfew explain` lists what it found.
|
|
213
|
-
- **Package names.** Python packages on the policy's package lists (the cooldown exceptions and `installScripts.
|
|
214
|
-
- **Install scripts are builds.** A wheel installs without running package code; a source distribution, a git repository or a local directory is built, which runs its build code. `installScripts.default: "deny"` forbids building, and the packages in `installScripts.allow` are the exceptions. None of the three tools can only allow a build, so these packages are always built from source, even where a wheel exists; `apply` says so, and `doctor` lists the packages in the lockfile that would be built.
|
|
383
|
+
- **Package names.** Python packages on the policy's package lists (the cooldown exceptions, `installScripts.allow` and `installScripts.deny`) take the prefix `pypi:`, for example `"pypi:docopt"`. Names without a prefix stay npm names, so an exception for an npm package never applies to a Python package of the same name.
|
|
384
|
+
- **Install scripts are builds.** A wheel installs without running package code; a source distribution, a git repository or a local directory is built, which runs its build code. `installScripts.default: "deny"` forbids building, and the packages in `installScripts.allow` are the exceptions. None of the three tools can only allow a build, so these packages are always built from source, even where a wheel exists; `apply` says so, and `doctor` lists the packages in the lockfile that would be built. Packages in `installScripts.deny` must not be built: `doctor` reports one that a tool would still build (Poetry, PDM, pip and Pipenv build git, local and URL dependencies whatever their settings say), and one without wheels that cannot be installed while building is blocked.
|
|
215
385
|
- **pip reads no project configuration.** `apply` writes the build options into `requirements.txt`. The release-age delay cannot live in the project, so `apply` prints the environment variable to set where pip runs. `doctor` also checks the files a requirements file includes with `-r` and `-c`, and reports an include it cannot read.
|
|
216
|
-
- **Lock again after `apply`.** `uv.lock` records the release-age delay, and PDM checks `pdm.lock` against all of its resolution settings, the delay and the build ban included. After `apply` changes them, `uv sync --locked` and `uv lock --check` fail until you run `uv lock`, and `pdm lock --check` and `pdm install --check` until you run `pdm lock`; `apply` says so (`lockfile-out-of-date`). `poetry.lock` and `Pipfile.lock` do not depend on the settings `apply` writes.
|
|
386
|
+
- **Lock again after `apply`.** `uv.lock` records the release-age delay, and PDM checks `pdm.lock` against all of its resolution settings, the delay and the build ban included. After `apply` changes them, `uv sync --locked` and `uv lock --check` fail until you run `uv lock`, and `pdm lock --check` and `pdm install --check` until you run `pdm lock`; `apply` says so (`lockfile-out-of-date`), and so does `doctor` (the rule `lockfile-out-of-date`) while the lockfile is not locked again: it compares what `uv.lock` records with the settings as uv does, and the hash in `pdm.lock` with the one PDM computes from `pyproject.toml`. `poetry.lock` and `Pipfile.lock` do not depend on the settings `apply` writes.
|
|
217
387
|
- **PDM and Pipenv cannot pin their own version.** `apply` configures for the latest version depcurfew knows and names that version (`version-not-pinned`). Pipenv takes the delay in whole days only (rounded up), has no exceptions and no project setting that forbids building; `apply` says what to set where Pipenv runs instead. `doctor` reads `pdm.lock` and `Pipfile.lock`, and reports settings that turn the delay or the build ban off without a word: Pipenv's pure-Python resolver and PDM's uv mode.
|
|
218
388
|
- **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.
|
|
219
389
|
- **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.
|
|
220
390
|
|
|
221
391
|
## doctor rules
|
|
222
392
|
|
|
223
|
-
Findings have a level: an **error** makes `depcurfew doctor` exit with 1, a **warning** or a **note** does
|
|
393
|
+
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`).
|
|
224
394
|
|
|
225
395
|
<!-- BEGIN GENERATED: rules -->
|
|
226
396
|
### `invalid-policy`
|
|
@@ -231,13 +401,37 @@ Without a valid policy, the rules that compare the repository with depcurfew.jso
|
|
|
231
401
|
|
|
232
402
|
Fix: Fix what the message lists; an editor that reads the $schema of depcurfew.json shows the problems as you type.
|
|
233
403
|
|
|
404
|
+
### `invalid-project`
|
|
405
|
+
|
|
406
|
+
**error** · needs depcurfew.json · A directory listed in projects in depcurfew.json does not exist, is not a directory, is a symlink, has a depcurfew.json of its own or overlaps a workspace package of the root.
|
|
407
|
+
|
|
408
|
+
depcurfew checks and configures each listed directory as a project of its own. A directory that is missing or misspelled is never checked, so its package managers keep whatever configuration they have; one with a depcurfew.json of its own would follow two policies, and a workspace package of the root is configured with the root already. depcurfew apply writes nothing while one of them is listed.
|
|
409
|
+
|
|
410
|
+
Fix: Correct or remove the entry in projects, or remove the depcurfew.json of the project.
|
|
411
|
+
|
|
412
|
+
### `depcurfew-outdated`
|
|
413
|
+
|
|
414
|
+
**warning** · The project installs a newer depcurfew than the one running.
|
|
415
|
+
|
|
416
|
+
A global install or an old cached copy may not know settings, rules or tool versions that the version pinned in the project knows, so it can miss problems or report fixed ones. The project copy is read from node_modules or package-lock.json, in the project and in the tools directory .claude/depcurfew that the README recommends for a repository without package.json in its root.
|
|
417
|
+
|
|
418
|
+
Fix: Run the project's copy of depcurfew (install the project's dependencies first when only package-lock.json pins it), or update the one you run.
|
|
419
|
+
|
|
234
420
|
### `manager-version-too-old`
|
|
235
421
|
|
|
236
|
-
**error** · A declared package manager version is older than a setting the repository relies on.
|
|
422
|
+
**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
|
+
|
|
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.
|
|
237
425
|
|
|
238
|
-
|
|
426
|
+
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.
|
|
239
427
|
|
|
240
|
-
|
|
428
|
+
### `runtime-end-of-life`
|
|
429
|
+
|
|
430
|
+
**warning** · A declared Node.js version is past its end of life.
|
|
431
|
+
|
|
432
|
+
After its end of life a Node.js release line gets no more releases, security fixes included. A version file, CI step or Dockerfile that only allows such lines keeps running an unpatched Node.js, and an older npm with it.
|
|
433
|
+
|
|
434
|
+
Fix: Move to a Node.js line that is still supported, in every place that declares the version.
|
|
241
435
|
|
|
242
436
|
### `setting-ignored-by-version`
|
|
243
437
|
|
|
@@ -251,7 +445,7 @@ Fix: Move the setting to the place and form this version reads.
|
|
|
251
445
|
|
|
252
446
|
**error** · An .npmrc file changes a security setting in a way the policy does not see, or is ignored.
|
|
253
447
|
|
|
254
|
-
Package managers read .npmrc from different places with different priorities. npm ignores the .npmrc of workspace packages and the keys below a [section] header, and a before date in .npmrc takes the place of min-release-age; pnpm 10 applies a root .npmrc value whenever pnpm-workspace.yaml does not set the same setting, which can weaken the policy.
|
|
448
|
+
Package managers read .npmrc from different places with different priorities. npm ignores the .npmrc of workspace packages and the keys below a [section] header, and a before date in .npmrc takes the place of min-release-age; pnpm 10 applies a root .npmrc value whenever pnpm-workspace.yaml does not set the same setting, which can weaken the policy, and dangerously-allow-all-builds there runs every build script while pnpm-workspace.yaml has no build settings of its own.
|
|
255
449
|
|
|
256
450
|
Fix: Keep security settings out of .npmrc files that are ignored or that override the policy; set them through depcurfew.json and depcurfew apply.
|
|
257
451
|
|
|
@@ -267,7 +461,7 @@ Fix: Remove security settings from package .npmrc files, or keep the default sha
|
|
|
267
461
|
|
|
268
462
|
**error** · A Python tool ignores a setting where it is written.
|
|
269
463
|
|
|
270
|
-
uv reads uv.toml instead of [tool.uv] when both exist, and in a workspace only the root; pip reads no configuration from the project and rejects options it does not know in requirements files; PDM in uv mode installs without only-binary; Pipenv with resolver_backend = "pure-python" locks without cool-down-period. A setting in such a place looks effective but is not, and an exclude-newer date freezes dependencies instead of delaying new versions.
|
|
464
|
+
uv reads uv.toml instead of [tool.uv] when both exist, and in a workspace only the root; pip reads no configuration from the project and rejects options it does not know in requirements files, so a CI job or a Dockerfile stage of the python image installs without the release-age delay unless it sets PIP_UPLOADED_PRIOR_TO or passes --uploaded-prior-to; PDM in uv mode installs without only-binary; Pipenv with resolver_backend = "pure-python" locks without cool-down-period. A setting in such a place looks effective but is not, and an exclude-newer date freezes dependencies instead of delaying new versions.
|
|
271
465
|
|
|
272
466
|
Fix: Move the setting to the file the tool reads.
|
|
273
467
|
|
|
@@ -275,7 +469,7 @@ Fix: Move the setting to the file the tool reads.
|
|
|
275
469
|
|
|
276
470
|
**error** · needs depcurfew.json · A configuration file does not match what depcurfew apply writes for depcurfew.json.
|
|
277
471
|
|
|
278
|
-
A generated setting was edited by hand, or depcurfew.json changed without running depcurfew apply, so the tools enforce something other than the policy.
|
|
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.
|
|
279
473
|
|
|
280
474
|
Fix: Run depcurfew apply (depcurfew apply --dry-run shows the difference), or change depcurfew.json if the edit was intended.
|
|
281
475
|
|
|
@@ -283,7 +477,7 @@ Fix: Run depcurfew apply (depcurfew apply --dry-run shows the difference), or ch
|
|
|
283
477
|
|
|
284
478
|
**error** · An update bot waits less than the package manager before proposing a new version.
|
|
285
479
|
|
|
286
|
-
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.
|
|
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.
|
|
287
481
|
|
|
288
482
|
Fix: Give the bot a delay at least as long as the package manager's (depcurfew apply does this).
|
|
289
483
|
|
|
@@ -307,9 +501,17 @@ Fix: Replace the dependency with a registry version, or allow that source in dep
|
|
|
307
501
|
|
|
308
502
|
**error** · needs depcurfew.json · The lockfile has packages with install scripts, or Python packages built from source, that depcurfew.json does not allow.
|
|
309
503
|
|
|
310
|
-
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, these packages execute code the policy does not allow; where it blocks them, they may not work without a build step. Only npm, Bun (1.4.4+) and Deno (lockfile v5) record install scripts in the lockfile; uv.lock, poetry.lock and pylock.toml show which Python packages have no wheel.
|
|
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.
|
|
505
|
+
|
|
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).
|
|
311
507
|
|
|
312
|
-
|
|
508
|
+
### `lockfile-out-of-date`
|
|
509
|
+
|
|
510
|
+
**warning** · A uv.lock or pdm.lock was locked with other settings than the project has now.
|
|
511
|
+
|
|
512
|
+
uv records the release-age delay (exclude-newer and exclude-newer-package) in uv.lock, and PDM checks pdm.lock against a hash of the dependencies, the sources and the [tool.pdm.resolution] table, the delay and the build ban included. After a change, by depcurfew apply or by hand, uv sync --locked, uv lock --check, pdm lock --check and pdm install --check fail until the lockfile is locked again, and the lockfile still holds what was resolved under the old settings.
|
|
513
|
+
|
|
514
|
+
Fix: Lock again (uv lock or pdm lock) and commit the lockfile.
|
|
313
515
|
|
|
314
516
|
### `unreadable-config`
|
|
315
517
|
|
|
@@ -322,9 +524,9 @@ Fix: Fix what the message describes: invalid syntax, missing read permission, a
|
|
|
322
524
|
|
|
323
525
|
## GitHub Action
|
|
324
526
|
|
|
325
|
-
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. It runs from its own checkout, with nothing to install, and sets up Node.js 22 only when the runner has an older version.
|
|
527
|
+
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.
|
|
326
528
|
|
|
327
|
-
Pin it to a full commit SHA, like every action in your workflows. If the repository cannot use code scanning, set `upload-sarif: false`; the findings stay in the job log.
|
|
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.
|
|
328
530
|
|
|
329
531
|
<!-- BEGIN GENERATED: action-workflow -->
|
|
330
532
|
```yaml
|
|
@@ -340,6 +542,8 @@ jobs:
|
|
|
340
542
|
timeout-minutes: 15
|
|
341
543
|
permissions:
|
|
342
544
|
security-events: write
|
|
545
|
+
# The depcurfew doctor check with the result in the Checks tab:
|
|
546
|
+
checks: write
|
|
343
547
|
# Only needed in private repositories:
|
|
344
548
|
actions: read
|
|
345
549
|
contents: read
|
|
@@ -347,7 +551,7 @@ jobs:
|
|
|
347
551
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
348
552
|
with:
|
|
349
553
|
persist-credentials: false
|
|
350
|
-
- uses: DevXord/depcurfew
|
|
554
|
+
- uses: DevXord/depcurfew@25fb6165f2d19767eb4d35c92606500e104b09f9 # v1.2.0
|
|
351
555
|
```
|
|
352
556
|
<!-- END GENERATED: action-workflow -->
|
|
353
557
|
|
|
@@ -358,9 +562,12 @@ jobs:
|
|
|
358
562
|
| `sarif-file` | `depcurfew.sarif` | Where to write the SARIF report, relative to the workspace; a path outside it works too. |
|
|
359
563
|
| `upload-sarif` | `true` | Upload the SARIF report to GitHub code scanning. The job needs the security-events write permission. |
|
|
360
564
|
| `category` | `depcurfew` | Code scanning category of the upload, to tell several depcurfew runs in one workflow apart. |
|
|
361
|
-
| `fail-on
|
|
362
|
-
| `
|
|
363
|
-
| `
|
|
565
|
+
| `fail-on` | empty | Which findings fail the step and the depcurfew doctor check. Empty (the default) follows doctor.failOn in depcurfew.json, which depcurfew init sets to warning, and fails on errors only without it; error or warning overrides the policy, and warning makes warnings fail them too, as depcurfew doctor --fail-on warning does. Notes never do. Any other value fails the step. |
|
|
566
|
+
| `fail-on-error` | `true` | Deprecated, use fail-on. false keeps the step from failing on findings at all, whatever fail-on says, and makes the depcurfew doctor check neutral instead of failed. |
|
|
567
|
+
| `annotations` | `true` | Show the findings as annotations on the files, each with how to fix it, or one notice when there are none, which lists every check and its result. The notes and warnings of the action about the run itself (a directory checked with the root that lists it under projects, a depcurfew doctor check that could not be added) are annotations too, of the same level as in the log. GitHub shows a limited number per step; the log lists every finding. Turn off when code scanning already shows them. |
|
|
568
|
+
| `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
|
+
| `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
|
+
| `github-token` | `${{ github.token }}` | Token for the depcurfew doctor check; the workflow token by default. |
|
|
364
571
|
|
|
365
572
|
| Output | Description |
|
|
366
573
|
| --- | --- |
|
|
@@ -372,7 +579,7 @@ jobs:
|
|
|
372
579
|
|
|
373
580
|
### Other CI systems
|
|
374
581
|
|
|
375
|
-
The action works only on GitHub. On GitLab CI or any other CI, install the project's dependencies without running their install scripts, so nothing from the lockfile runs before doctor has checked it, then run the depcurfew version the lockfile pins; the job fails when doctor finds an error (exit code 1):
|
|
582
|
+
The action works only on GitHub. On GitLab CI or any other CI, install the project's dependencies without running their install scripts, so nothing from the lockfile runs before doctor has checked it, then run the depcurfew version the lockfile pins; the job fails when doctor finds an error, or a warning when `doctor.failOn` is `warning` (exit code 1):
|
|
376
583
|
|
|
377
584
|
```yaml
|
|
378
585
|
depcurfew:
|
|
@@ -386,6 +593,10 @@ depcurfew:
|
|
|
386
593
|
|
|
387
594
|
`depcurfew doctor --json` and `--sarif` write the findings for other tools; set `FORCE_COLOR=1` if the CI log shows colors.
|
|
388
595
|
|
|
596
|
+
## What depcurfew does not do
|
|
597
|
+
|
|
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.
|
|
599
|
+
|
|
389
600
|
## Guarding installs
|
|
390
601
|
|
|
391
602
|
The configuration `apply` writes is what really protects a project: the package managers themselves wait out the release-age delay and refuse install scripts and sources the policy denies. `depcurfew check` adds a check before something is installed, which matters most for AI coding agents that add packages on their own:
|
|
@@ -399,26 +610,36 @@ For each package it looks up the npm registry (the one `--registry` or `registry
|
|
|
399
610
|
|
|
400
611
|
- a name the registry does not know, which is how made-up package names show up;
|
|
401
612
|
- a version younger than the cooldown in `depcurfew.json`, with the newest version that is old enough;
|
|
402
|
-
- 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;
|
|
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;
|
|
403
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;
|
|
404
615
|
- a deprecated version (a warning);
|
|
405
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.
|
|
406
617
|
|
|
407
|
-
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. For a name without a version or with a range, npm prefers the dist-tag `tag=` in `.npmrc` names, and so does `check`.
|
|
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`.
|
|
619
|
+
|
|
620
|
+
`.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.
|
|
408
621
|
|
|
409
622
|
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.
|
|
410
623
|
|
|
411
|
-
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`, and `errors` for a `depcurfew.json` that cannot be used, in which case no package is checked.
|
|
624
|
+
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.
|
|
412
625
|
|
|
413
626
|
### Claude Code
|
|
414
627
|
|
|
415
628
|
`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:
|
|
416
629
|
|
|
417
630
|
- **deny** when a package breaks the policy (Claude sees the reason and can pick another package or version);
|
|
418
|
-
- **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`);
|
|
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`);
|
|
419
632
|
- nothing when everything checks out, so the command goes through Claude Code's usual permission prompts.
|
|
420
633
|
|
|
421
|
-
|
|
634
|
+
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
|
+
|
|
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.
|
|
637
|
+
|
|
638
|
+
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
|
+
|
|
640
|
+
The hook takes its policy from the project Claude Code works on (`CLAUDE_PROJECT_DIR`), wherever Claude's working directory is inside it: when the project directory has a `depcurfew.json` (or one above it up to the root of the repository), that policy applies, and a different `depcurfew.json` nearer the working directory (in a nested repository, a vendored one or a package folder) is applied too, and the stricter answer counts, so it can tighten the check but never relax it; a decision says so. This holds whether or not the project is in a git repository. When the project directory has no `depcurfew.json`, the nearest one of the working directory applies alone, as `check --dir` finds it, so `frontend/depcurfew.json` decides for `frontend`. Paths are compared by where they really lead, through symlinks.
|
|
641
|
+
|
|
642
|
+
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.
|
|
422
643
|
|
|
423
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.
|
|
424
645
|
|
|
@@ -456,6 +677,33 @@ The matcher `Bash|PowerShell|Monitor` covers every tool that runs shell commands
|
|
|
456
677
|
|
|
457
678
|
`onFailure: "block"` needs Claude Code 2.1.295 or later; without it, a hook that fails to start, crashes or times out lets the command through.
|
|
458
679
|
|
|
680
|
+
**Before depcurfew is installed.** The hook runs the copy in `node_modules`. When that file is missing (a fresh clone before `npm ci`, a repository without `package.json` in its root, a new cloud session), node still starts, cannot load it and exits with code 1 and a stack trace, and with `onFailure: "block"` that failure blocks every command of these tools, including the `npm ci` that would install depcurfew; without it, every command runs unchecked. The blocked call shows the hook command, `failed; blocking because onFailure is "block"` and node's `Error: Cannot find module '…/node_modules/depcurfew/dist/cli.js'`: run the install (`npm ci --ignore-scripts` in the project, or in `.claude/depcurfew` for the tools directory below) from a terminal, then try again. So install depcurfew before the hook is needed:
|
|
681
|
+
|
|
682
|
+
- **On your machine:** run `npm ci` once after cloning, in a terminal, before you start Claude Code.
|
|
683
|
+
- **In every session:** a `SessionStart` hook installs the locked copy when it is missing (with pnpm, Yarn or Bun, use their install from the lockfile instead of `npm ci`). It runs when a session starts or resumes, and Claude's first response waits for it, so the guard is in place before the first command. `--ignore-scripts` keeps the install from running dependency scripts before the guard is there. Add it next to `PreToolUse` in the same `.claude/settings.json`:
|
|
684
|
+
|
|
685
|
+
```json
|
|
686
|
+
{
|
|
687
|
+
"hooks": {
|
|
688
|
+
"SessionStart": [
|
|
689
|
+
{
|
|
690
|
+
"matcher": "startup|resume",
|
|
691
|
+
"hooks": [
|
|
692
|
+
{
|
|
693
|
+
"type": "command",
|
|
694
|
+
"command": "cd \"$CLAUDE_PROJECT_DIR\" && { test -f node_modules/depcurfew/dist/cli.js || npm ci --ignore-scripts; }"
|
|
695
|
+
}
|
|
696
|
+
]
|
|
697
|
+
}
|
|
698
|
+
]
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
- **A repository without `package.json` in its root** (several projects side by side): keep depcurfew in a small tools directory, for example `.claude/depcurfew/package.json` with depcurfew pinned exactly and its `package-lock.json` committed, install it there (`cd .claude/depcurfew && npm ci --ignore-scripts`, also in the `SessionStart` hook), and point the hook at `${CLAUDE_PROJECT_DIR}/.claude/depcurfew/node_modules/depcurfew/dist/cli.js`.
|
|
704
|
+
- **Cloud sessions (Claude Code on the web):** the repository's `.claude/settings.json` hooks run there, user-level ones do not. The environment's setup script runs before Claude Code starts, so it can install depcurfew too; the `SessionStart` hook above covers a cached environment as well.
|
|
705
|
+
- **Locked out already:** install depcurfew from a terminal outside Claude Code, or remove the hook from `.claude/settings.json` there; Claude cannot do it for you while the hook blocks its commands, which is the point of `block`.
|
|
706
|
+
|
|
459
707
|
Commands the guard reads (an add subcommand of any npm package manager, such as `install` or `i`, is checked for all of them; Python commands are matched word for word, and `python -m pip` counts as `pip`):
|
|
460
708
|
|
|
461
709
|
| Package manager | Add packages | Download and run | Starter kits |
|
|
@@ -475,9 +723,92 @@ Commands the guard reads (an add subcommand of any npm package manager, such as
|
|
|
475
723
|
|
|
476
724
|
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.
|
|
477
725
|
|
|
726
|
+
### MCP server
|
|
727
|
+
|
|
728
|
+
`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
|
+
|
|
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.
|
|
732
|
+
|
|
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.
|
|
734
|
+
|
|
735
|
+
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
|
+
|
|
737
|
+
Install depcurfew as a development dependency of the project, then add the server. Each configuration starts `node` itself with the path of the installed script, which works without a shell on Windows too:
|
|
738
|
+
|
|
739
|
+
<!-- BEGIN GENERATED: mcp-clients -->
|
|
740
|
+
The server speaks the `initialize` handshake of the protocol revisions 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05 and the stateless revision 2026-07-28, without the handshake, for clients that use it.
|
|
741
|
+
|
|
742
|
+
**Claude Code.** Add the server to `.mcp.json` of the project, which everyone working on it shares (Claude Code asks before it starts a server from that file):
|
|
743
|
+
|
|
744
|
+
```bash
|
|
745
|
+
claude mcp add --scope project --transport stdio depcurfew -- node '${CLAUDE_PROJECT_DIR:-.}/node_modules/depcurfew/dist/cli.js' mcp
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
or write `.mcp.json` yourself:
|
|
749
|
+
|
|
750
|
+
```json
|
|
751
|
+
{
|
|
752
|
+
"mcpServers": {
|
|
753
|
+
"depcurfew": {
|
|
754
|
+
"command": "node",
|
|
755
|
+
"args": [
|
|
756
|
+
"${CLAUDE_PROJECT_DIR:-.}/node_modules/depcurfew/dist/cli.js",
|
|
757
|
+
"mcp"
|
|
758
|
+
]
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
Claude Code sets `CLAUDE_PROJECT_DIR` to the project root for the server, and `depcurfew mcp` takes it as the project directory. Claude Code itself does not have the variable, so in `args` it needs the default `${CLAUDE_PROJECT_DIR:-.}` (in cmd.exe, leave out the single quotes).
|
|
765
|
+
|
|
766
|
+
**Claude Desktop.** In `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`, Windows: `%APPDATA%\Claude\claude_desktop_config.json`, Linux: `~/.config/Claude/claude_desktop_config.json`), with absolute paths and `--dir`, because Claude Desktop starts servers in no particular directory:
|
|
767
|
+
|
|
768
|
+
```json
|
|
769
|
+
{
|
|
770
|
+
"mcpServers": {
|
|
771
|
+
"depcurfew": {
|
|
772
|
+
"command": "node",
|
|
773
|
+
"args": [
|
|
774
|
+
"/path/to/project/node_modules/depcurfew/dist/cli.js",
|
|
775
|
+
"mcp",
|
|
776
|
+
"--dir",
|
|
777
|
+
"/path/to/project"
|
|
778
|
+
]
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
**VS Code.** In `.vscode/mcp.json` of the workspace:
|
|
785
|
+
|
|
786
|
+
```json
|
|
787
|
+
{
|
|
788
|
+
"servers": {
|
|
789
|
+
"depcurfew": {
|
|
790
|
+
"type": "stdio",
|
|
791
|
+
"command": "node",
|
|
792
|
+
"args": [
|
|
793
|
+
"${workspaceFolder}/node_modules/depcurfew/dist/cli.js",
|
|
794
|
+
"mcp"
|
|
795
|
+
],
|
|
796
|
+
"cwd": "${workspaceFolder}"
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
`code --add-mcp '{"name":"depcurfew","type":"stdio","command":"node","args":["/path/to/project/node_modules/depcurfew/dist/cli.js","mcp","--dir","/path/to/project"]}'` adds the server for one project to your user profile instead.
|
|
803
|
+
<!-- END GENERATED: mcp-clients -->
|
|
804
|
+
|
|
805
|
+
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
|
+
|
|
807
|
+
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
|
+
|
|
478
809
|
## Network and privacy
|
|
479
810
|
|
|
480
|
-
Only `check
|
|
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.
|
|
481
812
|
|
|
482
813
|
## Library
|
|
483
814
|
|