depcurfew v1.6.1
On this page

Start with Composer

What the README and the knowledge base say about these tools, on one page.

Five-minute start

  1. npx depcurfew doctor works before init: without depcurfew.json it compares the repository with the policy init would write and shows, as notes in a block of their own, what init and apply would change, for example a package manager that installs new versions without a release-age delay, with the settings apply would write for it. These notes never make it fail.
  2. npx depcurfew init writes depcurfew.json with the default policy below and lists the tools it detected. When the repository already sets a longer release-age delay for one of them, cooldown.minimum keeps the longest one, and init says where it found it.
  3. npx depcurfew explain shows what it detected and what the policy means for each tool.
  4. npx depcurfew apply --dry-run prints the changes as a diff, without writing anything.
  5. npx depcurfew apply writes them. Run it again whenever depcurfew.json changes; a second run with the same policy changes nothing.
  6. 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.
  7. Add the GitHub Action so that doctor runs on every push and reports to code scanning.

The default policy:

{
  "$schema": "https://unpkg.com/depcurfew/schema.json",
  "version": 1,
  "cooldown": {
    "minimum": "3d",
    "exclude": [],
    "securityUpdates": "bypass"
  },
  "installScripts": {
    "default": "deny",
    "allow": [],
    "deny": []
  },
  "sources": {
    "git": "deny",
    "remoteTarball": "deny",
    "file": "allow",
    "directory": "allow"
  },
  "targets": "auto",
  "doctor": {
    "failOn": "warning"
  }
}

Composer projects

depcurfew configures Composer (PHP packages from Packagist) from the same depcurfew.json, and the bots for Composer packages: Dependabot composer entries and Renovate.

  • Detection. composer.json or composer.lock in the directory means Composer. Only the root composer.json counts: Composer reads config and repositories from it alone. Composer reads composer.json as strict JSON, so apply does not edit one with a comment or a trailing comma (cannot-edit, with the line).
  • Package names. Composer packages on the policy's package lists take the prefix composer:, for example "composer:monolog/monolog" or "composer:acme/*". Composer names are lowercase vendor/name; a name without a prefix stays an npm name. Composer reads the keys of allow-plugins and ignore without regard to case, and its * there matches / too, so depcurfew compares them the same way: "Symfony/Flex": true is the plugin composer:symfony/flex.
  • The release-age delay needs Composer 2.11.0. apply writes the delay of the cooldown policy in whole days, rounded up ("36h" becomes "2 days"); a period that is already as long in another form (259200 seconds, "72 hours") stays as it is. Composer reads "policy": true and "cooldown": true as {}, so apply writes its object there; below "policy": false, "cooldown": false or another value that is no object it writes nothing (cooldown-not-written), and explain shows that Composer cannot apply the delay. Composer counts the delay from published-time, the moment Packagist received the version, which the package author cannot set. The delay applies when composer update or composer require picks versions; composer install installs what composer.lock locks, so review lockfile changes as before. Older Composer ignores the setting without a word, and Composer has no setting that pins its own version: doctor reports a composer.lock written by a Composer older than 2.11 (its plugin-api-version) and otherwise gives a note (manager-version-too-old).
  • Exceptions. apply writes the composer: exceptions of the policy as the ignore list, adds them to a list that is there, or adds them to an ignore object with a reason that names depcurfew.json. That reason marks an entry as apply's: once the policy drops the name from its exceptions, apply removes it. Any other entry that exempts a package the policy does not stays, also a name of a list an earlier apply wrote, since a list has no room for a mark: apply warns (exclude-entry-outside-policy) and doctor reports it (config-drift, a warning) with the line, until you remove it and run composer update --lock.
  • Lock again after apply. Composer 2.11 hashes config.policy into composer.lock, so after apply changes it, composer install warns that the lock file is out of date and composer validate fails; apply says so (lockfile-out-of-date), only in the run that changes the setting: a later apply and doctor do not compute the hash of composer.lock and do not repeat it. Run composer update --lock, which writes the new hash without updating packages, and commit composer.lock. Dependabot writes composer.lock with Composer 2.10.3, whose hash leaves config.policy out, so the same happens on each of its composer pull requests once config.policy is there: run composer update --lock on the pull request's branch before merging (doctor says so in its note on each Dependabot composer entry).
  • Install scripts are plugins. Composer runs the scripts of the root composer.json only; the code of dependencies that runs during Composer commands is plugins (type composer-plugin, and the legacy composer-installer, in any case). With installScripts.default: "deny", apply writes the plugins of installScripts.allow into allow-plugins as true and those of installScripts.deny as false (patterns as they are, since Composer takes them). An unlisted plugin makes Composer ask, or fail without a terminal (one that declares extra.plugin-optional is skipped there). apply replaces "allow-plugins": true with the list, {} when the policy lists no plugins (allow-all-replaced), and never changes an entry it did not write: an earlier key that blocks a plugin the policy allows, such as a final "*": false, gives deny-conflicts-with-file with the fix to put the plugin before it. doctor lists the plugins in composer.lock that allow-plugins lets run but the policy does not allow, and those it does not list. A composer.lock without plugin-api-version (or with one below 2.2.0) next to an empty or missing allow-plugins makes Composer 2.2 to 2.9 run every plugin without asking, so doctor reports each plugin the policy does not allow as an error; composer update --lock records plugin-api-version. apply does not add a catch-all "*": false for this: Composer takes the first key that matches, and the plugins apply adds go at the end, where such a key would hide them.
  • No switch for dependency sources. Composer has no setting that forbids VCS, path, artifact or package repositories (source-not-enforceable), but only the root composer.json declares them. doctor reports the packages of composer.lock that come from such a repository, and a repository of a denied kind whose packages it cannot find in the lockfile. A package of composer.lock without a dist, or with a dist without a type, is installed from its source, a VCS checkout, whatever composer.json declares, so it counts as a VCS package, and so does one that config.preferred-install of composer.json makes Composer check out: "source", a pattern object whose first matching key gives anything but "dist" or "auto", and "auto" for a dev version (dev-main, 1.x-dev). Composer's default "dist" takes the archive, also of a dev version. Such packages are one error per entry of config.preferred-install, at its line in composer.json, naming the packages with their lines in composer.lock, and the fix is in composer.json (remove the entry or set it to "dist"), or sources.git: "allow"; init notes them (composer-source-checkouts). An archive from api.github.com, gitlab.com, bitbucket.org or codeberg.org next to a source on another host than its own (github.com for api.github.com; hosts are compared as URLs compare them, so github.com:443 is github.com, and www.github.com is github.com, as Composer's GitHub driver reads it) gives a warning while the policy denies VCS sources: Packagist.org never writes such an entry, and Composer installs from that source with --prefer-source, with preferred-install set in COMPOSER_HOME/config.json, and before 2.10.0 when the archive cannot be downloaded (2.10 does that only with source-fallback, which 2.11 removed). An archive from a host that neither the archives of Packagist.org (api.github.com, gitlab.com, bitbucket.org, codeberg.org) nor a repository of composer.json use is a source depcurfew does not recognize: one warning per host while the policy denies a source, naming its packages with their lines. A Composer repository of composer.json may keep its archives on another host than its own; depcurfew knows that of WPackagist (https://wpackagist.org serves downloads.wordpress.org), and for another one, such as packages.drupal.org with its archives on ftp.drupal.org, the warning names the declared repositories and says how to check that one of them serves those archives, since composer.lock does not say which repository a package comes from. doctor --osv looks up every release of composer.lock, also one from such a host and one Composer checks out of its source, but not a package of a VCS repository of composer.json, whose code is not the release on Packagist.
  • Edited lockfile entries. The rule lockfile-tampered reports a package of composer.lock (packages and packages-dev) whose transport-options has prevent_url_access_callable or prevent_ip_access_callable, and a Composer or VCS repository of composer.json that has one in its options, or a package a package repository defines that has one in its transport-options (a path or artifact repository passes no such option to its downloads). Composer 2.11.1 and earlier versions that know these options call them as PHP functions while downloading, when they name one, so an edited composer.lock can run a command during composer install (GHSA-jhfp-4ppm-crmm); Composer 2.11.2 no longer takes them from composer.lock. It is an error with or without depcurfew.json, whatever the Composer version, which depcurfew cannot tell: use Composer 2.11.2 or later before any composer install or update on such a lockfile, then remove the option or restore the lockfile, and find out who changed it. Without depcurfew.json, a composer.lock that does not parse is a note against the default policy, as another lockfile is. vendor/composer/installed.json, which Composer 2.11.1 reads the same way, is not checked.
  • Settings that turn protections off. The rule composer-config-weakens reports "policy": false, a cooldown or block turned off, malware blocking turned off or limited to updates, a repository with "require-published-time": false or "filter": false, "allow-plugins": true (or a key that matches every plugin, "*" or "*/*") while the policy denies plugins, "secure-http": false, "disable-tls": true, and COMPOSER_POLICY=0, COMPOSER_NO_BLOCKING or COMPOSER_NO_SECURITY_BLOCKING set to 1, true, on, false or off (any other value makes Composer stop with an error), a shorter COMPOSER_POLICY_COOLDOWN_PERIOD, COMPOSER_POLICY_COOLDOWN_BLOCK=0 or COMPOSER_POLICY_MALWARE_BLOCK=0 in the env of a CI workflow (of the workflow, a job, its container or a step), exported or set before a command in a run step, written to $GITHUB_ENV (which the later steps of the job get), or set with ENV or as the default of an ARG in a Dockerfile stage (Docker gives RUN the build arguments as variables, under ENV). It also reports composer config policy.cooldown false and the other settings above written by composer config in a run step or a RUN instruction (Composer takes true, false, 1 and 0 there and writes the setting into composer.json for the commands after it), and --no-cooldown, --no-blocking, --no-security-blocking or --no-secure-http on a composer command there.
  • Bots. apply gives Dependabot composer entries the cooldown and the composer: exceptions, and Renovate a rule for them limited to the packagist datasource. Dependabot counts its composer cooldown from the release time the package author writes and updates composer.lock with a Composer without the cooldown, so doctor gives each such entry a note; Renovate counts from published-time.
  • Checking before an install. depcurfew check composer:vendor/name (also composer:vendor/name:^3.9, as composer require takes it), the Claude Code hook (composer require, composer update of named packages, composer global require, composer create-project) and check_packages of depcurfew mcp look Composer packages up on Packagist: see Guarding installs.
  • Not covered. composer config --global (or --file) in CI writes COMPOSER_HOME/config.json or another file, which doctor does not follow. COMPOSER_HOME/config.json and COMPOSER_HOME/composer.json (Composer's configuration for the whole machine and its global project) are not read or written, so a repository, a minimum-stability or a preferred-install they set is not seen by doctor, check or the hook; the hook asks about a command that sets COMPOSER_HOME itself, and about composer global update of a package named without a constraint, whose constraint is in COMPOSER_HOME/composer.json. Command-line options in CI steps, such as --prefer-source, are not read either.

In a repository without package.json

depcurfew is a Node.js program, and a repository without package.json in its root, such as a Rust, .NET, Python, PHP or Ruby project, does not need one to run it.

In GitHub Actions, the GitHub Action needs nothing installed.

Once, by hand, run an exact release with npx:

npx --yes depcurfew@1.5.2 doctor

Name a release that is at least as old as cooldown.minimum of the policy: depcurfew's own Claude Code hook denies a younger one, and so does npm where its min-release-age is set (npx then fails with ETARGET). The example names the newest release that was at least 7 days old when this README was written, usually the one before the newest. Without --yes, npx asks before it downloads a package the project does not have, and without a terminal or in CI it downloads without asking.

Pinned in the repository, keep depcurfew in the tools directory .claude/depcurfew: its own package.json pins depcurfew exactly, its package-lock.json is committed and .claude/depcurfew/node_modules is git-ignored. npm ci installs what that package-lock.json locks, and --prefix runs it in the tools directory, so both commands run from the root of the repository, without cd:

npm ci --ignore-scripts --prefix .claude/depcurfew
node .claude/depcurfew/node_modules/depcurfew/dist/cli.js doctor

doctor reads this copy too: depcurfew-outdated says when the depcurfew that runs is older than it, and the Claude Code hook can run it. To be offered new releases of the pin, you can add a Dependabot npm entry for the directory /.claude/depcurfew; apply gives it the cooldown of the policy like any other npm entry. The tools directory and the Action pin their versions separately, so update both.

Supported tools

ToolRelease-age delayExceptionsInstall-script allow-list (Python: packages built from source; Composer: plugins)Dependency sourcesVerified
Composerconfig.policy.cooldown.period in composer.json (duration string), since 2.11.0config.policy.cooldown.ignore, since 2.11.0config.allow-plugins in composer.jsonnone2026-11-02 – 2026-11-11

⚠️ official sources disagree (depcurfew takes the safer reading) · ❓ official sources are incomplete. Every value with its source: docs/knowledge.md.

doctor rules for these tools

composer-config-weakens

error · composer.json, a CI workflow or a Dockerfile turns a Composer protection off: the cooldown, malware blocking, allow-plugins, secure-http or TLS.

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

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

lockfile-tampered

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

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

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

Every rule: doctor rules.

Sources

Every fact behind these tables, with its official source, is in the knowledge base: Composer.