This page documents depcurfew 1.4.0, an earlier version. The current version is 1.6.1: its documentation · all versions
On this page
Changes in depcurfew 1.4.0
Composer
depcurfew supports Composer (PHP packages from Packagist). apply writes the Composer 2.11 release-age delay into composer.json (config.policy.cooldown.period, in whole days rounded up, with the composer: exceptions of cooldown.exclude in ignore) and, with installScripts.default: "deny", the plugins of installScripts.allow and installScripts.deny into config.allow-plugins. When it changes config.policy and composer.lock exists, it says to run composer update --lock (lockfile-out-of-date): Composer 2.11 hashes the policy into the lock. A delay that is already as long, written another way (259200, "72 hours"), stays. When cooldown.exclude drops a package, apply removes the ignore entries it wrote itself in the object form, marked by their reason; any other entry, including one it wrote earlier in the list form, which carries no mark, stays and gets a warning (exclude-entry-outside-policy) and a config-drift finding.
init lists the Composer plugins of composer.lock to review, and says which ones allow-plugins already allows. apply refuses a composer.json with comments or trailing commas, which Composer cannot read (cannot-edit, with the line, and its fix in errors[].fix of apply --json), and nothing is written.
Policy package lists accept Composer packages with the prefix composer: ("composer:monolog/monolog", "composer:acme/*"), with hints for a misspelled prefix and for a vendor/name written without one.
New doctor rule composer-config-weakens: composer.json settings and CI env variables that turn Composer's cooldown, malware blocking, allow-plugins, secure-http or TLS off, such as "policy": false, "allow-plugins": true while the policy denies plugins, or COMPOSER_NO_BLOCKING=1.
doctor reads composer.lock as Composer does. Packages from VCS, path, artifact or package repositories, and packages without an archive or with an archive without a type (which Composer clones), are checked against sources (lockfile-forbidden-source). Packages that config.preferred-install checks out from source give one error per entry, at its line in composer.json (and init notes them, composer-source-checkouts). While sources.git is "deny", an archive from GitHub, GitLab, Bitbucket or Codeberg whose VCS source is on another host (compared as URLs compare hosts, www.github.com as github.com, also for VCS repositories in composer.json) gets a warning, and while sources denies a kind of source, archives from a host depcurfew does not recognize give one warning per host that names the declared Composer repositories (WPackagist archives on downloads.wordpress.org are recognized). Plugins (types composer-plugin and composer-installer, in any case) that allow-plugins lets run are checked against installScripts (lockfile-install-scripts), including every plugin of a composer.lock from before allow-plugins existed, which Composer 2.2 to 2.9 load without asking. allow-plugins keys match as in Composer (any case, * also covers /). A lock written by a Composer older than 2.11, which ignores the delay, is reported by manager-version-too-old. doctor --osv also checks every release locked in composer.lock, including releases from an unrecognized archive host and releases Composer checks out from source.
Dependabot composer entries and Renovate (the packagist datasource, the composer manager and the php category) get the delay and the Composer exceptions. doctor compares them with Composer's delay, and notes that Dependabot counts from the release time the package author sets, while Composer counts from when Packagist received the version, and that Dependabot's composer.lock changes fail composer validate after apply (it hashes the lock with Composer 2.10.3; run composer update --lock on its branch).
depcurfew check composer:vendor/name[:constraint] and check_packages of depcurfew mcp look Composer packages up on Packagist. They pick the version Composer would install, with minimum-stability and prefer-stable of the root composer.json, and report a release younger than cooldown.minimum (counted from Packagist's published-time, as Composer 2.11 does), unknown names, constraints no release matches, branches and @dev, plugins the policy does not allow, and abandoned packages. A root composer.json that declares other repositories or turns packagist.org off makes them unverified, with the file and line, and they are not sent to Packagist then. Packagist answers over 16 MiB, with more than 10,000 versions or with a version longer than 255 characters are unverified too.
The Claude Code hook checks composer require (also r and Composer's short command names), composer global require, composer create-project and composer update of named packages, including php composer.phar …, --working-dir and options before the command name, as Composer reads them; a script of composer.json counts as a command, like npm run, but a command word that is a Composer command without scripts-aliases (which Composer before 2.7 ignores) is checked as that command. It asks when a command turns off Composer's protections (--no-blocking, --no-cooldown, COMPOSER_NO_BLOCKING, COMPOSER_POLICY=0, COMPOSER_POLICY_MALWARE_BLOCK=0, a shorter COMPOSER_POLICY_COOLDOWN_PERIOD) or changes Composer's configuration or repositories next to another Composer command, when the command sets COMPOSER or COMPOSER_HOME, for composer global update of a package, and when the root composer.json declares other repositories.
A composer.json in a parent directory is read as Composer reads it: Composer uses it only in a terminal after asking, or with use-parent-dir: true, so check and the hook keep its repositories but read the command word also without its scripts and check versions also at stability stable, and the stricter answer counts. A root composer.json depcurfew does not read (over 1 MiB, or not a regular file) leaves Composer packages unverified, and their names are not sent to Packagist.
--osv covers Composer packages as the Packagist ecosystem (OSV.dev matches v-prefixed versions, so they are sent as composer.lock writes them), and explain_policy names the Packagist URL in lookups.composerRepository.
Known malicious versions (OSV.dev)
New opt-in --osv for depcurfew check, the Claude Code hook (check --hook claude-code --osv) and depcurfew mcp --osv: the version an install would pick is looked up in OSV.dev, in one batch query per command, and a version with an OpenSSF malicious-packages record (MAL-…) is an error (known-malicious) that names and links the record. The hook denies it and tells Claude to choose another package or version.
New depcurfew doctor --osv and doctor rule known-malicious: every locked package version in npm, pnpm, Yarn, Bun and Deno lockfiles, uv.lock, poetry.lock, pdm.lock, Pipfile.lock, pylock.toml, pinned requirements and composer.lock is checked in batch queries, with an error per package and lockfile at its line. New GitHub Action input osv (default false) runs the same check in CI.
doctor --osv checks the version the package manager installs: the tarball of npm's resolved and pnpm's resolution.tarball (read from the URL path as the registry reads it, also with a query, a fragment or escapes, and relative pnpm tarballs) and Yarn's resolution; a registry tarball URL it cannot read gives a warning. A lockfile entry whose name or version says otherwise gets a warning, and so does a registry entry of a lockfile without an exact version.
If OSV.dev cannot be reached, or its answer has malformed record ids or an implausible number of records, nothing passes silently: check reports the package as unverified, the hook asks, and doctor gives a warning per lockfile. Apart from the host a redirect points to and package names shown in quotes with control characters escaped (such as the replacement Packagist suggests for an abandoned package), no text of an answer from a registry, Packagist or OSV.dev reaches the messages: a redirect to something that is not a URL or an answer that is not JSON gets a fixed text.
Without --osv nothing is sent to OSV.dev. With it, package names, ecosystems and versions go to api.osv.dev, private packages included. It catches only reported malware; the README section "Known malicious versions (OSV)" says what it does not cover.
explain_policy of depcurfew mcp shows the OSV endpoint in lookups.osv; the library exports runDoctorWithOsv and queryOsv.
Install scripts of well-known packages
depcurfew init writes esbuild, fsevents and lefthook from the lockfile into installScripts.deny (unless the package manager's own allow-list already lets them run: then it lists them for review), with a note per package on why and what changes (install-script-denied, install-script-project-task), and lists the other packages with install scripts to review (install-scripts-to-review, install-scripts-not-recorded).
For lefthook, whose install script only installs the git hooks, init and doctor tell you to run lefthook install once after each clone, as lefthook documents. The hints for these packages are given only for the package from the registry under its own name; from another source it is listed for review like any other.
doctor gives esbuild, fsevents and lefthook findings of lockfile-install-scripts their own fix, and warns when esbuild is in installScripts.deny while npm leaves optional dependencies out (omit=optional, --omit=optional, --no-optional, NPM_CONFIG_OMIT=optional), because esbuild then has no binary. apply warns about the same case with the new code deny-needs-optional-dependencies, and explain --json shows it per install command as optionalDependencies (omitted or kept, taking the project .npmrc into account as npm does) in project.jobs[].commands[] and project.dockerfiles[].stages[].commands[].
Corepack on Node.js 25
New doctor rule corepack-unavailable (warning), which respects the order of commands within one line: packageManager or devEngines.packageManager pins Yarn or npm, but a declared Node.js is 25 or later, which no longer comes with Corepack, so the pin has no effect there. The fix says how to install it (npm install -g corepack, then corepack enable). pnpm pins, a yarnPath in .yarnrc.yml that points at a Yarn release committed in the repository, and CI jobs or Dockerfile stages that install Corepack, or the pinned npm release itself (npm install -g npm@11.19.1), first are not reported; one that installs another npm release first is reported with that release. explain --json gives the line of that command as corepack in project.jobs[] and project.dockerfiles[].stages[], and marks the commands that run after it with afterCorepack.
Update bots and GitHub Actions
apply gives the GitHub Actions that your workflows run the same release-age delay as your packages: it writes default-days into Dependabot's existing github-actions entries. Renovate's top-level minimumReleaseAge, which apply already writes, covers its github-actions manager.
doctor (bot-cooldown-mismatch) checks that each bot waits at least cooldown.minimum before proposing a new action version, and broad-exclusion reports a Renovate rule that turns that delay off. The README says why the delay protects actions less than packages: their date comes from git, which the author sets.
Renovate packageRules with matchManagers or matchCategories (for example {"matchManagers": ["npm"], "minimumReleaseAge": "1 hour"}) are now read. They give bot-rule-shortens-delay in apply and bot-cooldown-mismatch or broad-exclusion in doctor. A single string instead of a list is read as Renovate reads it, and a rule whose glob depcurfew cannot read (braces, character classes, ?, extglobs) gets a doctor note when it may shorten the delay. For the actions, a rule with matchFileNames that covers every workflow file, or matchDepTypes: ["action"], counts as covering them.
A .github/dependabot.yml with YAML aliases gets a bot-cooldown-mismatch note at the alias, apply warns (bot-config-unreadable) and init notes it: Dependabot does not read aliases. doctor reports every Renovate rule that gives some update types a shorter delay, not only the shortest, and a rule with a matcher depcurfew does not evaluate (matchBaseBranches, matchJsonata, …) gets a note when it sets a shorter delay. explain shows the bot delay apply writes, rounded up to the bot's unit. broad-exclusion reports "*/*" in a Dependabot composer entry. Its numbers are read as Dependabot's YAML library reads them (default-days: 010 is 8 days).
Documentation site
The documentation is also a website, https://depcurfew.github.io, built by npm run site from the README, the knowledge base and schema.json, so it says what the README says. It serves the policy schema at schema.json and at the stable address schema/v1.json, and has the depcurfew icon.
Start pages for JavaScript (npm, pnpm, Yarn, Bun, Deno), Python (uv, pip, Poetry, PDM, Pipenv), PHP (Composer) and the update bots, each with the five-minute start, the settings depcurfew writes and the matching doctor rules. Every doctor rule, every apply and init code, the Claude Code hook, the MCP server and the GitHub Action have their own pages with stable anchors, and every knowledge base value links to its official source.
The site loads nothing from other hosts, needs no JavaScript and follows the system's light or dark mode. site/org-repo/ holds the workflow for the repository depcurfew/depcurfew.github.io, which builds the site once a day from the latest release, with the Pages permissions limited to the deploy job; it is set up there after this release (docs/RELEASING.md).
The site has a look in the colors of the icon: grouped sidebar navigation (a Menu button on phones), a start page with quick links and the supported tools, an "On this page" list on long pages, and levels shown as labeled pills, with no JavaScript and nothing loaded from other sites.
It keeps the documentation of every earlier release from 1.1.0 on under /<version>/ (for example /1.3.0/): that version's README, knowledge base, changes and schema, with a banner naming the version and a link to the current docs, and a Versions page lists them. npm run site -- --versions builds them from the git release tags.
Fixes
doctor finds the lines of minified lockfiles in linear time: a minified package-lock.json with 20,000 entries takes a fraction of a second instead of about 48 seconds.
Fix texts name a GitHub Actions job by its line when its key is not a valid job id, instead of quoting the key. Workflow env and steps are read through YAML anchors and aliases, which GitHub Actions supports. A package in installScripts.deny with false in allowScripts is reported when the npm in use is too old to read that denial. The apply warnings from comparing allow-lists start with the tool name, and doctor's Tools line names a package manager set up in CI for the projects (setUpInCi in --json).
The Claude Code hook asks when a command sets a package manager's variable in a way it cannot read as a value: printf -v, read, mapfile, getopts, wait -p, namerefs, +=, for, ${NAME:=…}, arithmetic, eval, aliases, code of python -c or node -e, and cmd's set /p and set /a. Such a value counts as the worst one: the hook asks only where a weakening value would make it ask (a registry or version variable next to an install that names packages, a release-age or install-script override where the policy restricts them, a Composer protection next to any package-manager command). A command that only reads or mentions a variable (printenv, echo, grep, a commit message) does not count, and names are compared in the case each tool reads them (on Linux and macOS COMPOSER counts and Composer does not; npm_config_* and YARN_* count in any case; on Windows every case counts). The hook also asks about a global change of Composer's configuration (composer config --global, -g or --file, and the same after composer global) even on its own, and about poetry source add/remove and pdm config next to an install. An install in the callback of mapfile -C is checked. ${#NAME} no longer makes a command unreadable, and words with many unclosed [ or a { before a long run of commas are read in linear time.
The Claude Code hook reads environment variables set in PowerShell on the Env: drive (New-Item, Set-Item, Rename-Item, Copy-Item, Move-Item, Set-Content) and through [Environment]::SetEnvironmentVariable, with parameters bound as PowerShell binds them (-Name:COMPOSER, -N, positions) and names with backtick escapes or quotes; it asks when such a command uses a name it cannot read ("Env:$name", wildcards, splatting). A quoted composer command (& 'composer' require …) is read as the command.
init and apply quote a Dependabot number as written and as read (default-days: 016 …, which Dependabot reads as 14).
With projects, a package manager that the root shows only through a CI setup step (such as pnpm/action-setup) counts as set up for the projects when the root has no manifest of its registry, or when the CI jobs install with it at least once, every such install runs in a listed directory and no root Dockerfile installs with it. doctor no longer lists a root pnpm that does not exist and no longer warns about it on Dependabot entries of other projects, apply no longer writes its file (such as pnpm-workspace.yaml) in an npm root, and explain shows it under "Package managers set up in CI for projects" (project.ciForProjects in --json). manager-version-too-old still checks the version the step installs.
docs/knowledge.md shows a value with a backtick inside (docker.syntax) in full instead of ending its code early.
Upgrading from 1.3.0
Run depcurfew apply again after the upgrade. In a repository with workflows, a cooldown and a Dependabot github-actions entry, 1.4.0 writes default-days into that entry; until then config-drift reports it. A longer default-days there is reported by apply (cooldown-shortened) and kept by init.
In a repository with composer.json, apply now writes config.policy and config.allow-plugins there, the delay into Dependabot composer entries and a Renovate rule for composer: exceptions; until then config-drift reports them. After apply, run composer update --lock and commit composer.lock. The delay needs Composer 2.11.0 or later wherever composer update or composer require runs.
A policy with composer: names needs depcurfew 1.4.0: 1.3.0 rejects the prefix (invalid-policy in doctor and the Action, ask in the hook).
doctor checks more than 1.3.0 did, so a repository that passed can get new findings: corepack-unavailable for a Yarn or npm pin with Node.js 25 or later, the bot delay for GitHub Actions, Renovate rules with matchManagers or matchCategories (every rule that shortens the delay, and a note for matchers depcurfew does not evaluate), "*/*" in a Dependabot composer exclude, a package in installScripts.deny whose allowScripts denial the npm in use cannot read, and in a Composer project composer-config-weakens, the sources and plugins of composer.lock (one warning per unrecognized archive host, as on Drupal sites), and a lock written by Composer before 2.11. With doctor.failOn: "warning", which init writes, these warnings fail doctor and the Action. apply stops with cannot-edit on a composer.json with comments or trailing commas, which it edited before.
The lockfile-install-scripts findings for esbuild, fsevents and lefthook have a new message and fix; the rule id and level stay the same. doctor --osv warns about lockfile entries whose label differs from the tarball they install, and about registry entries without an exact version.
The Claude Code hook now reads composer commands, which 1.3.0 let through without a check: it denies or asks where the policy says so.
In a repository with projects and an npm root, 1.3.0 apply may have written a pnpm-workspace.yaml in the root because a CI job sets up pnpm for a project. 1.4.0 does not write it, but while the file exists it reads pnpm in the root from it, so doctor keeps listing a root pnpm (with bot-cooldown-mismatch warnings on the Dependabot entries) and apply keeps updating it. Delete the file by hand if nothing else uses it.
The Action input osv needs the 1.4.0 Action: pin its commit SHA.