Ghost Dependencies: How to Finally See What's Actually Running in Your Stack
Here's a fun thought experiment: open up your project's dependency manifest right now. Count the direct dependencies. Now ask yourself — how many total packages actually get installed when you run npm install or pip install? If you've never checked, the answer is probably going to make you uncomfortable.
We tend to think about our software supply chains like grocery lists. You write down what you need, you get what you asked for, done. But the reality is more like ordering a meal at a restaurant — you get what you ordered, plus whatever the kitchen decided to throw in based on what the recipe requires, what the sous chef prefers, and what was in the walk-in fridge that morning. You didn't choose any of that. You're eating it anyway.
This is the world of transitive dependencies, and it's where a huge chunk of modern software vulnerabilities quietly live.
Why Your Direct Dependencies Are Only Half the Story
When the event-stream incident hit the Node.js ecosystem back in 2018, most of the affected projects had no idea they were even using it. They were depending on something that depended on something that used it. That's the nature of the problem — the attack surface doesn't stop at what you consciously chose.
In a typical mid-sized JavaScript project, you might declare 30 direct dependencies. Those 30 packages can easily pull in 400 to 800 transitive ones. In Python projects using popular ML libraries, that number can crack into the thousands. Each of those packages is code running in your environment, potentially with access to your filesystem, your network, your secrets. And most teams have zero visibility into any of it.
The open-source ecosystem is genuinely incredible — it's one of the most powerful examples of collective innovation humanity has pulled off. But that same collaborative, permissive nature means anyone can publish a package, anyone can become a transitive dependency, and the chain of trust gets long and weird fast.
Step One: Actually Map the Tree
Before you can fix anything, you need to see it. Most modern package managers have built-in tooling for this that almost nobody uses.
For JavaScript/Node projects, npm ls --all will dump your full dependency tree to the terminal — it's verbose, but it's real. Tools like depdendency-cruiser go further, letting you visualize the graph and set rules about what can depend on what. For a more actionable security-focused view, npm audit is table stakes, but pair it with better-npm-audit or Snyk's CLI to get severity filtering and more useful output.
Python developers should get comfortable with pip-tree (or the newer pipdeptree) to render the full graph. For projects using poetry, the poetry show --tree command does the same job cleanly.
For teams running polyglot stacks — which, let's be honest, is most of us — OWASP Dependency-Check is worth the setup time. It works across ecosystems and cross-references against the National Vulnerability Database. It's not glamorous, but it covers ground that language-specific tools miss.
The goal here isn't a one-time audit. It's building a living map you can return to.
Step Two: Know What You're Looking For
Not all dependency risk looks the same. There are a few patterns worth flagging specifically.
Abandoned packages are one of the sneakier hazards. A package that hasn't had a commit in three years isn't necessarily dangerous, but it's also not getting patched if something goes wrong. Tools like npm-check will surface packages that have available updates you haven't applied — that's a useful starting signal.
Ownership changes are genuinely scary. When a popular package changes maintainers, that new maintainer has publish rights to something running inside thousands of production apps. Services like Socket.dev monitor for this kind of activity and flag it before you pull down an update that's been quietly modified.
Packages with excessive permissions are another red flag. If a utility library that formats dates is requesting filesystem access or making outbound HTTP calls, that's worth questioning. Tools like npm-audit and Snyk surface some of this, but manual review of what a package actually does versus what it claims to do is still irreplaceable.
Version pinning hygiene matters more than people realize. If you're using loose version ranges (like ^1.2.0 or >=2.0), you're implicitly agreeing to accept whatever changes land in that range without reviewing them first. Locking your lockfile and treating updates as deliberate decisions — not automatic — is one of the simplest posture improvements a team can make.
Step Three: Build Organizational Habits, Not One-Off Fixes
Here's where a lot of teams fall down. They run an audit, fix the flagged issues, and then go back to the same practices that created the problem. Six months later, the entropy is back.
The answer is to treat dependency hygiene like any other engineering practice — with process, automation, and shared ownership.
A few concrete things that work:
Automate your audit in CI. If a dependency with a critical CVE can merge to main without anyone noticing, your audit process isn't a process — it's a suggestion. Add a step to your pipeline that fails the build on high-severity findings. Snyk, Dependabot, and Renovate all have CI integrations that make this straightforward.
Create a dependency review checklist. When someone adds a new dependency, what questions do you ask? How many weekly downloads does it have? When was the last commit? Who maintains it? Is there a lighter alternative? Having a written checklist makes this a team norm rather than a thing one paranoid engineer does.
Set a review cadence. Once a quarter, pull the full tree and look for drift. Packages you added for a prototype that never got removed. Dependencies that have had ownership changes. Things that have fallen behind on updates. This doesn't have to take a full sprint — a focused two-hour review session with two engineers can cover a lot of ground.
Document your intentional dependencies. An Architecture Decision Record (ADR) for each significant dependency might sound like overkill, but even a simple deps.md that explains why you're using something and what the alternatives were creates institutional memory. When you're deciding whether to update or replace something two years later, you'll be glad you wrote it down.
The Bigger Picture
The open-source supply chain is a commons. It's built by thousands of developers contributing their time and expertise to something shared. That's worth celebrating and protecting.
But the decentralized nature of that commons also means there's no central authority making sure every package in your tree is safe, maintained, and acting in good faith. That responsibility lands on us — on the teams and communities building on top of it.
Getting serious about dependency visibility isn't about distrust. It's about being a responsible participant in a shared ecosystem. The more teams that build these practices into their normal workflows, the harder it becomes for supply chain attacks to find purchase.
You can't secure what you can't see. Start by looking.