README
¶
modslop
Catch slopsquatted and hallucinated Go module names in your go.mod.
LLM coding assistants occasionally invent import paths that sound
plausible but don't exist — "package hallucination." For most
registries that's just an install failure. Go modules are different:
because a module path is just a VCS location, anyone can stand up a
real repo at the exact path an LLM hallucinated and it will resolve
and build. That's "slopsquatting" applied to Go specifically, and it's
a live risk (the bpoorman/uuid
maintainer-name typosquat, live and installable for over four years, is
a real example of the same underlying weakness — no registry operator
anywhere in the loop to catch or remove it). Full writeup with sources:
docs/SLOPSQUATTING.md.
modslop reads a go.mod and flags requirements that look risky:
- proxy-blocklisted-malicious — the Go module proxy itself has
explicitly flagged this exact path as malicious and refuses to serve
it (a distinct
403with a "considers this module to be malicious" body, not a generic fetch error). Highest severity: this isn't a heuristic guess, it's the proxy operator's own confirmed verdict. Verified against real, publicly reported malicious Go modules, including ashopspring/decimaltyposquat and aboltdb/bolttyposquat carrying an RCE backdoor. - not-found — the path doesn't resolve via the Go module proxy at
all. If nothing pulled it in before, this is worth a hard look —
possibly a hallucinated import that was never real. Skipped for a
path covered by your local
GOPRIVATE/GONOPROXY(read viago env GONOPROXY, same as the realgocommand resolves it) — those are fetched directly from VCS, never through the public proxy, so a miss there is expected and carries no signal. - version-not-found — the module itself is real and resolves fine,
but the exact version your
go.modrequires was never published. A real, popular module doesn't shield a pinned dependency from this: verified live againstgithub.com/gorilla/mux(a long-established, never-past-v1.x module) —v3.5.0404s on the module proxy while the module's realv1.8.1resolves cleanly. A hallucinated version number of a genuine library is exactly as easy for an AI assistant to invent as a hallucinated module path, and every other check here (not-found, name-collision, new-and-thin) keys off the module path or its latest release, never the specific version actually pinned, so this was invisible before this check existed. - name-collision-risk — the module's name is one or two edits away
from a well-known module (e.g.
logrusxvs.logrus), the classic typosquat/slopsquat shape. Only raised when the module itself also looks unproven — brand new, single-version, or unresolved — since an established package sharing a generic word or short string with a popular one by chance (e.g.errors,protobuf) isn't evidence of anything on its own. - name-collision-exact — the module's name is identical to a
well-known module (not a typo), published at a different, unproven
path. This is the impersonation technique documented in a real,
disclosed, active Go supply-chain campaign — attackers republish a
popular module's exact name under a new, attacker-controlled owner
instead of misspelling it (Bae & Yagemann, "Beyond Takedown: Measuring
Malicious Go Module Persistence in the
Wild", 2026 — disclosure led to 684
GitHub takedowns and 1,377 proxy-cached versions remediated by the Go
team). Gated on the same unproven-path check as
name-collision-risk, so a long-lived, publicly known fork that deliberately kept the original name isn't flagged just for existing. - new-and-thin — the module exists, but has exactly one published version, released in the last 30 days. Could be a legitimate new project. Could also be a name registered specifically to catch whoever's AI assistant suggests it.
- retracted — the exact version your
go.modrequires is covered by aretractdirective in the module's own go.mod: the maintainer's own explicit "don't use this version" signal. Retraction is advisory only —proxy.golang.organd a plaingo install/go get/go mod downloadkeep serving a retracted version exactly like a healthy one (onlygo list -m -usurfaces it), so nothing about a normal build warns you. Verified against a real, current retraction:github.com/mattn/go-sqlite3's go.mod (at its latest tag) retracts[v2.0.0+incompatible, v2.0.7+incompatible]("Accidental; no major changes or features."), and every version in that range still resolves and installs cleanly. - deprecated — the module's own go.mod carries a
// Deprecated:comment on itsmoduledirective: a whole-module version of the retraction signal above, and exactly the shape of mistake stale LLM training data produces — suggesting a real, still-installable import path the ecosystem has since moved off of. Also advisory only, and also read from the module's latest go.mod, not the version yourgo.modactually requires (so an old, pre-deprecation version still gets flagged). Verified against a real, current deprecation:github.com/golang/protobuf's go.mod (at its latest tag) carries// Deprecated: Use the "google.golang.org/protobuf" module instead., andgo get github.com/golang/protobuf@v1.3.0— a version tagged years before that comment existed — still prints the warning. - excluded-requirement — your own
go.modbothrequires andexcludes the exact same version of the same module. Not a heuristic: verified live, this makesgo build/go list -m allfail outright with "ignoring requirement on excluded version" / "updates to go.mod needed", regardless of whether the module or version actually exists — reproduced withGOPROXY=offtoo, so it's a pure go.mod-authoring contradiction, not a network-dependent one. A plausible AI mistake: adding anexcludeline to "pin away" a bad version while leaving therequireline pointed at that same version instead of bumping it.
If you hit "no required module provides package" or "cannot find module"
Those are Go's own go build / go mod tidy errors for exactly this
situation — an import (yours or a dependency's) points at a module path
that doesn't resolve, or resolves to something nobody vetted. Verified
against a real hallucinated import:
main.go:4:2: no required module provides package github.com/nonexistent-org/some-pkg; to add it:
go get github.com/nonexistent-org/some-pkg
go: cannot find module providing package github.com/nonexistent-org/some-pkg: module github.com/nonexistent-org/some-pkg: git ls-remote -q origin in ...: exit status 128
The error text can't tell you whether that's a typo you'll fix in ten
seconds or an LLM-hallucinated path that happens to already resolve
because someone registered it first. modslop checks everything
already sitting in go.mod — including requirements that did
resolve — for the signals (not-found, near-miss-of-a-popular-name,
brand-new-and-thin) that a human skim of the error message won't catch.
How this differs from existing tools
pkgtwist and
typo-scanner
generate permutations of a name you already trust and check whether an
attacker has squatted the variants. modslop runs the other
direction: given a go.mod you already have (possibly AI-generated,
possibly reviewed by nobody), it checks whether any of these specific
imports look hallucinated or freshly squatted. Point one at a
dependency list you're not sure was written by a careful human.
A related experiment on where this actually happens for PyPI/npm is in Finding #2 of agent-bootstrap-log: 88 LLM-generated dependency names checked against the real registries, misses clustering in fast-moving/niche domains.
Install
No registry signup needed — go install fetches directly from the
module proxy and GitHub:
go install github.com/experimental-gains/modslop@latest
Or via Homebrew:
brew install experimental-gains/tap/modslop
Or build from source:
git clone https://github.com/experimental-gains/modslop.git
cd modslop && go build .
Usage
# check ./go.mod
modslop
# check a specific file
modslop path/to/go.mod
# machine-readable output, e.g. for CI
modslop --json
Exit code is 1 if anything was flagged, 0 otherwise.
Use as a GitHub Action
- uses: experimental-gains/modslop@v0.2.31
With arguments:
- uses: experimental-gains/modslop@v0.2.31
with:
args: --json
A non-zero exit (something flagged) fails the step, so this is
CI-gateable as-is — no extra run: glue needed.
Use with pre-commit
repos:
- repo: https://github.com/experimental-gains/modslop
rev: v0.2.31
hooks:
- id: modslop
Runs on any commit that touches go.mod. pre-commit builds the hook
via go install the first time (needs Go available, no other setup).
Use as a Claude Code / Copilot CLI plugin
modslop also ships as a skill in the
supplychain-guard
plugin, so an agent checks a new Go module path before
running go get, not just at commit time:
claude plugin marketplace add experimental-gains/claude-plugins
claude plugin install supplychain-guard@experimental-gains-plugins
Works the same way with GitHub Copilot CLI (copilot plugin marketplace add experimental-gains/claude-plugins, same install command with copilot).
Limitations
- Checks
go.mod's ownrequire/replace/tool/excludedirectives — areplacetarget is checked in place of the original path (since that's what actually gets fetched and built), and atoolpath not already covered by arequireentry is resolved to its owning module and checked the same way, but the full transitive module graph (what those dependencies themselves require) isn't walked. Areplacedirective whose old path isn't named by anyrequireline at all — legal go.mod syntax that overrides a transitive dependency without promoting it to a direct one, confirmed live against the real go toolchain (go run/go list -m all/go mod tidyall honor such a replace with zerorequireline needed) — is still checked, under its own new-side target.excludeis only cross-checked against this same go.mod's ownrequirelines for an exact version match (seeexcluded-requirementabove) — it isn't otherwise used to influence version selection, matching this tool's general choice not to walk the full module graph. - Private/internal modules covered by your
GOPRIVATE/GONOPROXYare exempted fromnot-found/new-and-thin(see above) but still get checked forname-collision-riskagainst the popular-module list — if that ever produces noise on a real internal naming scheme, it's worth an issue. - The "well-known module" list (
popular.go) is a curated ~130 names, not exhaustive. A near-miss against a module that isn't on the list won't be caught byname-collision-risk(thenot-foundandnew-and-thinchecks don't depend on the list, though). new-and-thinis a heuristic, not proof. Every real project was new once.retractedonly looks at theretractdirectives in the module's current latest release's go.mod, matching whatgo list -m -retracteditself honors. A version retracted only by some earlier, since-superseded release that isn't the current latest anymore is a case this won't catch.
Support
If this caught something useful, a star helps others find it — that's
the main thing. This project is free and open source; if it's useful
to you, tips are also welcome via
Liberapay or this ETH
address (self-custody, no KYC, no obligation):
0x87053a1898994043e7476800cB5d4BDB423eADD7
Build-in-public updates on Nostr (no account needed to read).
License
MIT