On this page
Start with Composer
What the README and the knowledge base say about these tools, on one page.
Five-minute start
npx depcurfew doctorworks beforeinit: withoutdepcurfew.jsonit compares the repository with the policyinitwould write and shows, as notes in a block of their own, whatinitandapplywould change, for example a package manager that installs new versions without a release-age delay, with the settingsapplywould write for it. These notes never make it fail.npx depcurfew initwritesdepcurfew.jsonwith 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.minimumkeeps the longest one, andinitsays where it found it.npx depcurfew explainshows what it detected and what the policy means for each tool.npx depcurfew apply --dry-runprints the changes as a diff, without writing anything.npx depcurfew applywrites them. Run it again wheneverdepcurfew.jsonchanges; a second run with the same policy changes nothing.npx depcurfew doctorreports 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" }thatinitwrites, a warning.- 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.jsonorcomposer.lockin the directory means Composer. Only the rootcomposer.jsoncounts: Composer readsconfigandrepositoriesfrom it alone. Composer readscomposer.jsonas strict JSON, soapplydoes 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 lowercasevendor/name; a name without a prefix stays an npm name. Composer reads the keys ofallow-pluginsandignorewithout regard to case, and its*there matches/too, so depcurfew compares them the same way:"Symfony/Flex": trueis the plugincomposer:symfony/flex. - The release-age delay needs Composer 2.11.0.
applywrites 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 (259200seconds,"72 hours") stays as it is. Composer reads"policy": trueand"cooldown": trueas{}, soapplywrites its object there; below"policy": false,"cooldown": falseor another value that is no object it writes nothing (cooldown-not-written), andexplainshows that Composer cannot apply the delay. Composer counts the delay frompublished-time, the moment Packagist received the version, which the package author cannot set. The delay applies whencomposer updateorcomposer requirepicks versions;composer installinstalls whatcomposer.locklocks, so review lockfile changes as before. Older Composer ignores the setting without a word, and Composer has no setting that pins its own version:doctorreports acomposer.lockwritten by a Composer older than 2.11 (itsplugin-api-version) and otherwise gives a note (manager-version-too-old). - Exceptions.
applywrites thecomposer:exceptions of the policy as theignorelist, adds them to a list that is there, or adds them to anignoreobject with a reason that namesdepcurfew.json. That reason marks an entry asapply's: once the policy drops the name from its exceptions,applyremoves it. Any other entry that exempts a package the policy does not stays, also a name of a list an earlierapplywrote, since a list has no room for a mark:applywarns (exclude-entry-outside-policy) anddoctorreports it (config-drift, a warning) with the line, until you remove it and runcomposer update --lock. - Lock again after
apply. Composer 2.11 hashesconfig.policyintocomposer.lock, so afterapplychanges it,composer installwarns that the lock file is out of date andcomposer validatefails;applysays so (lockfile-out-of-date), only in the run that changes the setting: a laterapplyanddoctordo not compute the hash ofcomposer.lockand do not repeat it. Runcomposer update --lock, which writes the new hash without updating packages, and commitcomposer.lock. Dependabot writescomposer.lockwith Composer 2.10.3, whose hash leavesconfig.policyout, so the same happens on each of its composer pull requests onceconfig.policyis there: runcomposer update --lockon the pull request's branch before merging (doctorsays so in its note on each Dependabotcomposerentry). - Install scripts are plugins. Composer runs the scripts of the root
composer.jsononly; the code of dependencies that runs during Composer commands is plugins (typecomposer-plugin, and the legacycomposer-installer, in any case). WithinstallScripts.default: "deny",applywrites the plugins ofinstallScripts.allowintoallow-pluginsastrueand those ofinstallScripts.denyasfalse(patterns as they are, since Composer takes them). An unlisted plugin makes Composer ask, or fail without a terminal (one that declaresextra.plugin-optionalis skipped there).applyreplaces"allow-plugins": truewith 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, givesdeny-conflicts-with-filewith the fix to put the plugin before it.doctorlists the plugins incomposer.lockthatallow-pluginslets run but the policy does not allow, and those it does not list. Acomposer.lockwithoutplugin-api-version(or with one below 2.2.0) next to an empty or missingallow-pluginsmakes Composer 2.2 to 2.9 run every plugin without asking, sodoctorreports each plugin the policy does not allow as an error;composer update --lockrecordsplugin-api-version.applydoes not add a catch-all"*": falsefor this: Composer takes the first key that matches, and the pluginsapplyadds 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 rootcomposer.jsondeclares them.doctorreports the packages ofcomposer.lockthat come from such a repository, and a repository of a denied kind whose packages it cannot find in the lockfile. A package ofcomposer.lockwithout adist, or with adistwithout a type, is installed from itssource, a VCS checkout, whatevercomposer.jsondeclares, so it counts as a VCS package, and so does one thatconfig.preferred-installofcomposer.jsonmakes 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 ofconfig.preferred-install, at its line incomposer.json, naming the packages with their lines incomposer.lock, and the fix is incomposer.json(remove the entry or set it to"dist"), orsources.git: "allow";initnotes them (composer-source-checkouts). An archive fromapi.github.com,gitlab.com,bitbucket.orgorcodeberg.orgnext to a source on another host than its own (github.comforapi.github.com; hosts are compared as URLs compare them, sogithub.com:443isgithub.com, andwww.github.comisgithub.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, withpreferred-installset inCOMPOSER_HOME/config.json, and before 2.10.0 when the archive cannot be downloaded (2.10 does that only withsource-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 ofcomposer.jsonuse 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 ofcomposer.jsonmay keep its archives on another host than its own; depcurfew knows that of WPackagist (https://wpackagist.orgservesdownloads.wordpress.org), and for another one, such aspackages.drupal.orgwith its archives onftp.drupal.org, the warning names the declared repositories and says how to check that one of them serves those archives, sincecomposer.lockdoes not say which repository a package comes from.doctor --osvlooks up every release ofcomposer.lock, also one from such a host and one Composer checks out of its source, but not a package of a VCS repository ofcomposer.json, whose code is not the release on Packagist. - Edited lockfile entries. The rule
lockfile-tamperedreports a package ofcomposer.lock(packagesandpackages-dev) whosetransport-optionshasprevent_url_access_callableorprevent_ip_access_callable, and a Composer or VCS repository ofcomposer.jsonthat has one in itsoptions, or a package a package repository defines that has one in itstransport-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 editedcomposer.lockcan run a command duringcomposer install(GHSA-jhfp-4ppm-crmm); Composer 2.11.2 no longer takes them fromcomposer.lock. It is an error with or withoutdepcurfew.json, whatever the Composer version, which depcurfew cannot tell: use Composer 2.11.2 or later before anycomposer installorupdateon such a lockfile, then remove the option or restore the lockfile, and find out who changed it. Withoutdepcurfew.json, acomposer.lockthat 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-weakensreports"policy": false, acooldownorblockturned off, malware blocking turned off or limited to updates, a repository with"require-published-time": falseor"filter": false,"allow-plugins": true(or a key that matches every plugin,"*"or"*/*") while the policy denies plugins,"secure-http": false,"disable-tls": true, andCOMPOSER_POLICY=0,COMPOSER_NO_BLOCKINGorCOMPOSER_NO_SECURITY_BLOCKINGset to1,true,on,falseoroff(any other value makes Composer stop with an error), a shorterCOMPOSER_POLICY_COOLDOWN_PERIOD,COMPOSER_POLICY_COOLDOWN_BLOCK=0orCOMPOSER_POLICY_MALWARE_BLOCK=0in theenvof a CI workflow (of the workflow, a job, its container or a step), exported or set before a command in arunstep, written to$GITHUB_ENV(which the later steps of the job get), or set withENVor as the default of anARGin a Dockerfile stage (Docker givesRUNthe build arguments as variables, underENV). It also reportscomposer config policy.cooldown falseand the other settings above written bycomposer configin arunstep or aRUNinstruction (Composer takestrue,false,1and0there and writes the setting intocomposer.jsonfor the commands after it), and--no-cooldown,--no-blocking,--no-security-blockingor--no-secure-httpon acomposercommand there. - Bots.
applygives Dependabotcomposerentries the cooldown and thecomposer:exceptions, and Renovate a rule for them limited to thepackagistdatasource. Dependabot counts its composer cooldown from the release time the package author writes and updatescomposer.lockwith a Composer without the cooldown, sodoctorgives each such entry a note; Renovate counts frompublished-time. - Checking before an install.
depcurfew check composer:vendor/name(alsocomposer:vendor/name:^3.9, ascomposer requiretakes it), the Claude Code hook (composer require,composer updateof named packages,composer global require,composer create-project) andcheck_packagesofdepcurfew mcplook Composer packages up on Packagist: see Guarding installs. - Not covered.
composer config --global(or--file) in CI writesCOMPOSER_HOME/config.jsonor another file, whichdoctordoes not follow.COMPOSER_HOME/config.jsonandCOMPOSER_HOME/composer.json(Composer's configuration for the whole machine and its global project) are not read or written, so a repository, aminimum-stabilityor apreferred-installthey set is not seen bydoctor,checkor the hook; the hook asks about a command that setsCOMPOSER_HOMEitself, and aboutcomposer global updateof a package named without a constraint, whose constraint is inCOMPOSER_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
| Tool | Release-age delay | Exceptions | Install-script allow-list (Python: packages built from source; Composer: plugins) | Dependency sources | Verified |
|---|---|---|---|---|---|
| Composer | config. in composer.json (duration string), since 2.11.0 | config., since 2.11.0 | config. in composer.json | none | 2026-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.