Go treats module paths as distinct strings, so example.com/M and example.com/m can be different modules. Because many filesystems ignore case, the module proxy protocol encodes each uppercase letter as an exclamation mark followed by its lower case form. That is why you sometimes see !m in a URL or cache path.
What the reference says
The Go Modules Reference describes the proxy protocol. To avoid ambiguity when serving from case insensitive file systems, the module and version elements of a request are case encoded by replacing every uppercase letter with an exclamation mark and the matching lower case letter. The reference gives the example that example.com/M is encoded as example.com/!m, which lets both example.com/M and example.com/m be stored on disk without colliding.
The same encoding applies to versions, since a version can contain letters in a pre-release suffix.
Where you meet it
You will see the encoded form in module proxy URLs, in the module cache on disk, and in logs from a private proxy or mirror. A path with a capital letter in a GitHub username or repository name is common in Go imports, and each such capital shows up as an exclamation mark in the cache directory.
A few examples of how to read it:
| Original path | Encoded form |
|---|---|
| example.com/M | example.com/!m |
| example.com/MyLib | example.com/!my!lib |
| example.com/m | example.com/m |
The third row shows why the encoding exists. Without it, the first and third could land in the same folder on a case insensitive disk.
Why it matters for security work
Name matching is the heart of vulnerability scanning and SBOM work. A scanner that lower cases every package name before matching would treat example.com/M and example.com/m as one package. Whether that is a harmless convenience or a source of wrong results depends on the ecosystem, and the Go reference suggests case is meaningful here.
There are two practical risks.
First, a false match. If an advisory names one path and a different module differs only by case, a case folding comparison could attach the advisory to the wrong module.
Second, a missed match. If your tool reads encoded paths from a cache directory and does not decode them, !my!lib will not equal MyLib, and an advisory for MyLib will not match.
Steps to check your tooling
- Find a Go module in your build whose path contains an uppercase letter.
- Look at how your scanner or SBOM lists it. Is the path shown as written in go.mod, or in the encoded form?
- If the output shows an exclamation mark, the tool is exposing the cache encoding and may fail to match advisories. Raise it with the vendor or normalise before matching.
- Keep the original path from go.mod in the SBOM, as it is the identifier other tools will use.
- If you run a private proxy, check that it follows the encoding in the reference.
Keep it in proportion
Most Go dependencies use lower case paths, so this will rarely bite. But when it does, the cause is hard to spot, because the module builds fine and the scan simply returns nothing. A five minute check on one capitalised path tells you whether your pipeline is safe from this class of problem.
Safeguard's SBOM and dependency scanning records components for your projects, and the free scan lets you see how paths appear in the result.
Quick check
How is example.com/M encoded for a Go proxy?
As example.com/!m. Each uppercase letter becomes an exclamation mark followed by the lower case letter.
Should an SBOM store the encoded path?
No. Store the path as written in go.mod. The encoding is a protocol and storage detail.
Sources
Last reviewed: 19 October 2026.