Many teams keep private packages with plain names such as company-utils on an internal registry. If the package manager is set up to look at both the internal and the public registry, it has to choose when the same name exists in both. OWASP's npm guidance describes the attack: the attacker publishes a package with the same name on the public registry with a higher version number, and the tool picks that one. Installing it runs the attacker's code on developer laptops and build servers.
The attacker only needs to learn your internal package names, and those leak easily: from a package.json in a public repository, from a JavaScript bundle or source map, or from an error message. npm's own documentation states that the registry is not able to detect dependency confusion attacks by itself, so the defence has to be on your side.
The reliable fix on npm is to use a scope. Publish internal packages as @yourcompany/name, register that scope on the public registry so nobody else can claim it, and map the scope to your private registry in .npmrc. Commit a lockfile and install with npm ci so versions and sources cannot change silently. Be careful when an AI assistant suggests installing a package by name: confirm that it resolves to the registry and publisher you expect.
@yourcompany:registry=https://npm.pkg.github.com