Safeguard
Supply Chain•

How do you keep Composer from pulling internal packages from Packagist?

Composer 2 treats repositories as canonical by default. How the allow-list and exclude options keep internal vendor names off the public registry.

Safeguard Security Team
3 min read

Declare your private repository above Packagist, and use the filter options to say which package names each repository may serve. Composer 2 checks repositories top to bottom and stops at the first one that has a package, which already helps. Filters make the intent explicit and protect you if someone reorders the file.

How Composer chooses a repository

The Composer documentation says packages are looked up from repositories from top to bottom, and by default Composer stops looking once one repository has the package. The repository priorities article adds that in Composer 2 all repositories are canonical, with packagist.org added last unless you disable it. Composer 1 behaved differently, so check your version.

Repositories are also scoped to the root package. A repository declared in a dependency is not loaded, which means the root project's composer.json is the one place that decides.

The three options

The priorities article describes three options you can add to a repository entry:

OptionWhat it does
canonical set to falseComposer keeps searching other repositories even when this one has the package
onlyLimits the repository to the listed package names, wildcards allowed
excludeBlocks listed package names from that repository

Both of the list options take arrays of package names, and wildcards match any characters.

A setup for a team with one private repository

{
  "repositories": [
    {
      "type": "composer",
      "url": "https://packages.example.internal",
      "only": ["acme/*"]
    },
    {
      "type": "composer",
      "url": "https://repo.packagist.org",
      "exclude": ["acme/*"]
    }
  ]
}

The private repository can serve only names under acme. The public one is told never to serve that vendor namespace. An attacker who registers an acme package on the public registry gains nothing, because Composer will not accept it from there.

If you want no public registry at all, the docs show how to disable it for one project in composer.json, or for every project through the global config.

Steps to adopt this

  1. List every internal vendor name in use.
  2. Add the allow-list option to the private repository and the exclude option to any public one.
  3. Run a clean install and confirm all internal packages resolve.
  4. Commit composer.lock and review changes to it like code.
  5. Add a note in the repository readme explaining why the filters exist, so nobody removes them as clutter.

Where Safeguard fits

Safeguard's SBOM and dependency scanning lists the PHP packages a project resolves, which makes an unexpected vendor name easy to spot. Try a free scan at gold.safeguard.sh/scan.

Try it yourself

Click through it at your own pace, or open it full screen.

Scan the inventory for vendor names you recognize as internal and confirm they are not duplicated.

Quick check

Does the order of repositories still matter with filters?

Yes, since Composer checks top to bottom. Filters add a second layer, so a reorder does not open a gap.

Can a dependency add its own repository to my project?

No. The docs say repositories are available to the root package, so those in dependencies are not loaded.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.