Help us shape the future of Go! Take the 2026 Go Developer Survey

modslop

command module
v0.2.31 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 28, 2026 License: MIT Imports: 19 Imported by: 0

README

modslop

Go Reference Latest release

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 403 with 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 a shopspring/decimal typosquat and a boltdb/bolt typosquat 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 via go env GONOPROXY, same as the real go command 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.mod requires was never published. A real, popular module doesn't shield a pinned dependency from this: verified live against github.com/gorilla/mux (a long-established, never-past-v1.x module) — v3.5.0 404s on the module proxy while the module's real v1.8.1 resolves 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. logrusx vs. 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.mod requires is covered by a retract directive in the module's own go.mod: the maintainer's own explicit "don't use this version" signal. Retraction is advisory only — proxy.golang.org and a plain go install/go get/go mod download keep serving a retracted version exactly like a healthy one (only go list -m -u surfaces 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 its module directive: 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 your go.mod actually 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., and go 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.mod both requires and excludes the exact same version of the same module. Not a heuristic: verified live, this makes go build/go list -m all fail outright with "ignoring requirement on excluded version" / "updates to go.mod needed", regardless of whether the module or version actually exists — reproduced with GOPROXY=off too, so it's a pure go.mod-authoring contradiction, not a network-dependent one. A plausible AI mistake: adding an exclude line to "pin away" a bad version while leaving the require line 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 own require/replace/tool/exclude directives — a replace target is checked in place of the original path (since that's what actually gets fetched and built), and a tool path not already covered by a require entry 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. A replace directive whose old path isn't named by any require line 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 tidy all honor such a replace with zero require line needed) — is still checked, under its own new-side target. exclude is only cross-checked against this same go.mod's own require lines for an exact version match (see excluded-requirement above) — 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/GONOPROXY are exempted from not-found/new-and-thin (see above) but still get checked for name-collision-risk against 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 by name-collision-risk (the not-found and new-and-thin checks don't depend on the list, though).
  • new-and-thin is a heuristic, not proof. Every real project was new once.
  • retracted only looks at the retract directives in the module's current latest release's go.mod, matching what go list -m -retracted itself 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

Documentation

Overview

Command modslop audits a Go module's dependencies for signs of slopsquatting: names that don't exist, names that are suspiciously close to a well-known module, and modules that are brand-new with no track record. See README.md for the full rationale.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL