This page documents depcurfew 1.3.0, an earlier version. The current version is 1.6.1: its documentation · all versions
On this page
Changes in depcurfew 1.3.0
An MCP server for other coding agents
depcurfew mcp is a Model Context Protocol server over stdio, so that Claude Desktop, VS Code, Cursor and other agents can ask before they install a package. It has two tools: check_packages (the same checks and the same document as depcurfew check --json) and explain_policy (which depcurfew.json applies and what it allows). It speaks both the initialize handshake (protocol revisions 2024-11-05 to 2025-11-25) and the stateless revision 2026-07-28 with server/discover.
The registry and the Python index come only from the server's own options and .npmrc, never from a tool call, so an agent cannot point the check at a registry that approves anything. A directory argument must stay inside the project. Fixes that would change depcurfew.json are not offered to the agent.
It limits what a client can send (1 MiB per line, nesting depth 64, 4 calls at once), stops on a cancelled request, and ends quietly when stdin or stdout closes or on SIGINT and SIGTERM. The README has configurations for Claude Code (claude mcp add, .mcp.json), Claude Desktop and VS Code. The server only advises; the configuration apply writes is what enforces.
Several projects in one repository
The new policy field projects lists directories that are independent projects, such as frontend and backend, each with its own manifest and lockfile. apply, doctor and explain in the root handle every project with the root's policy, and configure the update bots once, from the root, with a delay that covers every project's package managers. init in a root without a manifest proposes the directories that have one.
A listed directory that is missing, is not a directory, is reached through a symlink, has its own depcurfew.json or is a workspace package of the root is an error (new apply codes project-not-found, project-not-a-directory, project-symlink, project-has-policy, project-in-workspace, and the doctor rule invalid-project); nothing is written. A target that no directory uses gets target-not-found. The user policy cannot list projects. Policies without the field are unchanged.
The npm and pip that come with Node.js and Python
doctor now knows which npm each Node.js release brings and which pip each Python release brings. Node.js 22, still supported until 2027-04-30, comes with npm 10, which has no release-age delay and ignores settings it does not know without a word; Node.js 24 brings npm 11.10 or later only from 24.14.1. manager-version-too-old reports a CI job that installs with npm after setting up such a Node.js, a Dockerfile stage that runs npm ci or pip install on such an image, and the version files developers use, with the npm or pip version to install or the Node.js or Python release to move to.
It reads Node.js versions from .nvmrc, .node-version, engines.node of a private package, the files setup-node's node-version-file names, the values of ${{ matrix.<key> }}, and Dockerfiles (Dockerfile, *.Dockerfile, Dockerfile.<name>, Containerfile): FROM node:<tag> and FROM python:<tag> with ARG substitution and stages, and npm install -g npm@… or pip install pip==… before the install. A setup step only counts for the installs that follow it, before the next setup step. explain shows the jobs and Dockerfiles it read.
New rule runtime-end-of-life (warning): a declared Node.js version that only allows release lines past their end of life, where they install packages or run code.
doctor and the GitHub Action
doctor --strict (also with --user) exits with 1 for warnings too; notes never fail. The JSON has a new field strict.
New policy section doctor with failOn: "error" | "warning" (also in the user policy). init and init --user write "warning", so new policies make doctor and the Action fail on warnings too; a policy without the section fails on errors only, as before (the new checks can still find new errors, see "Upgrading from 1.2.0"). doctor --fail-on error|warning overrides it (--strict is --fail-on warning), and so does the Action input fail-on, which is now empty by default and follows the policy. The JSON of doctor has the new fields failOn and failOnFrom (option, policy or default).
The rule list says "not applicable" for a check that does not concern the repository (Python rules in a JavaScript project, bot rules without a bot), instead of "passed".
Action: new input fail-on: error | warning; with warning, warnings fail the step and the "depcurfew doctor" check like errors. fail-on-error still works and is deprecated; false keeps the step from failing whatever fail-on says, and now every other value counts as on.
The "depcurfew doctor" check shows the findings as annotations on their lines (at most 100, errors first), and a workflow that runs on both push and pull_request updates the entry of the same commit instead of adding a second one.
Protecting the whole machine
init --user keeps the longest release-age delay already set in the user files of the installed tools, as init does in a project.
Yarn's version is read from more places without running it: a global install of @yarnpkg/cli-dist, Corepack's lastKnownGood.json, and a yarnPath in ~/.yarnrc.yml that names yarn-<version>.cjs. Bun from its official installer still needs --for.
Poetry and PDM on macOS: the file they read depends on the platformdirs version installed next to them, which depcurfew now reads; when it cannot, apply --user writes where the documentation says and warns with both paths (user-file-uncertain).
Claude Code hook and installation
The README says what the hook does before depcurfew is installed: with onFailure: "block" a missing node_modules/depcurfew blocks every shell command, including the install that would bring it. It shows a SessionStart hook that installs the locked copy first, a layout for repositories without package.json in the root, and what to do in cloud sessions or when already locked out.
init warns when it finds Python tools that the default policy also blocks building packages from source, so a package without wheels stops installing; run doctor before apply.
The README recommends --save-exact and says what depcurfew does not do: it has no malware database, so use it together with a tool that blocks known malware.
Fixes from the review before the release
apply now considers every npm the repository runs: the pinned one, the npm that CI jobs and Dockerfiles install, and the npm that the declared Node.js versions bring (in a repository with projects, also the root's .nvmrc, .node-version and .tool-versions for projects without their own). If one of them is npm 11.16 to 11.x and install scripts are denied, it writes strict-allow-scripts=true (warning strict-allow-scripts-for-npm-in-use; with npm 12 an unreviewed script then fails the install instead of being skipped with a warning). doctor counts script blocking as the files stand, so the version it recommends really blocks them.
npm install -g <another package> in CI or a Dockerfile no longer counts as a project install (npm ignores the project .npmrc in global mode). pnpm, Yarn and Bun installs in CI jobs count for runtime-end-of-life. In a repository with projects, a root CI job that installs with working-directory in a project is checked with that project's settings.
Workflows and Dockerfiles from a pull request can no longer crash or stall doctor and the Action: large matrices are capped with a notice, the command and Dockerfile readers run in linear time, and ARG values are limited in size. A here-document marker inside quotes no longer swallows the rest of a Dockerfile. Identical findings are reported once, and findings at one place that differ only in numbers are capped with a count.
apply checks again, right before writing, that no directory on the way to a file was replaced by a symlink that leads out of the repository.
The Claude Code hook takes its policy from the project (CLAUDE_PROJECT_DIR) when the working directory is inside it, so a depcurfew.json in a vendored repository or package folder cannot relax it. The MCP server's directory argument cannot relax it either. Warnings without a decision (a deprecated version) reach Claude as context, and registry text is quoted and marked as data, with invisible format characters shown escaped.
Node.js before 18 (npm 8 or older, which runs every install script) is no longer taken for an npm that blocks scripts: depcurfew knows the npm of every Node.js release, and an npm it cannot tell counts as running scripts (scripts-not-enforceable, and lockfile-install-scripts reports the packages). A Dockerfile COPY source is resolved from the build context as Docker does, so COPY frontend/../ . belongs to the root. A project inherits each root Node.js version file (.nvmrc, .node-version, .tool-versions) that it lacks, and explain shows what a project takes from the root.
check_packages works on every Node.js that engines allows (it used AbortSignal.any, which needs Node.js 18.17 or 20.3).
With --json, a usage error (an unknown option, check without a package, --json with --sarif) is a JSON document with errors on stdout, exit code 2, and stderr stays empty. explain --json warnings have level, location and fix, and an invalid project makes explain exit with 1.
doctor --user warns when a tool in PATH is too old for the user policy instead of reporting no problems. In a project, bot files of a listed project are a note (bots read their configuration only from the repository root), and a project without a Dependabot entry for its directory is a warning.
The "depcurfew doctor" check no longer keeps annotations of an earlier run with different findings: it replaces that entry. A symlink loop in the repository no longer crashes the Action.
The Claude Code hook and depcurfew mcp apply the project's depcurfew.json and a nearer one together (the stricter answer counts) also when the project is not a git repository; when the project root has no policy, the nearest one applies alone, as in 1.2.0.
A hook event that is not a JSON object, or a Bash, PowerShell or Monitor call without a readable command, now blocks the call (exit code 2) instead of letting it through.
check, the hook and check_packages read .npmrc where npm reads it, so a listed project's own .npmrc counts. An .npmrc elsewhere, or a registry in .yarnrc.yml, bunfig.toml or uv's files, that names another registry than the one depcurfew checked makes the packages it applies to unverified (the hook asks); the fix says to pass --registry or --index to the hook command, depcurfew mcp or check when you trust that registry.
The hook reads installs hidden in a trap, an alias defined in the command, grouped env -S options, sudo -s, . npm …, an interpreter that runs a package manager's script (node /usr/bin/npm, node .yarn/releases/yarn-*.cjs, python3 -m pip.__main__), and commands after a here-document with CRLF line endings.
The hook asks when the command itself weakens the policy, also for npm ci: a shorter release-age delay (--min-release-age=0, --before, UV_EXCLUDE_NEWER), install scripts turned on where the policy denies them (--ignore-scripts=false, npm_config_ignore_scripts=false, bun add --trust), or a configuration or package.json change before an install (npm config set registry … && npm ci, npm pkg set …). config get, config list, an equal or longer delay and --ignore-scripts pass.
The hook checks each install with the policies of the directory it runs in: after cd, pushd/popd, cd -, PowerShell's Set-Location/Push-Location, cmd's cd /d, and with npm --prefix, pnpm --dir, yarn --cwd, bun --cwd, uv --directory/--project, poetry -C/-P or pdm -p, so cd pkgs/strict && npm install x meets pkgs/strict/depcurfew.json and that directory's .npmrc. A directory outside the project is checked against the project's policy only. When the hook cannot tell the directory (cd "$DIR", a cd in a loop or function), it asks about installs that name packages.
Notes of check --json and the MCP tools have a level (note), a code, a location and a fix when there is one, and the text output shows note: … (code) with a fix: line.
doctor checks the npm that a CI job or Dockerfile installs (npm install -g npm@X, also ranges such as npm@^11.10.0) for the project it installs in; npm@latest and npm@${VAR} count as an npm that may run every install script, in doctor and apply alike. A place that installs only with npm ci no longer needs a release-age setting, and one that installs with --ignore-scripts or npm_config_ignore_scripts=true no longer needs script blocking. Fixes no longer suggest an npm that cannot run on that Node.js.
doctor reads job containers (container: node:20), cd <dir> && before an install, lts/-1 in .nvmrc, and Poetry installed with pipx install poetry==X. A stage whose node or python image is pinned by digest only, or has a tag depcurfew does not know, is a note with a fix in manager-version-too-old too.
doctor reports pip installs in CI jobs and in Dockerfile stages of the python image that run without the policy's release-age delay (no PIP_UPLOADED_PRIOR_TO in env:, ENV or on the command line and no --uploaded-prior-to) or with a shorter one (python-config-ignored). Root jobs that install in a project count for that project; jobs and stages that set PIP_CONFIG_FILE, run pip config set or write a pip.conf are not checked.
init skips git submodules, and apply and doctor refuse them as projects (project-is-repository). doctor frontend and the Action's directory: frontend use the root policy when it lists the directory in projects.
New doctor rule lockfile-out-of-date (warning): a uv.lock or pdm.lock locked with other settings than the project has now, for example after apply changed exclude-newer. apply warns about a Renovate packageRules entry with a shorter delay (bot-rule-shortens-delay) and about Yarn's enableScripts: true (allow-all-replaced).
config-drift reports every changed place in a file, rules that did not run because the policy is invalid show as skipped, and a Renovate file depcurfew cannot read is a note instead of a passed check. Notes have a level, a code and a fix in JSON and start with note in the text; syntax errors and depcurfew.json problems point at their line and column; doctor --user shows user-file drift with its line and the expected value.
explain shows, for each tool, what installScripts and sources mean before apply: the setting apply writes and its file, or why the tool cannot enforce it, with the same warning code and fix as apply. explain --json has them in policy.installScripts, policy.sources and per project.
The hook also applies the working directory's policy to installs after cd, pushd, env -C or --prefix, so changing directory can only tighten the check; a cd to a missing directory or a file asks, and cd -P and set -P follow symlinks. It decodes $'…' as bash does, asks about command names built at run time inside eval, sh -c and trap, reads abbreviated --no- options of npm and pnpm (--no-ign), treats the directory of find -execdir and of an install in a trap before a cd as unknown, and reads PowerShell paths with \.
The hook asks about packages that Poetry ([[tool.poetry.source]]), PDM ([[tool.pdm.source]]) or Pipenv ([[source]] in Pipfile) would take from another index, and about packages that [tool.uv.sources] pins to an explicit uv index. When such a setting applies and the checked registry has no such package or version, the install is unverified (the hook asks) instead of denied as a made-up name.
doctor reports a syntax error in a package manager's or bot's configuration file also without a valid depcurfew.json, and says "needs a valid depcurfew.json" for the checks it skipped. doctor --user reports each place where a user file differs from the user policy. An invalid depcurfew.json shows its line in the text of apply, check and explain, the Action's own log lines start with their level, and notes of apply --json and explain --json have a level and a code.
depcurfew init --json (also with --user) prints the file written, the projects and tools, and every warning and note with a code, level, location and fix; the text output gives each its level word, code and fix.
The hook reads env -S strings the way GNU env does (including -C inside them), follows the bash directory stack exactly (dirs -c, pushd -n, popd -n, +N/-N, a popd on an empty stack), and asks about installs after OLDPWD or DIRSTACK may be set through a name the shell builds, after a trap on DEBUG, ERR, RETURN or a signal that changes directory, after . or source of a file, or after a function that changes directory. Sourcing a Python virtual environment's activation script (source .venv/bin/activate, Scripts\Activate.ps1) stays quiet when the environment holds pyvenv.cfg and nothing earlier in the command may have written the script (a redirection with <> or after ( … ) counts as writing). When two policies deny a release for its age, the fix names the longer wait. PIPENV_PIPFILE moves a Pipenv install to its Pipfile's directory, aliases ending in a blank expand the next word as in bash, and override messages name the policy file that sets the delay.
doctor without a valid depcurfew.json reports a YAML file whose aliases expand without end, as it does with a policy, and an update-bot file depcurfew cannot read or parse gets a note instead of a claimed default delay. The script-not-followed note points at its line, the Action names the line of an invalid policy, home-missing names the HOME variable when it is not an absolute path, and the .npmrc of a workspace package, which npm does not read, no longer makes the hook and check ask when depcurfew matches the workspace exactly as npm does.
Smaller fixes: configuration files are read only as regular files of at most 1 MiB, so a named pipe in place of ~/.npmrc no longer hangs check, the hook or mcp; the hook no longer stalls on self-expanding aliases, on workspaces patterns that repeat ** or on long pushd sequences, and reads hostile commands up to 1 MiB in linear time; doctor and the Action read a CI run step or Dockerfile RUN of any length in linear time (past 256 cd or 1024 variable assignments they say so in a script-not-followed note), and large PDM, uv and drift checks no longer take minutes; registry documents with more than 100,000 versions are not checked, and two policies share each lookup; usage errors print as error: … (usage) with a fix, and an unknown --hook or a --registry or --index that is not an http(s) URL is a usage error (exit code 2); fixes given to agents tell them to ask the user; PyPI yank reasons are quoted, and line separators and Hangul fillers are escaped; apply's diff hides the .npmrc client key; apply --user without a home directory reports home-missing; doctor --user calls Yarn 1.x Yarn Classic; doctor: {} counts as the default; depcurfew-outdated also reads .claude/depcurfew; the Action names an http://GITHUB_API_URL; apply removes an empty [installer] table it created in poetry.toml.
Compatibility
depcurfew runs on Node.js 18.3.0 or later (it needed 22 before); CI runs every command on the oldest supported version. Node.js 18 and 20 no longer get security fixes from the Node.js project.
With FORCE_COLOR and NO_COLOR both set, and on Node.js 18 before 18.17 for check, Node.js no longer prints its own warnings to stderr, which stays empty with --json.
Upgrading from 1.2.0
Run depcurfew apply again after the upgrade. 1.3.0 knows which npm each Node.js release brings and writes strict-allow-scripts=true where an npm 11.16 to 11.x runs and install scripts are denied; until then doctor reports config-drift, and manager-version-too-old for the places that run such an npm and that 1.2.0 did not know of, such as a setup-node 24 or 26 job.
doctor checks more than 1.2.0 did, so a repository that passed can get new findings, which apply cannot clear: a CI job or Dockerfile stage that installs packages on Node.js 20 or 22, which come with npm 10, is manager-version-too-old (error). With the policy init writes, which denies install scripts, that holds for npm ci too, because npm 10 runs every dependency install script; install npm 11.16 or later in that job (npm install -g npm@11.16.0, which needs Node.js 20.17.0 or 22.9.0 or later) or move it to Node.js 24.14.1 or later. With a policy that sets only a release-age delay, only installs that resolve packages (npm install, not npm ci) are reported. Also, a pip install in CI or a Dockerfile without PIP_UPLOADED_PRIOR_TO is python-config-ignored (error), and a uv.lock or pdm.lock locked before apply changed the delay is lockfile-out-of-date (warning).
The hook may ask where 1.2.0 allowed: when the install would use another registry than the one depcurfew checked, or when the command itself weakens the policy.
Use the same depcurfew version everywhere: on developer machines, in the npx cache, in the Action (pinned by commit SHA) and in the hook (node_modules/depcurfew or .claude/depcurfew). 1.2.0 apply rewrites the managed block without the lines 1.3.0 adds, and 1.3.0 doctor then reports them again.
A policy with the doctor section, which init now writes, or with projects needs depcurfew 1.3.0: 1.2.0 rejects it as an unknown field (invalid-policy in doctor and the Action, ask in the hook).