skip to content

In Composer, what does config.allow-plugins control, and why does a newly added plugin fail a non-interactive CI install?

level: seniorimportance: should knowfreq 40%

answer

  1. plugin code runs inside Composer
  2. default {} allows nothing
  3. interactive prompt: y, n, d
  4. PluginBlockedException when non-interactive
  5. false silences, true allows

basics

~10 s

allow-plugins lists which composer-plugin packages may execute code inside Composer. It defaults to empty, so an unlisted plugin triggers a prompt interactively and, with --no-interaction in CI, a blocked-plugin error that fails the command.

solid answer

~40 s

A Composer plugin is a package of type `composer-plugin` whose class Composer loads into its own process, where it can hook events, add commands and change how packages download. Since Composer 2.2, `config.allow-plugins` must name each plugin: keys are package names or patterns, values are `true` to run it or `false` to skip it quietly. The default is `{}`, so nothing runs until approved. Run interactively, Composer asks whether you trust the plugin and writes the answer into `composer.json`. Run with `--no-interaction`, as CI should, it throws a blocked-plugin error and the command fails, because silently skipping a plugin could leave a broken install. The fix is to commit the decision, for example `composer config --no-plugins allow-plugins.vendor/plugin true`. Setting `allow-plugins` to `true` for everything defeats the control.

code

json · 10 lines
json
{
    "config": {
        "allow-plugins": {
            "acme/legacy-installer": false,
            "acme/*": true,
            "phpstan/extension-installer": true,
            "php-http/discovery": false
        }
    }
}

go deeper

for a junior

Recall that some packages are Composer plugins, that allow-plugins in composer.json approves them, and that CI fails when a plugin is not listed.

for a middle

Explain the prompt versus the non-interactive error, the true, false and pattern forms, and why the first matching key decides.

for a senior

Treat each allow-plugins change as a review point for code that runs with CI credentials, prefer explicit false over true-for-all, and know the global config is separate.

for a principal

Set the rule for approving plugins across many repositories: who reviews them, where the list lives, and how it combines with advisory policy and registry controls.

## Why plugins need a gate Composer has two ways for code to run during `install` or `update`: - **Scripts** in the root `composer.json`. Only the root package's scripts run; scripts declared by dependencies are ignored. - **Plugins**: packages whose `type` is `composer-plugin`, whose `extra.class` names a class implementing `Composer\Plugin\PluginInterface`, and which require `composer-plugin-api`. Composer loads that class into **its own PHP process**. It can subscribe to events, register commands, change downloads or install packages into custom paths. A plugin can arrive transitively: requiring one library can pull in a plugin two levels down. Before Composer 2.2, every plugin in the dependency graph ran automatically. `allow-plugins` turns that into an explicit, reviewable decision recorded in the project. ## How the setting works `config.allow-plugins` defaults to `{}`, which allows **no** plugins. It accepts: | Value | Effect | |---|---| | `{"vendor/plugin": true}` | that plugin runs | | `{"vendor/plugin": false}` | that plugin is skipped, with no further prompt or error | | `{"my-org/*": true}` | a pattern: every matching package is allowed | | `false` | no plugin runs | | `true` | every plugin runs; the docs mark this as not recommended | Patterns are checked **in order**, and the first matching key decides, so a specific `false` belongs above a broad `true`. When Composer meets a plugin that no key matches: 1. **Interactive run**: it warns and asks `Do you trust "vendor/plugin" to execute code and wish to enable it now?`, with `y` (allow and write to `composer.json`), `n` (disallow and write it), `d` (discard for this run) and `?` (help). 2. **Non-interactive run** (`--no-interaction`, `-n`, or no TTY): it throws a `PluginBlockedException` saying the package *contains a Composer plugin which is blocked by your allow-plugins config*, and the command fails. Failing is deliberate. A plugin such as a custom installer decides where files go; skipping it silently would produce a `vendor/` directory that looks complete but is wrong. The error message itself suggests the fix: `composer config --no-plugins allow-plugins.vendor/plugin true` (or `false`). The `--no-plugins` flag there stops plugins from loading while the config is being edited. ## Operating it in a team - **Commit the decision.** The entry lives in `composer.json`, so it goes through code review like any other change that lets third-party code execute. - **Review what the plugin does** before answering `y`: it runs with the same rights as the developer or CI job running Composer, including any tokens in the environment. - **Use `false` for plugins you do not need.** A transitive plugin that you have decided against becomes `false`, which silences the prompt and the error. - **Keep it tidy.** `composer remove` drops the matching rule for a plugin it removes. When Composer writes a prompt answer and `sort-packages` is enabled, it re-sorts the keys, which can move `acme/*` above a specific `acme/legacy-installer: false`; check the order after such a write. - **Global plugins** installed with `composer global require` are checked against the **global** config's `allow-plugins`, not the project's. - **Very old lock files**: a `composer.lock` written before Composer 2.2 with no `allow-plugins` in `composer.json` falls back to a compatibility mode. Composer 2.10 now refuses it in non-interactive runs and asks you to run `composer update --lock` and add an explicit `allow-plugins` section. `--no-plugins` (a global option) disables all plugins for one run. It is useful for diagnosing a failure, but a project whose install depends on a plugin will not install correctly with it. ## Reviewing a new entry When a pull request adds a line to `allow-plugins`, the reviewer is approving code that will run on every developer machine and CI runner that installs the project. Useful questions: 1. Which direct dependency pulled the plugin in, and is that dependency still wanted? 2. What does the plugin do: install files to a custom path, register a command, change downloads? 3. Does the project work with the plugin set to `false`? If so, `false` is the safer answer. 4. Is the rule a single package name, or a pattern that will silently approve future packages from the same vendor? ## What it does not cover `allow-plugins` gates code that runs **inside Composer**. It does not vet what a library does at runtime when your application calls it, and it says nothing about known vulnerabilities; that is the job of `composer audit` and `config.policy`. It is one control for the install step, not a supply-chain strategy.

  • Why does Composer fail instead of just skipping an unlisted plugin in CI?
    Many plugins change the install itself, for example custom installers that decide where a package's files go. Silently skipping one could produce a `vendor/` directory that is incomplete or laid out wrongly, and nobody would notice until runtime. Failing makes the missing decision visible; `false` in `allow-plugins` is the explicit way to skip it.
  • Is setting allow-plugins to true acceptable in a project?
    It restores the pre-2.2 behaviour: every plugin anywhere in the dependency graph runs code inside Composer, including one added by a future transitive update. The docs mark it as not recommended. Listing each plugin keeps the approval reviewable, and a vendor pattern such as `my-org/*` covers your own packages.

saying these in an interview costs you the question

  • allow-plugins also decides which dependency scripts may run.
  • An unlisted plugin is skipped with a warning when running in CI.
  • The default allows plugins from Packagist and blocks private ones.
  • Setting allow-plugins to true is the recommended fix for CI failures.
  • allow-plugins checks packages for known security advisories.