October 7, 2026

Every package I add to a project brings in code I didn't write and can't control. Sometimes that's a good trade, but often it isn't, so I'm fairly strict about which ones I adopt. Better safe than sorry!

Some of that comes from experience. I started building Taproot I/O partly because I took one look at WordPress's bloated ecosystem and wanted to build something better. The same instinct shows up in smaller decisions every day, like whether to install a package or write the twenty lines I actually need.

Retro Solar Panel.png

What a package costs me

The first cost is security. If a vulnerability turns up in a package I depend on, I can't fix it myself right away. I have to wait for the maintainer to release a patch or work around the problem until they do.

The risk isn't limited to honest mistakes. In September 2025, an attacker phished the npm account of the maintainer behind chalk, debug, and more than a dozen other widely used packages, then published versions containing malware aimed at cryptocurrency wallets. The community caught it within a couple of hours, but every project that installed those versions during that window pulled in the attacker's code. The xz Utils backdoor in 2024 was slower and more deliberate: someone spent years earning maintainer status on a compression library before slipping a backdoor into it. The projects depending on those packages hadn't done anything unusual, which is what makes this kind of risk so hard to guard against.

Then there's the maintainer. They might stop maintaining the package, and when that happens I'm the one who has to change my application, on a timeline I didn't choose. Sometimes a package disappears outright. In 2016, the author of left-pad, an eleven-line function for padding strings, unpublished it from npm, and builds across the JavaScript ecosystem broke until npm restored it. More often a package just winds down. Moment.js declared itself a legacy project in maintenance mode in 2020 and recommends choosing a different library in most cases.

Even a well-maintained package runs on its own schedule. The maintainer decides when the next major version ships, and my plans end up kowtowing to their release cycle. I either upgrade on their terms or keep falling further behind, and falling behind eventually turns into a forced migration anyway.

Every package is also one more thing outside my core code that I have to watch. I need to keep track of it, decide whether it still belongs in the project and plan its upgrades. That work doesn't show up when you run the install command, but it shows up later.

Russ Cox described the larger version of this problem in Our Software Dependency Problem: adding a dependency has become so easy that we rarely stop to ask whether we should trust the code we're pulling in.

More than I need, and not quite what I need

Most packages are much bigger than the problem I'm solving. I've seen colleagues reference giant libraries so they can use one function. When I'm performing surgery, I prefer a scalpel to a Swiss Army knife every time.

Even that one function usually doesn't fit cleanly. It was built for a much broader set of use cases, so it doesn't do exactly what my application needs. It carries options and edge cases for other people's problems, and I end up bending my code around it.

Write it or vendor it

So my default is to write a small function of my own, or to vendor only the pieces of a package that I actually need. The Go community has a proverb for this, from a 2015 talk by Rob Pike: "A little copying is better than a little dependency."

Vendoring has rules of its own. I check the license and keep its notice with the code I copied. I also accept that the copy is now mine. If the original gets a security fix, nobody is going to open a pull request against my repository, so I have to know where the code came from and pay attention to it.

Once that code lives in my repository, it's under my control. I can write tests for it and add logging where it's needed. If something breaks, I can fix it on my schedule. It does exactly what my application needs and nothing more.

Where AI fits

AI has made this approach much easier and very fast. Writing a focused function, or pulling the few pieces I need out of a larger package, is now a quick task instead of a project of its own. I can have tests written alongside it, so the code I own starts out checked.

That used to be the main argument for the package: someone else already wrote it. When an assistant can help me write and test the narrow version quickly, that argument gets a lot weaker, and the costs above start to dominate.

AI can push in the other direction too. In its August 2026 status update, the Moment.js team said weekly downloads had more than tripled since 2020, and connected much of that growth to coding agents choosing Moment without a person making a deliberate library choice. Left alone, an assistant tends to reach for whatever it has seen used most often.

That makes it worth writing down when a new package is acceptable. Project instructions can say so, and checks can back that up. I wrote about how we turn written rules into executable checks in how we enforce coding standards.

Even my own packages cost something

Packages I control add overhead too. Taproot's web components used to live in a separate package with exactly one consumer. Our migration notes recorded 23 published versions and more than a dozen dependency-version updates in the generator, along with the publishing workflows to keep everything connected. Bringing it back into the main repository removed that work, which I described in why moving to a monorepo made AI-assisted development easier. WTFM, our documentation tooling, has the same trade-off: making it a separate package introduced another thing to maintain.

When I do publish something, I try to keep its dependencies small and explicit. The Taproot site authoring CLI has one runtime dependency, Espalier, pinned to an exact version so the CLI validates against the same theme contract it was tested with.

When a package is worth it

Sometimes you really do need a third-party package. Some do so much that trying to rebuild or vendor them would be a fool's errand.

Taproot I/O uses Entity Framework Core, and Espalier uses Lit. Espalier is otherwise a zero-dependency design system I built from scratch, so Lit is the one exception there. Both libraries help me write code faster and more cleanly. They're also broad enough that I can't wrap them in an interface inside my own code, so I use them directly.

Wrapping the smaller ones

When I reference a library with a smaller, more specific footprint, I wrap my usage of it in an interface that matches what my application is trying to do. The rest of the code talks to that interface instead of to the package.

The interface describes what my application needs in its own terms, instead of mirroring the library's API. That keeps the library's options and assumptions from leaking into the rest of the code.

If we need to replace the package later, or decide to vendor it, there's one interface to implement. We can do that without changing the rest of the application. A restricted-import lint rule, like the ones we use to keep frontend dependencies flowing in one direction, can also make sure the wrapper stays the only place that imports the package.

Before you add the next one

Before I add a package, I ask a few questions. How much of it will I actually use? Could an assistant and I write and test that part in less time than I'd spend reviewing the package's changelog over the next year? If the maintainer disappeared tomorrow, what would I have to change?

If the answers point toward the package, I add it, and wrap it if it's small enough. If they don't, I write the small version and keep it close to the code that uses it.