A single line in go.mod can pull in code from a vendor you would never trust on purpose.
Your project may depend on the russian-maintained mailru/easyjson without ever choosing it, buried several levels deep in the dependency graph of popular Go libraries.
This is a real risk, because the dependencies you did not pick are the ones you never check.
In this post, I show how I found one such indirect dependency in a chain of popular libraries, and how a single prompt to Claude helped me remove it.
TL;DR: oapi-codegen depended on mailru/easyjson without asking for it directly.
The dependency came through getkin/kin-openapi, which used perimeterx/marshmallow, which used easyjson.
I asked Claude if marshmallow was really needed, it was not, and a few PRs later the dependency was gone from the whole chain.
The problem: a dependency you can’t live without
About a year ago, Hunted Labs published research called “The Russian open source project that we can’t live without” . It is about mailru/easyjson , a fast JSON library made by Mail.Ru (VK), a russian company. The library is used everywhere in the Go ecosystem , often as an indirect dependency that teams never chose on purpose.
The concern is not a known bug, but trust and control.
The project is run by a company under russian jurisdiction, so the people with commit access could be pressured to add harmful code.
And because so many projects depend on easyjson, one bad release could reach a huge part of the ecosystem.
Why this matters: every dependency is attack surface
Attacks through dependencies are real, and Go is not safe from them.
In 2025,
Socket researchers found
a backdoored copy of
boltdb/bolt
, a popular Go key-value store, published under a typosquat name to look like the real package.
The
Go module mirror
never changes a version once it is cached, and the attacker abused this: after the bad version was cached, the mirror kept serving it even after the GitHub tag was cleaned.
The backdoor stayed hidden for years.
I have seen this myself. In a previous post , I wrote about a group of malicious Go projects that copy real tools and run a trojan the moment you build them.
Both cases teach the same lesson: every dependency you add is attack surface, and a dependency you never chose is the worst kind, because nobody on your team watches it.
You cannot check what you do not know is there.
The safest way to shrink the attack surface is to remove a dependency, because code that is not in your go.mod cannot be hacked.
A little copying is better than a little dependency
There is a good rule for this from Rob Pike, one of the authors of Go, and it is one of the Go Proverbs :
A little copying is better than a little dependency.
In other words, copying a few lines of code is often safer than adding a whole dependency you must trust and keep up to date.
This is the idea I followed to remove easyjson.
Tracing the dependency chain
We use
oapi-codegen/oapi-codegen
, a popular OpenAPI code generator, in production, so I picked it as the first project to clean up.
There I saw
mailru/easyjson
in its go.mod, where it appeared as an
indirect dependency
.
❯ git switch -d 1e1b2a251bc24450a5766af9d648e6601436e5aa
HEAD is now at 1e1b2a25 chore(deps): update dessant/label-actions action to v5.0.3 (.github/workflows)
❯ sed -n '16,25p' go.mod
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
github.com/go-openapi/jsonpointer v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.26.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.9.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/oasdiff/yaml v0.1.0 // indirect
github.com/oasdiff/yaml3 v0.0.13 // indirect
The command
go mod why
shows the exact path that pulls it in:
❯ go mod why -m github.com/mailru/easyjson
# github.com/mailru/easyjson
github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen
github.com/getkin/kin-openapi/openapi3
github.com/perimeterx/marshmallow
github.com/mailru/easyjson/jlexer
Read from top to bottom, the chain is:
oapi-codegen
└── getkin/kin-openapi
└── perimeterx/marshmallow (now HumanSecurity/marshmallow)
└── mailru/easyjson
getkin/kin-openapi
used perimeterx/marshmallow, and marshmallow used easyjson.
Fixing it at the source
My first idea was to fix marshmallow itself, so I opened
issue #32
.
The maintainer agreed to accept a patch , so I sent a fix in PR #33 .
But the PR was not merged yet, and waiting for a maintainer is slow, so I went one level up the chain instead.
I opened
issue #1192
on kin-openapi to raise the problem.
Asking Claude: do we even need this dependency?
Instead of fixing marshmallow, I asked if kin-openapi needed it at all.
I gave
Claude
a simple, open prompt:
Explore if this project requires using perimeterx/marshmallow or we can use some other more trusted dependency.
The answer was clear and useful:
The Ref struct has only one JSON field, $ref, so two json.Unmarshal calls do the same job.
The first call fills the struct, and the second fills a map[string]any from which you delete "$ref".
This gives the same result as marshmallow.Unmarshal(..., WithExcludeKnownFieldsFromMap(true)), but uses only the standard library.
So I followed Claude’s advice and sent
PR #1196
to kin-openapi, replacing marshmallow with encoding/json.
A few lines of the standard library replaced a whole external package and broke the chain that pulled in easyjson.
The fix propagates through the ecosystem
After that, the change spread out on its own, and I never touched oapi-codegen:
kin-openapimerged my PR and released it as v0.140.0 .- Renovate then opened
oapi-codegen PR #2395
on its own to upgrade to v0.140.0.
This upgrade dropped
easyjsonfrom the dependency graph .
❯ git switch -d 5d02a03ac0e92e7c3abe9f185800ea26b3a47fed
HEAD is now at 5d02a03a refactor petstore example (#2277)
❯ go mod why -m github.com/mailru/easyjson
# github.com/mailru/easyjson
(main module does not need module github.com/mailru/easyjson)
Conclusion
You can use AI for more than writing code: you can also use it to ask whether a dependency should exist at all.
One prompt helped me see that marshmallow was easy to replace with the standard library.
Fixing the problem upstream helped everyone, not just my project.
Once kin-openapi dropped marshmallow, the change reached oapi-codegen and every project that uses it.
Removing a dependency is the best way to remove its risk, because code that is not in your go.mod cannot be hacked, and you do not have to trust its vendor.
I help teams clean up their dependencies and use AI to write safer Go code.
See my
GitHub profile
.
If you want help auditing your go.mod or setting up CI/CD for this,
reach out
.
I would be happy to help.




