depcurfew v1.6.1
On this page

Supported tools

What depcurfew configures and checks for each tool, from its knowledge base, where every value links to the official documentation or changelog it was verified against.

ToolRelease-age delayExceptionsInstall-script allow-list (Python: packages built from source; Composer: plugins)Dependency sourcesVerified
npmmin-release-age in .npmrc (days), since 11.10.0min-release-age-exclude, since 11.17.0allowScripts in package.jsongit: allow-git
remote tarball: allow-remote
local tarball: allow-file
local directory: allow-directory
2026-10-28 – 2026-11-11
pnpmminimumReleaseAge in pnpm-workspace.yaml (minutes), since 10.16.0minimumReleaseAgeExclude, since 10.16.0allowBuilds in pnpm-workspace.yamlgit: blockExoticSubdeps
remote tarball: blockExoticSubdeps
2026-10-28 – 2026-11-09
Yarn (Berry)npmMinimalAgeGate in .yarnrc.yml (minutes), since 4.10.2npmPreapprovedPackages, since 4.10.0dependenciesMeta.<package>.built in package.jsongit: approvedGitRepositories2026-10-28 – 2026-11-03
Buninstall.minimumReleaseAge in bunfig.toml (seconds), since 1.3.0install.minimumReleaseAgeExcludes, since 1.3.0trustedDependencies in package.jsonnone2026-10-28 – 2026-11-11
DenominimumDependencyAge in deno.json (minutes) or min-release-age in .npmrc (days), since 2.5.5 / 2.6.0 ⚠️minimumDependencyAge.exclude, since 2.5.5allowScripts in deno.jsonnone2026-10-28 – 2026-11-11
uvexclude-newer in uv.toml, pyproject.toml (duration string), since 0.9.17exclude-newer-package, since 0.9.25no-binary-package in pyproject.tomlnone2026-10-30 – 2026-11-09
pipuploaded-prior-to (days) on the command line or in the environment, no project file, since 26.1.0none--no-binary in requirements.txtnone2026-10-30 – 2026-11-11
Poetrysolver.min-release-age in poetry.toml (days), since 2.4.0solver.min-release-age-exclude, since 2.4.0installer.no-binary in poetry.tomlnone2026-10-30 – 2026-11-03
PDMexclude-newer in pyproject.toml (duration string), since 2.26.9exclude-newer-override, since 2.29.1no-binary in pyproject.tomlnone2026-10-30 – 2026-11-03
Pipenvcool-down-period in Pipfile (duration string), since 2026.6.2nonenonenone2026-10-30 – 2026-11-11
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
Bundlercooldown in Gemfile, gems.rb (days), since 4.0.13nonenonenone2026-11-03
Cargoregistry.global-min-publish-age in .cargo/config.toml (duration string), since 1.100.0nonenonenone2026-11-12 – 2026-11-13
RenovateminimumReleaseAge in renovate.json, renovate.jsonc … (duration string), since 35.46.0packageRules: matchPackageNames + minimumReleaseAge: null, since 35.46.0 ❓nonen/a2026-10-28 – 2026-11-03
Dependabotcooldown in .github/dependabot.yml, .github/dependabot.yaml (days), since 2025-07-01cooldown.exclude, since 2025-07-01 ❓nonen/a2026-10-28 – 2026-11-03
Cargo (bots only)registry.global-min-publish-age in .cargo/config.toml, since Cargo 1.100.0 (depcurfew does not write it) ❓; the delay comes from the bots: Dependabot cargo, Renovate cargon/an/an/a2026-11-03
Go modules (bots only)none ❓; the delay comes from the bots: Dependabot gomod, Renovate gomodn/an/an/a2026-11-03
Maven (bots only)none ❓; the delay comes from the bots: Dependabot maven, Renovate mavenn/an/an/a2026-11-03
Gradle (bots only)none ❓; the delay comes from the bots: Dependabot gradle, Renovate gradlen/an/an/a2026-11-03
NuGet (bots only)minPublishAgeHours in nuget.config, since .NET SDK 11.0.100, only for the versions dotnet package add and dotnet package update choose (depcurfew does not write it); the delay comes from the bots: Dependabot nuget, Renovate nugetn/an/an/a2026-11-03 – 2026-11-11

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

Yarn Classic (1.x) is detected but not supported: apply writes nothing for it. doctor still looks up the versions its yarn.lock locks for known malicious versions (known-malicious, with --osv or the local data of depcurfew osv update): the registry tarball of each entry's resolved, which Yarn Classic fetches, read as the parser of Yarn 1.22 reads the file.

The Yarn that runs. Every Yarn from 1.18 runs the release that yarnPath in .yarnrc.yml names instead of itself, with or without Corepack, so a yarnPath to a release in the repository named yarn-<version>.cjs, as yarn set version names it, is the version of the project and wins over packageManager. apply writes only the settings that release supports, so a Yarn 3 project gets no release-age delay and no allow-list of git repositories, settings Yarn 3.6.4 would stop at as unknown, and its version-too-old points to yarnPath and names yarn set version. doctor checks that release (manager-version-too-old and the other rules), and setting-ignored-by-version warns when packageManager pins another release, which then has no effect. A yarnPath whose file name gives no version, a path outside the repository or a missing file leaves the version as before: packageManager, or unknown.

pnpm: include, pnpmfiles and plugins. From pnpm 12.15, include in pnpm-workspace.yaml can give settings to local YAML files and to config dependencies, each setting with one owner. depcurfew reads every setting where pnpm takes it from: apply writes it into the file that owns it (the file whose list names it, also when the file does not set it yet; a '*' file that sets it; otherwise pnpm-workspace.yaml) and never into a second place, which pnpm rejects with ERR_PNPM_DUPLICATE_SETTING, and doctor reports a weakening setting in such a file with its file and line. A setting a config dependency owns (or a file outside the directory) can be neither read nor written: apply says so (cooldown-not-written, setting-owned-elsewhere) and doctor warns (pnpm-config-unverifiable), as it does for an include pnpm rejects. The updateConfig hook of a pnpmfile (.pnpmfile.mjs, .pnpmfile.cjs, or the files the pnpmfile and globalPnpmfile settings name) and of the pnpmfile pnpm loads by itself from a config dependency named pnpm-plugin-*, @*/pnpm-plugin-* or @pnpm/plugin-* can change every setting after pnpm has read its files. depcurfew does not run them, so doctor warns about each one (an error for a plugin the knowledge base knows to set a shorter delay than the policy, such as @pnpm/plugin-better-defaults 0.3.0) and explain lists them (pnpm-hook). ignoreScripts does not stop them; ignorePnpmfile: true does, and pnpmfile: [] keeps pnpm from loading a new .pnpmfile.mjs by itself. Versions before 12.15 read pnpm-workspace.yaml alone, as before.

Tarballs of private npm registries. In package-lock.json, a tarball URL counts as a registry package (for lockfile-forbidden-source and known-malicious) when it is in npm's layout, in the layout of GitHub Packages (https://npm.pkg.github.com/download/@scope/name/<version>/<digest>) or of GitLab and Artifactory (…/@scope/name/-/@scope/name-<version>.tgz), or below the registry that the .npmrc next to package-lock.json sets for the package, which npm 12 installs as a registry package too; a dependency that asks for it by URL makes it a tarball URL all the same. A tarball URL in another layout that package.json asks for by version stays an error, and its fix says to set that registry in .npmrc, not to allow tarball URLs.

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, [tools] of mise.toml (also .mise.toml, mise/config.toml, .mise/config.toml, .config/mise.toml, .config/mise/mise.toml and .config/mise/config.toml, the first file that sets a tool winning, as mise reads them; a list gives its first entry, a table its version), actions/setup-node in workflows (with node-version-file, in whose mise.toml it reads tools.node, and matrix values, also inside more text, ${{ matrix.node }}.x, or as a property of the matrix objects, ${{ matrix.cfg.node }}, with the values include adds and those exclude removes), jdx/mise-action (the Node.js of the mise configuration of its working directory or of its mise_toml input, unless install: false), 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. npm installs the newest release of a range that runs on the Node.js there, so a range (npm@11, npm@^11.10.0), npm@latest and a bare npm count as the newest such release depcurfew knows (a warning when it lacks a setting, since a newer release may be out), never as the lowest version of the range; npm@${NPM_VERSION} takes the value of NPM_VERSION in the env of the workflow, the job and the step, or in the ENV and ARG of the stage; a spec depcurfew still cannot read (npm@next, a variable without a value, a range with none of the releases it knows) counts as an npm that runs every dependency install script. apply names the places that run an npm without a setting it writes for the release-age delay or the dependency sources (version-in-use-too-old), from the same list as doctor: .nvmrc with 24 can come with npm 11.3.0, which has no release-age delay. 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 pip.conf of the repository that PIP_CONFIG_FILE names is read where it does, so python-config-ignored does not report it: in a CI job as its path from the workspace (pip.conf, ${{ github.workspace }}/pip.conf), in a Dockerfile stage as a path with its name in a stage that copies it (COPY pip.conf ./ with ENV PIP_CONFIG_FILE=/app/pip.conf). 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. corepack-unavailable reports a packageManager or devEngines.packageManager pin of Yarn or npm, which only Corepack reads, where a declared Node.js is 25 or later: Node.js 25.0.0 stopped distributing Corepack, so the pin has no effect there until Corepack is installed (npm install -g corepack, then corepack enable, or corepack enable npm for npm). pnpm 10 and later switches to its pinned version itself and npm checks devEngines.packageManager itself, so those pins are not reported, and neither is a CI job or Dockerfile stage that installs or runs Corepack, or installs the pinned npm release itself (npm install -g npm@<version>), before it installs with the pinned tool; one that installs another npm release first gets the warning with that release named. depcurfew explain lists what it found.

Settings that CI and Dockerfiles turn off. Package managers take settings from the command line and from environment variables over their configuration files, so a workflow or a Dockerfile can undo what apply wrote for every install it runs. The rule ci-weakens-policy reads the variables of the env of a workflow, a job, its container and a step, export NAME=value and NAME=value before a command in a run step, NAME=value lines a step writes to $GITHUB_ENV with echo or printf (which the later steps of the job get, over the env of the workflow and the job and under that of a step), ENV and the defaults of ARG in a Dockerfile stage (an ARG before the first FROM only where a stage declares it again), and the options on the commands of run steps and RUN instructions, for npm, pnpm, Yarn, Bun, Deno, uv, Poetry and PDM: NPM_CONFIG_MIN_RELEASE_AGE=0 (in any case), npm_config_strict_allow_scripts=false where .npmrc sets it, npm_config_allow_git=all or root while the policy denies git sources, pnpm_config_minimum_release_age=0, YARN_ENABLE_SCRIPTS=true, UV_EXCLUDE_NEWER with a shorter delay, POETRY_SOLVER_MIN_RELEASE_AGE=0, and npm install --min-release-age=0 --allow-git=all --no-strict-allow-scripts, pnpm install --config.minimum-release-age=0 --config.dangerously-allow-all-builds=true, uv lock --exclude-newer "1 hour" or pdm lock --exclude-newer 1h. Each one is an error at its line, with a fix; a value with $ or ${{ }}, and a delay depcurfew cannot compare (a date), give a warning. The options and variables come from the knowledge base (those the install guard asks about, the option of each tool's delay, and for npm the settings of install scripts and sources it writes into .npmrc); npm_config_ignore_scripts=false and --ignore-scripts=false are not reported, since apply never writes ignore-scripts. pip has python-config-ignored, Composer composer-config-weakens and Bundler bundler-config-weakens, which read the same places. Not read: a run step in PowerShell ($env:NAME), { ...; } >> "$GITHUB_ENV" and multiline values (NAME<<EOF), npm config set, pnpm config set, yarn config set, poetry config and pdm config in CI, abbreviated options (--min-rel=0) and the commands of a script that sh -c runs.

Python projects

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.

  • 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.
  • 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. Ruby gems, JSR packages, Rust crates and NuGet packages have no names on these lists, since no setting depcurfew writes takes one: an entry such as "gem:rails" or "cargo:serde" is an error that says to remove it, and an entry with another prefix is an error that lists the prefixes.
  • Install scripts are builds. A wheel installs without a build step, so no build code runs; a source distribution, a git repository or a local directory is built, which runs its build code. Forbidding builds does not keep an installed package's own code from running later, whenever Python imports it. 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.
  • 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. pip reads the options of the files a requirements file includes with -r and -c, and the options after a requirement on its line, into the same install, so an option there that makes pip build packages from source again (a package name or :all: for building from source, --only-binary=:none:) undoes --only-binary=:all:: apply comments such option lines out in the included files inside the repository (they come back when the policy stops forbidding builds), and warns with the file and line where it cannot (build-switch-undone for an option after a requirement or next to other options, requirements-include-unread for a URL, a path outside the repository or a missing file). doctor reports them (config-drift, and unreadable-config for an include it cannot read), and follows the options in pip's order when it decides whether building is forbidden.
  • 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.
  • PDM and Pipenv have no setting that pins their own version. Where nothing pins it, apply configures for the latest version depcurfew knows and names that version (version-not-pinned). What CI jobs and Dockerfiles install counts as a version, and doctor checks it (manager-version-too-old): the version input of pdm-project/setup-pdm (head names none); pdm==<version>, pipenv==<version>, uv==<version> and poetry==<version> in pip install, pipx install or uv tool install, with a variable read from the env of the job or the ENV and ARG of the stage (pip install "poetry==${POETRY_VERSION}"); and the uv of the official uv image a Dockerfile copies from or starts from (COPY --from=ghcr.io/astral-sh/uv:0.8.22 /uv /uvx /bin/, FROM ghcr.io/astral-sh/uv:0.8.22-python3.12-bookworm; latest and a base name alone name no version). 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.
  • 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.
  • Running depcurfew. depcurfew is a Node.js program; a Python project without package.json runs it as In a repository without package.json describes.

Update bots

apply gives Renovate and Dependabot the policy's delay, rounded up to their unit and at least as long as the delay of every package manager after rounding, so that they never propose a version the package managers would refuse. It edits only bot configuration files that exist and adds no entries to them.

  • Dependabot gets cooldown in each updates entry of an ecosystem depcurfew configures (npm, bun, deno, pip, uv, composer, bundler), with the policy's exceptions for that registry under exclude (a bundler entry keeps its own, since the policy names no gems), and in the entries of the ecosystems updated by bots only (below). An include list in such a cooldown stays, but it limits the cooldown to the dependencies it names, so apply warns (bot-rule-shortens-delay, with the entry and the line) unless it is "*"; so it does when an entry keeps its own exclude (actions, gems, the ecosystems updated by bots only) and that list matches every dependency ("*"), since Dependabot then waits for none of them. doctor reports both as before (bot-cooldown-mismatch, broad-exclusion).
  • GitHub Actions. When the repository has workflows in .github/workflows and targets is "auto" or names the bot, the actions they run wait as long as the packages. The github-actions entries of Dependabot get the same default-days (GitHub Actions reads no semver-*-days), and their exclude stays as written, because the policy's exceptions name packages, not actions. The delay at the top level of the Renovate configuration covers its github-actions manager too. doctor (bot-cooldown-mismatch) compares the bot's delay for the actions with cooldown.minimum.
  • Cargo, Go modules, Maven, Gradle and NuGet are updated by bots only. Their package managers have no release-age delay that depcurfew configures (the rows "bots only" in Supported tools say what each has), so depcurfew delays their new versions through the bots alone. When depcurfew.json has a cooldown and targets is "auto" or names the bot, the Dependabot entries cargo, gomod, maven, gradle and nuget get the same default-days as the packages, lower semver-*-days are raised to it, and their exclude stays as written, because the policy's exceptions name npm, Python and Composer packages. The delay at the top level of the Renovate configuration covers its cargo, gomod, maven, gradle and nuget managers; packageRules entries for them by manager, category (rust, golang, java, dotnet) or datasource (crate, go, maven, nuget) count as for the package managers, and the preset security:minimumReleaseAgeCrate gives Cargo dependencies 3 days. doctor (bot-cooldown-mismatch) compares the bot's delay for them with cooldown.minimum, and gives one note per ecosystem when the repository has their files but no update bot, since nothing delays their new versions then. depcurfew looks for these files (Cargo.toml, go.mod, pom.xml, settings.gradle, a .csproj…) in the root, in its direct subdirectories that are not hidden, in workspace packages and in the directories listed in projects; depcurfew explain lists what it found under "Updated by bots only". Other dependencies that come in with the bot's update of a lockfile (Cargo.lock, go.sum…) are not held back, because the package manager has no delay of its own to apply to them.
  • What NuGet's minPublishAgeHours covers. From .NET SDK 11.0.100 (NuGet 8.0, Visual Studio 18.12) on, the attribute minPublishAgeHours of a source in nuget.config holds back new versions only where dotnet package add and dotnet package update choose the version themselves; the 10.0 SDKs and earlier ignore it, and depcurfew does not write it. The NuGet documentation says that an explicit version bypasses it, that restore does not check it (for transitive dependencies too), and that with an age set, restore fails with NU1020 for a floating version such as 1.2.*. The Visual Studio Package Manager UI and Package Manager Console, dotnet package list and dotnet package search are not documented as covered (their NuGet issues are open), nor are dotnet tool, dnx and dotnet new install. check, the Claude Code hook and depcurfew mcp do not look NuGet packages up, so depcurfew delays NuGet dependencies only through the bots.
  • Renovate gets the delay at the top level of its configuration and a packageRules entry for the exceptions of each registry, limited to its datasource: matchDatasources: ["npm"] for npm names, ["pypi"] for pypi: names and ["packagist"] for composer: names, so an exception never reaches a crate, a Go module, a gem or an action of the same name. A built-in preset of extends that gives a datasource in use a shorter delay than the policy (config:best-practices and security:minimumReleaseAgeNpm 3 days for npm, security:minimumReleaseAgePypi for pypi, security:minimumReleaseAgeCrate for crate) wins over the top-level value, since a matching rule wins over the top level; apply then writes a rule with that datasource, matchUpdateTypes: ["major", "minor", "patch"] and the policy's delay at the front of packageRules, which Renovate applies after the rules of the presets and before the repository's own rules and the exception rules, and removes it when the preset goes or no longer waits less. The deprecated renovate key of package.json and .renovaterc get it in the snippet. doctor reports a rule of the repository that gives npm, Python or Composer packages of some names a shorter delay than cooldown.minimum, or none, while the policy's exceptions do not exempt them (broad-exclusion, at the line of the rule), and a rule an earlier apply wrote without matchDatasources: ["npm"]. apply and doctor read the other packageRules entries the way Renovate matches them, also by matchManagers and matchCategories: npm covers the packages of npm, pnpm and Yarn, bun and deno those of Bun and Deno, pep621 those of uv and PDM, poetry, pip_requirements and pipenv those of Poetry, pip and Pipenv, composer those of Composer, bundler those of Bundler, and github-actions the actions of the workflows; the categories are js, python, php, ruby and ci. Globs, /regex/, ! and a single string instead of a list work as in Renovate. A rule that shortens the delay of a package manager in use (or of the actions) gives bot-rule-shortens-delay in apply and bot-cooldown-mismatch in doctor, and one that turns it off gives broad-exclusion. For the actions, a rule with matchFileNames that matches every workflow file of the repository (read as Renovate reads it: * within one directory, ** across them, any case, or /regex/) and a rule with matchDepTypes that matches action, the depType Renovate gives an action of a workflow step, count too; one that matches only some workflow files, or only reusable workflows (workflow), leaves the other actions their delay and does not count. A rule with a glob matcher depcurfew cannot read (braces, character classes, ?, or extglobs such as @(a|b) and +(…)) does not count, and doctor gives a note when it may set a shorter delay. depcurfew does not run a /regex/ that a crafted file could make take minutes: one with a quantified group, a backreference, a lookaround, more than one *, + or {n,}, or more than 256 ways to match through ?, {n,m} and | (16 next to a *, + or {n,}), nor the patterns of a file past a fixed amount of matching work. Such a pattern counts as matching where the rule shortens or turns off the delay, so bot-cooldown-mismatch, broad-exclusion and bot-rule-shortens-delay report the rule and name the pattern; where the rule would make the delay longer, it does not count; so does a rule with a matcher depcurfew does not evaluate (matchBaseBranches, matchJsonata, matchRepositories, matchCurrentAge, and matchFileNames or matchDepTypes for the packages of a package manager), unless a package matcher already limits it to some packages. When rules give the update types different delays, doctor reports each one that is shorter, not only the shortest.
  • Numbers in dependabot.yml are read as Dependabot's YAML library, Ruby's Psych, reads them, which follows YAML 1.1: default-days: 010 is 8 days (octal), 1_0 and 1,0 are 10, 1:30 is 5400, and 0o12 is text. doctor compares the number Dependabot reads with the policy and says how it read it ("default-days: 010 in .github/dependabot.yml, which Dependabot reads as 8"), and apply writes the policy's number in place of one Dependabot reads otherwise. Words are read the same way: yes, no, on and off in any case are booleans and ~ and null in any case are null, so apply writes an exception such as yes, off, 1_0 or 2024-01-01 in double quotes (- "yes"), which Dependabot reads as the name, and treats an unquoted - yes in the file as the boolean it is.
  • YAML aliases. Dependabot does not accept YAML aliases (*name) in .github/dependabot.yml (dependabot-core loads the file without them), so doctor does not read a delay or an exception through one: bot-cooldown-mismatch gives a note at the first alias instead of a result, apply writes the cooldown of an entry out in place of an alias and warns that Dependabot reads nothing from the file (bot-config-unreadable), and init notes it, since it does not keep a longer delay from such a file in cooldown.minimum. GitHub Actions workflows may use aliases: doctor and detection read the env maps, jobs and steps of a workflow through them, at most 100 aliases per file.
  • How old an action is comes from git. Renovate takes the commit date of a tag, the tagger date of an annotated tag, or the date of a GitHub release of that version when that is newer, and holds an update without a date; Dependabot takes the date of the GitHub release, then of the tag, then of the commit, and lets an update without a date through. Whoever makes a commit or a tag sets its git date, so the delay protects actions less than packages from a registry, which records when a version was published.

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.

Ruby projects

depcurfew configures Bundler (Ruby gems from rubygems.org) from the same depcurfew.json, and the bots for gems: Dependabot bundler entries and Renovate.

  • Detection. A Gemfile, gems.rb, Gemfile.lock or gems.locked in the directory means Bundler; with both gems.rb and Gemfile, Bundler reads gems.rb, and so does depcurfew (only a regular file counts, as for Bundler: a directory named gems.rb does not). The version is the one under BUNDLED WITH at the end of the lockfile, which Bundler installs and switches to. The bundler input of ruby/setup-ruby (a version; Gemfile.lock, default, latest and none name none), gem install bundler -v <version> (also -v<version>, --version=<version> or bundler:<version>) and bundle _<version>_ install in a CI job or a Dockerfile count as versions too. depcurfew explain lists what it found.
  • The release-age delay is the cooldown: of the source. Bundler 4.0.13 and later hold back gem versions younger than the cooldown: days of the source call that serves them. apply writes the policy's delay in whole days, rounded up ("36h" becomes cooldown: 2), into source "https://rubygems.org" of the Gemfile, which is committed, so every developer and CI run get it. It edits only a plain call with a literal URL on a line of its own that starts a statement at the top level of the Gemfile, outside any block, method, class, condition, loop, lambda or begin (with or without parentheses, with one existing cooldown: or :cooldown =>, and with a comment after it), and keeps the rest of the line and of the file as they are. The cooldown: is read as Ruby reads an integer (010 is 8 days, 0x10 16).
  • The Gemfile is read as Ruby reads it. Bundler runs the Gemfile as Ruby code, so depcurfew reads it as Ruby's lexer does: comments, lines between =begin and =end, everything after __END__ (or after a NUL, ^D or ^Z outside strings and comments), strings and %q, %w, %(...) and other literals over several lines with any delimiter, and the bodies of heredocs (also <<~"END-X", which ends only at a line that is exactly END-X) are no calls. \r\n is one newline, for apply and doctor alike, as for Ruby. Where Ruby's reading depends on something only Ruby knows, depcurfew stops: after a name that may be a local variable or a method, platforms %w[ruby] is a %w literal or a modulo, and so with /, <<, ? and : after such a name and a space; also alias, undef and syntax errors (an end or ) without its opening, a do or if without its end). It stops as well where Ruby 3.4 and newer (Prism, the default parser since 3.4) and Ruby 3.3 and older (parse.y) read the same text differently: ? before a character beyond ASCII (?€or is a character literal for one and a ternary for the other), =begin followed by ^D or ^Z, and $0 followed by a name. Such a Gemfile is not edited at all (cooldown-not-written with the line and what to change there, such as platforms(%w[ruby])), and doctor does not count a cooldown in it.
  • What apply does not edit is reported. A source with a block, a modifier (if, unless), other options, a URL depcurfew cannot read or a call over several lines gives cooldown-not-written with its line, and so does a Gemfile without source "https://rubygems.org". So do a source inside a block, method, class, condition, loop, lambda or begin, which Ruby runs only when that code runs, a cooldown: given twice (Ruby keeps the last of two equal keys, and Bundler takes a symbol key over a string key), a cooldown: whose value goes on (cooldown: 7 - 7, or a next line that starts with &&, ||, and or or, which Ruby 4.0 joins to the call as it joins .method), and a source whose URL depcurfew cannot read also next to a plain call; doctor reports the same place as an error of config-drift, and as a warning when the call already has a cooldown: at least as long as the policy, since depcurfew cannot tell whether Ruby runs it. The fix never asks for a shorter cooldown:. The Gemfile has no markers, so a longer cooldown: of a plain call written by hand is replaced (cooldown-shortened). Other gem servers keep their own cooldown:; cooldown: 0 exempts a private one, as Bundler documents.
  • Bundler 4.0.14 for projects with a lockfile. Bundler 4.0.13 loses the cooldown: of a Gemfile source once Gemfile.lock exists, which 4.0.14 fixed; older versions accept the keyword and ignore it without a word. apply writes nothing for an older pinned version (version-too-old, which names bundle update --bundler), and doctor reports BUNDLED WITH and the CI versions below what the project needs (manager-version-too-old).
  • What the delay covers. Bundler applies it when it picks versions: bundle install without a lockfile or for gems added to the Gemfile, bundle update, bundle lock --update and bundle add. Versions already in Gemfile.lock install as they are, also younger ones, so review lockfile changes as before. A version without a publication date (a gem server without the v2 compact index) is not held back. When every version that fits is too new, Bundler stops and names --cooldown 0.
  • .bundle/config is not written. Bundler's cooldown setting (bundle config set cooldown N, BUNDLE_COOLDOWN) applies to every source and takes the place of the Gemfile's, so it would also end the exemption of a private server, and .bundle/config is usually local and git-ignored (bundle gem ignores /.bundle/). apply leaves it alone. The rule bundler-config-weakens reports BUNDLE_COOLDOWN with fewer days than the policy, or 0, in .bundle/config (which Bundler reads before the environment), in the env of a CI workflow, written to $GITHUB_ENV by an earlier step of the job, before a bundle command, in a Dockerfile ENV or as the default of a Dockerfile ARG (which RUN gets as a variable, under ENV), bundle config set cooldown N (with --local or --global, and the older bundle config cooldown N) in a run step or a RUN instruction, which writes the setting for the bundle commands after it, and a shorter --cooldown on bundle install, update, lock, add or cache in a CI job or a Dockerfile, also as Bundler's CLI reads them: bundle i, bundle package and bundle pack, an unambiguous prefix such as bundle inst, a bare bundle, which installs, and the hidden bundle install_or_cli_help (also install-or-cli-help), which runs install. .bundle/config is read as Bundler reads it, line by line and not as YAML: quotes stay unless they enclose the whole value (BUNDLE_COOLDOWN: "14" # two weeks is the text "14", so 0), # starts a comment, the last line of a key counts, and only a key spelled exactly BUNDLE_COOLDOWN at the top level is the setting. Bundler reads the setting with to_i, so a value that does not start with a digit (an empty one, ~, true, 0x10) turns the delay off and 1e3 is 1 day; a number in a workflow env is read as written.
  • No exceptions for gems. Bundler has no per-gem exception, and the policy's package lists have no prefix for Ruby gems: the cooldown exceptions of the policy stay npm, Python and Composer names. When the policy has exceptions, apply says that they do not apply to Bundler (exclude-unsupported). To take a newer version of one gem sooner, review it and run bundle update <gem> --cooldown 0 --conservative.
  • Install scripts are native extensions. Bundler builds the native extensions of a gem (its extconf.rb, Rakefile, CMakeLists.txt or Cargo.toml) when it installs it, which runs code of the gem, and has no setting that stops this or allows it for some gems only. With installScripts.default: "deny", apply and explain say that it cannot be enforced for Bundler (scripts-not-enforceable).
  • No switch for dependency sources. A Gemfile can name git sources (git:, github:) and paths (path:), and Bundler has no setting that forbids them (source-not-enforceable); doctor reads only the version of Gemfile.lock, not its gems and sources, so review such sources by hand.
  • Bots. apply gives Dependabot bundler entries the cooldown (their own exclude stays) and Renovate the delay at the top level, which covers its bundler manager. The Renovate rule for the policy's npm exceptions is limited to the npm datasource, so an npm name does not exempt a gem of the same name. bot-cooldown-mismatch compares the bots' delay for gems with the cooldown: of the Gemfile, as for the other package managers; Renovate rules by manager bundler, category ruby or datasource rubygems count. Renovate updates Gemfile, not gems.rb.
  • Not covered. check, the Claude Code hook and depcurfew mcp do not look gems up on rubygems.org (gem install, bundle add). doctor does not check the gems of Gemfile.lock (lockfile-forbidden-source, lockfile-install-scripts and known-malicious do not apply to it) and does not read ~/.bundle/config, BUNDLE_APP_CONFIG or BUNDLE_GEMFILE. The Bundler a ruby Docker image comes with is not read: Bundler switches to the version of BUNDLED WITH.