The registry said 404, npm publish --dry-run passed, and the real publish still came back 403 Forbidden - Package name too similar to existing package: that is how a developer described it in npm CLI issue 9188 in April 2026, adding that by then a developer has usually named the repository and written the README.
Short answer: a package name is free only when four things hold. The registry has no package by that exact name, the name passes the registry's format rules, nothing already published matches it once the registry normalizes both names, and no reserved, blocked or too-similar name stands in the way. A lookup answers only the first question. Check the other three, and the GitHub name, domain and trademark that go with it, before anything public uses the name.
Two words come up throughout. Normalization is the rewrite a registry applies to a name before comparing it, such as lowercasing it or removing punctuation. Name squatting is registering a name only to hold it for later; npm, PyPI, crates.io and GitHub all forbid it.
What does a registry compare when you claim a name?
None of the three big registries compares your name letter for letter. Each one rewrites it first, and two names that end up the same after the rewrite count as one name.
npm
Removes the punctuation and compares what is left (npm blog, 2017).
You typejson.stream
It comparesjsonstream
jsonstream exists, so npm names json.stream and json_stream as names no one can publish; both answer 404 today.
PyPI
Lowercases the name and turns every run of ., - and _ into one - (name normalization spec).
You typeFriendly._Bard
It comparesfriendly-bard
The spec lists Friendly-Bard, friendly.bard and FrIeNdLy-._.-bArD as the same name.
crates.io
Detects collisions without regard to case and treats - and _ as the same (Cargo Book, registry index).
You typeserde-json
It comparesserde_json
Asking the crates.io API for serde-json returns the existing crate serde_json (checked 24 October 2026).
npm's similarity check goes further than punctuation. Its own guidelines ask that an unscoped name "is not spelled in a similar way to another package name" (npm Docs, package name guidelines), and the npm docs do not describe how the check works.
What are the name rules on npm, PyPI and crates.io?
The format rules decide whether a name can exist at all; the last three rows decide whether you can have it. Each cell comes from the registry's own documentation, linked under the table.
| Rule | npm | PyPI | crates.io |
|---|---|---|---|
| Characters | URL-safe characters only; no spaces, no ~)('!*; unscoped names cannot start with ., _ or - | ASCII letters, digits, ., _ and -; must start and end with a letter or digit | ASCII letters, digits, - and _; the first character must be a letter |
| Length | 214 characters at most, the scope included | The name spec sets no length | 64 characters at most |
| Case | New names must be lowercase | Any case, compared lowercased | Any case, compared without regard to case |
| What is compared | The name without punctuation, plus a similarity check on unscoped names | The normalized name, plus a similarity check | The name without case, with - and _ treated as one |
| Blocked up front | Node.js core module names such as http and stream, and reserved names such as node_modules | Standard library module names (Python 2.6 on), names the admins prohibit such as requirements.txt | Reserved names and Windows special names such as nul |
| Squatting | A package with "no genuine function" counts as squatted; such accounts "may be removed or renamed without notice" | A package with "no functionality or is empty" is invalid and removed | A crate that only reserves a name for a prolonged period, without genuine functionality or significant development activity, breaks the usage policy and may be deleted |
| Getting a taken name | Not on request; a trademark or other intellectual property claim is "the only path we act on" | Under PEP 541: an abandoned project, never "against the wishes of a reachable owner", or the PSF acting on an infringement claim | With "the explicit approval of the current owner"; the team may also delete crates that break its policies |
Sources. npm: package.json docs, validate-npm-package-name, moniker rules, Username Policy. PyPI: names and normalization, Choosing a Project Name, Name Retention. crates.io: Cargo manifest format, registry index, crates.io policies.
What does a too-similar refusal look like?
On 12 June 2026 a release workflow published a scoped package fine, then failed on a new unscoped launcher named fit-map. The maintainers recorded the registry's answer in a public GitHub issue:
npm error 403 403 Forbidden - PUT https://registry.npmjs.org/fit-map - Package name
too similar to existing package is-map; try renaming your package to
'@dickolsson/fit-map' and publishing with 'npm publish --access=public' instead
Three things stand out. fit-map and is-map differ in letters, not only in punctuation, so the check is wider than the 2017 rule. The name did not exist on the registry, so a registry lookup said it was free. And the registry itself pointed to the way out: the same name under the publisher's scope.
The rule is also why some old names look like exceptions. reactnative is still on the registry, with a first publish date of 5 March 2015; npm's rule covers new packages only, "a package that has not been in the registry before", so names that came first stay.
How do you check a package name from the command line?
These lookups need no account. Each answers one question, and the comments say which. The 404 for json.stream is real (checked 24 October 2026), and still npm's 2017 post names it as a name no one can publish.
# npm: 404 means no package has exactly this name.
# It does not run the similarity check that the first publish runs.
curl -s -o /dev/null -w "%{http_code}\n" https://registry.npmjs.org/json.stream
# npm: format rules, with the validator library the npm team maintains.
# json.stream passes; JSON.stream fails for new packages (capital letters).
npm install --no-save validate-npm-package-name
node -e 'console.log(require("validate-npm-package-name")(process.argv[1]))' JSON.stream
# PyPI: the JSON API answers for the normalized name too
curl -sL -o /dev/null -w "%{http_code}\n" https://pypi.org/pypi/Python.Dateutil/json
# crates.io: the API answers for the - and _ variant of a taken name
curl -s -A "name-check ([email protected])" https://crates.io/api/v1/crates/serde-json | head -c 200
# Normalize a name the PyPI way before you compare it with anything
python3 -c 'import re,sys; print(re.sub(r"[-_.]+", "-", sys.argv[1]).lower())' Friendly.Bard
crates.io asks API clients for at most one request per second and a User-Agent that identifies the application (crates.io data access policy); put your own contact in place of the example. No public endpoint runs npm's similarity check before you publish, which is what issue 9188 asks for.
Which beliefs about free names are wrong?
A 404 from the registry means you can have the name.
It means no package has that exact name. PyPI says a name can be unavailable "even if a project doesn't appear on PyPI": standard library conflicts, similar names, prohibited names and names registered without a release (PyPI Docs).
A name nobody uses can be claimed if you ask the registry.
npm does "not transfer package, organization, or username ownership simply because another user wants the name"; GitHub does not release usernames "on the basis that they appear inactive or unused"; PyPI needs an owner who cannot be reached, no release in twelve months and no activity on the home page, all three (npm, GitHub, PyPI).
Publishing an empty placeholder keeps the name safe until launch.
That is the squatting every registry in the table forbids, and the placeholder can be removed. Publish when the package does something, or publish under your scope first.
In what order should you check a new name?
Cheapest checks first, and nothing public until the last one passes. A rename after launch costs more than any of these steps.
Run the format rules for every registry you will publish to
A name that is valid on PyPI can fail on crates.io (a leading digit) or on npm (an uppercase letter).
Normalize the name the way each registry does, then look it up
Use the commands above. A 200 for any variant means the name is taken.
Decide whether you need an unscoped npm name at all
npm's guidelines put the similarity rule on unscoped names, and a scope "allows you to create a package with the same name as a package created by another user or organization" (npm Docs, About scopes). Scoped packages are private by default, so the first public publish needs
--access public.Check the GitHub user or organization name
GitHub names are "first-come, first-served", and "accounts with no visible activity may be in active use" (GitHub Username Policy).
Look up the domain
ICANN Lookup shows whether a domain is registered and by which registrar.
Search trademarks in the markets you sell to
USPTO trademark search for the United States, TMview for the EU and its partner offices, WIPO Global Brand Database for international filings. A search is not legal advice; it finds the obvious conflicts early.
Publish a working first version
On npm, only this step runs the similarity check. Publish something that installs and runs, so the name is used, not squatted.
Questions developers ask about package names
Does a scoped npm name avoid the too-similar check?
npm's guidelines list the similarity rule only for unscoped packages, and in the refusal above the registry itself suggested the scoped name. Inside your scope you still need a name you have not used before.
Should my PyPI name match the name people import?
Usually, but nothing forces it: "PyPI and other package indices do not enforce any relationship between the name of a distribution package and the import packages it provides" (Python Packaging User Guide). A match saves your users a lookup.
Should I add "js" or "node" to an npm name to make it unique?
npm's package.json docs say not to: "Don't put "js" or "node" in the name" (npm Docs). npm's 2017 post shows jsonstream blocking json.stream and json_stream, and the fit-map refusal above shows a near-variant blocked: adding a word to a taken name is where refusals happen.
Can I delete a crate and keep the name for later?
No. The Cargo Book says that once a crate name is taken, "it cannot be used for another crate", and deletion is the exception: owners may delete a crate only under set conditions, such as within 72 hours of publishing (crates.io policies), and the crates.io code then opens the name to anyone 24 hours later (crates.io source, delete.rs).
Sources
All pages read on 24 October 2026.
- Package name guidelines - npm Docs
- package.json - npm Docs
- About scopes - npm Docs
- Username Policy - npm Docs
- npm Blog Archive: New Package Moniker rules (26 December 2017)
- validate-npm-package-name - npm on GitHub
- npm/cli issue 9188: npm publish --dry-run should detect name similarity conflicts
- forwardimpact/monorepo issue 1724: fit-map launcher rejected by npm name-similarity policy
- Names and normalization - Python Packaging User Guide
- Distribution package vs. import package - Python Packaging User Guide
- Choosing a Project Name - PyPI Docs
- Name Retention - PyPI Docs (PEP 541)
- The Manifest Format - The Cargo Book
- Registry Index - The Cargo Book
- Publishing on crates.io - The Cargo Book
- Usage Policy - crates.io
- crates.io source code: src/controllers/krate/delete.rs
- Data Access Policy - crates.io
- GitHub Username Policy - GitHub Docs



