TufExt All articles
Build & Infrastructure

Zombie Dependencies in Monorepos: The Hidden Security Debt That Keeps Compounding

TufExt
Zombie Dependencies in Monorepos: The Hidden Security Debt That Keeps Compounding

Photo: monorepo dependency graph security audit developer workspace, via www.syncfusion.com

Monorepos have become the architectural default for many engineering teams across the United States, and for good reason. Unified versioning, shared tooling, and consolidated CI/CD pipelines offer genuine operational advantages. But that same centralization creates a particular kind of technical debt that is easy to accumulate and difficult to audit: zombie dependencies.

These are the packages nobody removed when a feature was deprecated, the transitive dependencies three levels deep that no application in your workspace actually imports, and the modules that were added during a proof-of-concept sprint and never cleaned up. Individually, they seem harmless. Collectively, they represent a compounding security liability and a measurable drag on your build infrastructure.

Why Monorepos Are Especially Vulnerable to Dependency Accumulation

In a single-package repository, dependency hygiene is relatively straightforward. If a package is unused, it tends to surface quickly during audits or simply during routine development. In a monorepo, the problem is structurally different.

Packages exist across multiple workspaces, often maintained by different teams with different release cadences. A dependency that was critical to one workspace may become irrelevant after a refactor, but it remains declared in package.json or pyproject.toml indefinitely. Hoisted dependencies in JavaScript monorepos — a common pattern with tools like npm workspaces or Yarn Berry — further obscure the actual consumption chain. A package may appear to be used because it is hoisted to the root, while in reality no workspace references it directly.

The result is a dependency graph that grows in one direction only: outward. And every node in that graph is a potential entry point for a supply chain attack.

The Security Calculus of Unused Packages

The relationship between unused dependencies and security risk is not theoretical. Every package you carry in your monorepo — regardless of whether your code ever calls it — is included in your vulnerability scanning scope. Your security tooling, whether that is Dependabot, Snyk, or a custom SAST pipeline, must evaluate each one. This has two consequences.

First, your signal-to-noise ratio degrades. When vulnerability alerts include packages that are never actually executed in production, engineers learn to treat those alerts with less urgency. That normalization is dangerous. A legitimate critical vulnerability in an active dependency can get lost in the noise generated by alerts on packages your application has not touched in eighteen months.

Second, unused packages can still contribute to your threat surface in indirect ways. Build tooling, test runners, and code generation scripts frequently import dependencies from the broader workspace graph. A zombie package that sits unused in application code may still be pulled into a build artifact or a development container image, where it can be exploited if that environment is compromised.

Identifying the Dead Weight: A Forensic Approach

Removing dependencies without a systematic audit is how teams break things. A forensic approach requires understanding not just what is declared, but what is actually consumed — and at which layer.

Step one: Static import analysis. Tools like depcheck for JavaScript projects or deptry for Python can perform static analysis across your workspace to identify declared dependencies that have no corresponding import statements. Run these tools at the workspace level, not just the root, and treat their output as a starting inventory rather than a final answer.

Step two: Trace transitive consumption. A package that appears unused in your direct dependency declarations may still be required by a transitive chain. Before removing anything, use your package manager's dependency graph tooling — npm ls, yarn why, or pnpm why — to understand whether any active package depends on it. Document this chain explicitly.

Step three: Cross-reference your build artifacts. Static analysis tells you what your source code imports. It does not tell you what ends up in your production bundles or container images. Use bundle analysis tools — Webpack Bundle Analyzer, Vite's rollup-plugin-visualizer, or equivalent — to verify that packages flagged as unused are genuinely absent from compiled output.

Step four: Review CI/CD and tooling configurations. Build scripts, linting configurations, and test frameworks can pull in packages that never appear in application source. Audit your devDependencies with the same rigor you apply to production dependencies. An outdated version of a build tool is still a vulnerability.

Establishing a Removal Protocol That Does Not Break Production

Once you have a validated list of candidates for removal, the temptation is to delete them all and run the test suite. Resist that impulse in a monorepo context. The interconnected nature of workspaces means that a package appearing unused in one context may be implicitly required in another through a mechanism your static analysis missed.

A safer protocol operates in three phases. In the first phase, deprecate rather than delete. Mark the package as deprecated in your internal documentation and remove it from new workspaces while leaving it declared in existing ones. Monitor for a defined period — typically one to two release cycles — to confirm nothing breaks.

In the second phase, remove the declaration from package.json or the equivalent manifest file, but do not yet run a lock file update. Allow CI to catch any failures before the lock file is modified, which makes rollback significantly easier.

In the third phase, commit the updated lock file and run a full vulnerability scan against the cleaned dependency graph. Document the before-and-after state of your CVE exposure. This documentation serves both as an audit trail and as a concrete demonstration of the security improvement to stakeholders who may question the investment of engineering time.

Building Systemic Defenses Against Future Accumulation

A one-time audit addresses existing debt but does nothing to prevent the next wave of accumulation. Sustainable dependency hygiene in a monorepo requires process changes, not just tooling.

Integrate static dependency analysis into your CI pipeline as a required check. Any pull request that introduces a new dependency should trigger a review of whether that dependency is already available in the workspace or whether a lighter alternative exists. Establish a dependency review gate — similar to a code review gate — for additions above a defined threshold of transitive dependencies.

Assign ownership at the workspace level. When individual teams are accountable for the dependency health of their workspaces, accumulation slows because the cost of carrying dead weight becomes visible to the people best positioned to address it.

Finally, schedule quarterly dependency audits as a standing engineering task. The tooling to support this workflow exists and is mature. The gap is almost always organizational, not technical.

The Compounding Cost of Inaction

Every zombie dependency your monorepo carries today will be harder to remove six months from now. The code around it will have grown, the original context will have faded, and the engineers who added it may have moved on. Meanwhile, the attack surface it represents grows as new vulnerabilities are discovered in packages that nobody is actively monitoring because nobody knows they are still there.

Dependency hygiene is not glamorous engineering work. It does not ship features or close tickets. But it is foundational to the kind of codebase that can be shipped with confidence — and in a monorepo, where the blast radius of a compromised dependency can span dozens of applications simultaneously, the stakes are considerably higher than they appear.

All Articles

Related Articles

When Green Means Nothing: Rethinking What Your CI/CD Pipeline Actually Validates

When Green Means Nothing: Rethinking What Your CI/CD Pipeline Actually Validates

The Local Environment Is a Legacy Constraint — Cloud-Native Dev Workflows Are the Replacement

The Local Environment Is a Legacy Constraint — Cloud-Native Dev Workflows Are the Replacement

Configuration Sprawl in Containerized Environments: A Practical Guide to Stopping Drift Before It Reaches Production

Configuration Sprawl in Containerized Environments: A Practical Guide to Stopping Drift Before It Reaches Production