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:
| Option | What it does |
|---|---|
| canonical set to false | Composer keeps searching other repositories even when this one has the package |
| only | Limits the repository to the listed package names, wildcards allowed |
| exclude | Blocks 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
- List every internal vendor name in use.
- Add the allow-list option to the private repository and the exclude option to any public one.
- Run a clean install and confirm all internal packages resolve.
- Commit composer.lock and review changes to it like code.
- 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.