Bumblebee Tutorial: Scan Your Dev Machine for Supply Chain Risks
How to install and use Bumblebee, Perplexity's open-source scanner for npm, MCP configs, and extensions. Real commands, scan profiles, and incident response setup.
On this page
Every time a supply chain attack hits, security teams ask: which dev machines have the compromised package installed? SBOMs cover production builds. EDR covers running processes. Neither checks the actual files sitting on a developer's laptop.
Perplexity open-sourced Bumblebee (May 22, 2026) to fill that gap: a small, read-only Go binary that inventories npm, pnpm, Yarn, Bun, PyPI, Go modules, RubyGems, Composer packages, plus MCP configs and editor/browser extensions. No install scripts, no network calls during scanning, no write operations.
DepScan checks a single package.json in the browser; Bumblebee checks your entire machine across 8 ecosystems. The same supply chain principles apply locally, not just in CI.
What Bumblebee Scans
| Category | What it checks | Example paths |
|---|---|---|
| npm/pnpm/Yarn/Bun | Lockfiles and node_modules | ~/.npm, ~/code/**/package-lock.json |
| PyPI | pip installs and virtualenvs | ~/.local/lib/python*, **/venv/ |
| Go modules | go.sum and module cache | ~/go/pkg/mod |
| RubyGems | Gem installs | ~/.gem, **/Gemfile.lock |
| Composer | Vendor directories | **/composer.lock |
| MCP configs | AI agent server configs | mcp.json, claude_desktop_config.json, .claude.json |
| Editor extensions | VS Code, Cursor, Windsurf | ~/.vscode/extensions, ~/.cursor/extensions |
| Browser extensions | Chromium and Firefox | Chrome/Default/Extensions, .mozilla/firefox |
All scans are read-only. Bumblebee never executes a package manager or runs install scripts, which sidesteps attacks that hide in preinstall/postinstall hooks.
Installing Bumblebee
- 1
Clone and Build (requires Go 1.25+)
bashgit clone https://github.com/pplx-oss/bumblebee.git cd bumblebee go build -o bumblebee ./cmd/bumblebee - 2
Verify the Binary
bash./bumblebee --versionYou should see the version string and build commit. If not, confirm your Go version with
go version(must be 1.25 or later). - 3
(Optional) Move to PATH
bashsudo mv ./bumblebee /usr/local/bin/
The Three Scan Profiles
| Profile | What it scans | Best for |
|---|---|---|
| baseline | Common global/user package roots, language toolchains, extensions, MCP configs | Daily cron, routine inventory |
| project | Targeted sweep of specific dev directories (~/code, ~/src) | Auditing work projects on demand |
| deep | Operator-supplied roots including full home directory | Active incident response |
Running Your First Scan
- 1
Run a Baseline Scan
bashbumblebee scan --profile baseline > inventory.ndjsonOutput is NDJSON (one JSON record per line). Diagnostics go to stderr, inventory goes to stdout. This makes piping clean.
- 2
Filter Output with jq
Pull only npm packages:
bashcat inventory.ndjson | jq -r 'select(.ecosystem == "npm") | .package'List all discovered MCP config file paths:
bashcat inventory.ndjson | jq -r 'select(.ecosystem == "mcp") | .path' - 3
Count Packages by Ecosystem
bashcat inventory.ndjson | jq -r '.ecosystem' | sort | uniq -c | sort -rnThis gives you a quick breakdown of how many packages each ecosystem contributes to your machine's footprint. A typical active dev machine shows 500+ npm packages, a handful of Go modules, and 10-30 editor extensions.
Triaging Scan Findings
The first baseline scan on a working developer machine is a shock. Thousands of records, most of them transitive npm packages you have never heard of, plus every editor extension you installed once and forgot about. That is not a bug in the tool. An inventory is supposed to be exhaustive.
The mistake teams make here is trying to read the output. You do not read an inventory, you query it. Bumblebee's job ends at producing accurate NDJSON; deciding what matters is a separate step, and it is the step where most rollouts stall.
Before writing any query, look at exactly one record so you know what fields you actually have to work with:
head -1 inventory.ndjson | jq .Do this every time you upgrade the binary. Field names in a young tool are the least stable part of its interface, and a jq filter that silently returns nothing because a key was renamed is worse than no check at all, because it looks like a clean result.
A Triage Order That Actually Works
Work outward from the things that can execute code with the least warning, not alphabetically and not by ecosystem size. In rough priority order:
- MCP server configs. These grant an AI agent the ability to run commands and read your project context. The blast radius is the largest and the population is the smallest, usually under a dozen entries, so this is the highest value per minute of review.
- Browser and editor extensions. They run with broad permissions, update automatically without a lockfile, and nobody reviews their diffs. A handful of entries, high impact each.
- Global package installs. Anything installed with
npm i -g,pipx, orgo installsits on your PATH and gets invoked by shell aliases and build scripts. These bypass every per-project control you have. - Direct project dependencies. Packages you or a teammate deliberately added. Moderate count, and you can usually explain why each one is there.
- Transitive dependencies. The largest bucket by far, and the one you triage last, because you triage it with a catalog query rather than with your eyes.
If you invert that order and start with npm packages because there are the most of them, you will burn a day and never reach the MCP configs, which are the part of a modern dev machine that changed most recently and got reviewed least.
Handling False Positives
An inventory scanner produces a different class of false positive than a malware scanner. It is not telling you something is malicious, it is telling you something exists. The false positive appears one layer up, when you or your catalog decides that an existing thing is suspicious.
These are the patterns that reliably generate noise on a real machine:
| What you see | Why it looks alarming | Why it usually is not |
|---|---|---|
| The same package listed many times at different versions | Looks like a version conflict or a shadowed install | Nested node_modules and pnpm's content-addressed store legitimately hold multiple versions side by side |
| Packages under a directory you have not touched in a year | Looks like an unmanaged, unpatched install | Usually an archived project or a stale clone; real, but low risk until someone runs its build |
| A vulnerable version that a catalog flags | Reads as a live exposure | A vulnerable version present on disk is not the same as a vulnerable version reachable at runtime; confirm the path before escalating |
| Extensions you do not recognise | Looks like something installed itself | Most editors auto-install extension dependencies and pack extensions; check the publisher before assuming compromise |
| Hits inside test fixtures or sample projects | Same package name as a real advisory hit | Fixture directories often pin deliberately old versions; scope your paths rather than deleting the fixture |
The practical fix for all five is scoping. Use --profile project against your actual working directories for routine checks and save --profile deep for the moments when completeness genuinely beats signal-to-noise, which is to say incidents.
Resist the urge to build a permanent suppression list early. Suppressions written in the first week encode assumptions you have not tested yet, and they are the single most common reason a scanner goes quiet right before it was needed.
Lockfiles and Transitive Dependencies
Almost every serious npm compromise in recent years reached its victims transitively. Nobody deliberately installed the malicious package; they installed something four levels above it. That is why an inventory built from lockfiles and installed trees tells you more than a list of your direct dependencies ever will.
The distinction matters enough to be explicit about it, because the two files answer completely different questions:
| package.json | Lockfile (package-lock, pnpm-lock, yarn.lock) | |
|---|---|---|
| What it records | Your intent, as version ranges | The exact resolved versions of every package in the tree |
| Transitive coverage | None, only what you asked for directly | Complete, every level of the tree |
| Answers 'am I exposed?' | No, a range like ^1.4.0 could resolve to a clean or compromised version | Yes, it names the version that was actually resolved |
| Changes when | A human edits it | Any install, upgrade, or bot PR touches the tree |
| Reviewed in pull requests | Almost always | Almost never, which is exactly the problem |
A scanner reading lockfiles and installed trees closes that gap without asking anyone to review a 4,000 line diff. Once you have the inventory, the follow-up question is always the same: who pulled this in?
Bumblebee tells you a package is present and where. Your package manager tells you why it is present. Use them together:
# npm: show the full dependency path to a package
npm ls <package-name> --all
# pnpm: explain why a package is in the tree
pnpm why <package-name>
# yarn (berry): same question
yarn why <package-name>If the answer is a single direct dependency, you can usually resolve it by bumping that one package. If the answer is nine different paths through your tooling, you are looking at an override or a resolution pin instead.
One inventory-level check worth running regularly is a duplicate-version count. A package resolving to many versions across your projects means an advisory against any one of them requires per-project work rather than a single upgrade:
cat inventory.ndjson \
| jq -r 'select(.ecosystem == "npm") | .package' \
| sort | uniq -c | sort -rn | head -20Using an Exposure Catalog for Incident Response
When a new supply chain compromise drops, the first question is: are we affected? Bumblebee's exposure catalog feature turns that into a 5-second answer instead of a manual grep across every developer machine.
Create a JSON file describing the compromised packages:
{
"schema_version": "1.0",
"entries": [
{
"id": "ADV-2026-0042",
"name": "colors-hijack",
"ecosystem": "npm",
"package": "colors",
"versions": [">=1.4.1 <1.4.3"],
"severity": "critical"
},
{
"id": "ADV-2026-0043",
"name": "faker-hijack",
"ecosystem": "npm",
"package": "faker",
"versions": [">=6.6.6"],
"severity": "high"
}
]
}Then run a targeted deep scan against it:
bumblebee scan --profile deep --ecosystem npm --exposure-catalog advisory.json --findings-onlyThe --findings-only flag means you only see records that match the catalog. No match, no output. This turns "is anyone running the compromised version?" into a trivially scriptable check you can run across your fleet.
Incident Response for a Confirmed Compromise
A catalog hit is the start of the work, not the end of it. Once you have confirmed that a compromised package version is genuinely present on a machine, the clock is running on credential theft, and the order you do things in matters.
The single most important thing to internalise: uninstalling the package does not undo what it already did. If a malicious install script executed, it ran with your user's permissions, and it had access to everything your user has access to. Treat removal as cleanup, not containment.
- 1
Preserve the evidence before you change anything
Save the scan output that produced the hit, with a timestamp. If you delete the package first, you lose the only record of what version was where, and you will need that record for the postmortem and possibly for a customer notification.
bashbumblebee scan --profile deep --exposure-catalog advisory.json --findings-only \ > "findings-$(hostname)-$(date +%Y%m%dT%H%M%S).ndjson" - 2
Rotate credentials that the machine could reach
This comes before cleanup, because it is the part that stops ongoing damage. Assume anything readable from the developer's home directory is gone.
- npm, PyPI, RubyGems, and container registry publish tokens
- Cloud provider access keys, including anything cached by a CLI in the home directory
- SSH keys and any signing keys used for commits or releases
- Personal access tokens for GitHub, GitLab, and CI providers
- Session cookies and browser-stored credentials if browser extensions were in scope
- Environment variables checked into local
.envfiles, which are usually the fastest thing for a payload to read
- 3
Determine the exposure window
Find out when the compromised version was installed, not when the advisory was published. Lockfile history in git is usually the most reliable source, because it records the exact resolved version and the commit date it landed.
bashgit log -p --follow -- package-lock.json | grep -n "<package-name>" | head -40Everything that ran between that date and now is inside the window: every CI job, every local build, every developer who pulled the branch.
- 4
Widen the scan to the rest of the fleet
One confirmed machine almost never means one affected machine. Push the same catalog to every endpoint and collect findings centrally, because a partial answer here produces a false all-clear that is worse than no answer.
bashbumblebee scan --profile deep --ecosystem npm \ --exposure-catalog advisory.json --findings-only \ | tee "/var/log/bumblebee/ir-$(hostname).ndjson" - 5
Remove the package and rebuild from a clean lockfile
Delete the installed tree rather than upgrading in place, because an upgrade leaves whatever the old version wrote on disk. Then reinstall with scripts disabled so you are not re-running install hooks during recovery.
bashrm -rf node_modules npm ci --ignore-scriptsAlso clear the package manager caches. A poisoned tarball sitting in a local cache will happily reinstall itself into your clean tree.
- 6
Re-scan to confirm the machine is clean
Run the same catalog again and require empty output.
--findings-onlymakes this a clean pass/fail signal: no output means no match. Record the clean result alongside the original finding so the timeline is complete.
Once the immediate response is done, work through the follow-up items while the details are still fresh. These are the ones teams routinely skip and later wish they had not:
- Audit registry publish history for every rotated token, in case the attacker published under your identity
- Check CI logs across the exposure window for outbound requests to unfamiliar domains during install steps
- Review git history for commits or tags you cannot attribute to a real person
- Confirm no long-lived secrets were baked into build artifacts released during the window
- Add the compromised package and version range to your permanent catalog so re-introduction is caught automatically
- Write the timeline down while you still remember it, including what the scan found and when
CI runners deserve the same treatment as laptops here. They hold the most valuable credentials on the network and they install dependencies far more often than any human does, which is why hardening your GitHub Actions workflows belongs in the same remediation pass.
Checking MCP Configs
MCP (Model Context Protocol) configs define which external servers your AI coding tools connect to. After the ContextCrush incident in March 2026, this became a real attack surface: a malicious MCP server can exfiltrate code context, inject prompts, or pivot into your development environment.
Bumblebee checks these config files: mcp.json, .mcp.json, claude_desktop_config.json, mcp_config.json, mcp_settings.json, cline_mcp_settings.json, Gemini CLI/Code Assist settings, and ~/.claude.json.
bumblebee scan --profile baseline --ecosystem mcpThis outputs every MCP server config found on the machine, with the server URL and transport type for each entry. You can then cross-reference against known compromised servers or your organization's allowlist.
For a quick browser-based check of a single config file without installing anything, use the MCPConfigCheck tool.
Scheduling Regular Scans
A one-off scan is useful during an incident. A daily scan turns Bumblebee into continuous inventory. Here's a crontab entry that runs at 2 AM every day:
0 2 * * * /usr/local/bin/bumblebee scan --profile baseline > /var/log/bumblebee/inventory-$(date +\%Y-\%m-\%d).ndjson 2>&1On macOS, launchd is more reliable than cron for scheduled tasks. Here's a plist that achieves the same thing:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.bumblebee.scan</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/bumblebee</string>
<string>scan</string>
<string>--profile</string>
<string>baseline</string>
</array>
<key>StandardOutPath</key>
<string>/var/log/bumblebee/inventory.ndjson</string>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>2</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
</dict>
</plist>For security teams managing a fleet, forward these NDJSON files to a central log aggregator (Splunk, Elastic, or even a simple S3 bucket). When an advisory drops, you query your existing inventory instead of scanning every machine in real time.
Wiring the Scan Into CI
A scheduled scan tells you about a problem the next morning. A scan in CI tells you before the code merges, which is the difference between a fix and an incident. The pattern is the same one you already used for incident response: a catalog plus --findings-only, evaluated as pass or fail.
Rather than depending on a specific exit code, treat any output at all as a failure. --findings-only prints nothing when nothing matches, so a line count is a reliable gate that will not break if the tool's exit-code behaviour changes between versions:
#!/usr/bin/env bash
set -euo pipefail
CATALOG="${1:-security/advisories.json}"
OUT="$(mktemp)"
bumblebee scan \
--profile project \
--exposure-catalog "$CATALOG" \
--findings-only > "$OUT"
if [ -s "$OUT" ]; then
echo "Supply chain gate FAILED: known-bad packages present"
cat "$OUT"
exit 1
fi
echo "Supply chain gate passed: no catalog matches"The runner needs the dependency tree on disk before the scan runs, so install first. Install with scripts disabled, because you do not want a lifecycle hook from an unreviewed package executing on a runner that holds your deploy credentials.
Check bumblebee scan --help on the version you pinned before wiring this up. Profiles differ in whether they infer roots or expect you to name them, and a CI job that scans the wrong directory reports a clean result forever.
name: Supply chain gate
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies without running scripts
run: npm ci --ignore-scripts
- uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: Build Bumblebee from a pinned commit
run: |
git clone https://github.com/pplx-oss/bumblebee.git /tmp/bumblebee
git -C /tmp/bumblebee checkout "${{ vars.BUMBLEBEE_COMMIT }}"
go build -C /tmp/bumblebee -o /tmp/bin/bumblebee ./cmd/bumblebee
echo "/tmp/bin" >> "$GITHUB_PATH"
- name: Run supply chain gate
run: bash scripts/supply-chain-gate.sh security/advisories.jsonPinning the commit rather than tracking the default branch matters more than it looks. A security tool you rebuild from HEAD on every run is itself an unpinned dependency with the ability to read your entire workspace, which is the exact category of risk you installed it to manage.
Keep the catalog in the repository next to the workflow. Adding a compromised package to a JSON file in a pull request is a reviewable, auditable action, and it means the gate strengthens through the same process as the rest of your code.
| Where it runs | Profile | Why |
|---|---|---|
| Pull request | project | Fast, scoped to the checkout, keeps the feedback loop tight enough that people do not route around it |
| Nightly scheduled job | baseline | Catches packages that entered through global installs or tooling rather than the repo |
| Developer laptop, on demand | project | Same gate a developer can run locally before pushing, so CI is never the first place they hear about it |
| Active incident | deep | Completeness beats speed, and the noise is acceptable because someone is reading every line |
What Bumblebee Doesn't Do
- Windows support (macOS and Linux only currently)
- Runtime behavior detection (it reads files, doesn't execute them)
- SBOM replacement (covers dev endpoints, not production builds)
- EDR replacement (reads on-disk state, doesn't monitor running processes)
- Network analysis (doesn't check what packages do when executed)
- Remediation (reports findings, doesn't auto-fix or quarantine)
Bumblebee fills the specific gap between SBOMs and EDR. It tells you what's on disk right now, not what ran or what shipped. Pair it with a VS Code extension audit for a more complete picture of your local attack surface.
What Any Scanner Fundamentally Cannot Catch
The previous section lists what this particular tool has not built yet. This section is different: these are limits that no inventory scanner can engineer its way past, and knowing them is what keeps a green scan from turning into false confidence.
An inventory scanner answers exactly one question well: what is on this disk right now, by name and version. Every question that depends on behaviour, intent, or timing is outside its reach by construction.
- A compromise nobody has published yet. Catalogs are lists of known-bad things. The window between a malicious version going live and an advisory naming it is measured in days, and during that window every catalog-based check returns clean, correctly and uselessly.
- Malicious code in a legitimately named package. If
left-pad@1.3.0is the version everyone expects and the tarball was swapped, the inventory shows a normal package at a normal version. Name and version matching cannot see inside the file. - Anything that already ran. A payload that executed during install and deleted itself leaves nothing on disk to inventory. Read-only file scanning is a snapshot of surviving state, not a record of history.
- Payloads that live outside package directories. Malware that writes to a temp directory, installs a launch agent, or appends a line to your shell profile is not a package, so it is not in a package inventory.
- The gap between the published tarball and the source repository. What ships to a registry is not guaranteed to match what is visible on GitHub. An inventory records what you installed, not whether what you installed matches what you reviewed.
- Compromise of the machine doing the scanning. Any tool running as your user reports what the operating system tells it. If the host is already owned at a level that can lie to userspace, the scan output is just as compromised as everything else on the box.
None of that makes the scan worthless. It makes it one layer. The layer above it is behavioural (what the code does when it runs), the layer below it is provenance (whether the artifact you installed is the artifact that was built from the source you read), and a scanner sits squarely between the two without replacing either.
Provenance is where the biggest gains are available right now, because it is the layer most teams have nothing at all. Signed builds, published attestations, and integrity checks on anything you load from a third party all attack the problem earlier than a scan can. Subresource integrity is the cheapest version of this idea, and you can generate SRI hashes for third-party scripts in a minute.
The honest framing to carry into a security review: a clean Bumblebee scan means "none of the things we currently know are bad are sitting on this machine." That is genuinely valuable and it is not the same sentence as "this machine is clean." Attacks that hide in install-time lifecycle hooks or in a compromised CDN delivery path are specifically designed to be true and invisible at the same time.
Frequently Asked Questions
What does Bumblebee scan for?
Bumblebee inventories installed packages across 8 ecosystems (npm, pnpm, Yarn, Bun, PyPI, Go modules, RubyGems, Composer), MCP configs, and editor/browser extensions. It's read-only and never executes package managers or install scripts.
Is Bumblebee safe to run?
Yes. It's read-only by design and never executes install scripts or package managers. The binary makes no network calls during scanning. It's Apache 2.0 licensed and fully source-available, so you can audit the code before building.
What is the difference between Bumblebee and an SBOM?
SBOMs inventory your production build artifacts (what ships to users). Bumblebee inventories your developer machine (what's installed locally). They complement each other: an SBOM tells you what's in the container you deployed, while Bumblebee tells you what's sitting on every engineer's laptop right now.
How do I check my MCP configs for known compromises?
Run the following command to list all MCP server configs found on your machine:
bumblebee scan --profile baseline --ecosystem mcpFor a browser-based check without installing anything, use the MCPConfigCheck tool. It validates a single config file against known-bad server patterns.
Does Bumblebee work on Windows?
Not currently. Bumblebee supports macOS and Linux. Windows support is tracked in the GitHub issues but has no release date yet. If you need Windows coverage, you can run it inside WSL2 to scan packages visible from the Linux filesystem.
How do I cut down the noise from my first scan?
Scope before you suppress. A baseline scan on an active machine returns thousands of records by design, and most of the noise comes from stale clones, test fixtures, and duplicate transitive versions rather than from anything the tool got wrong.
- Use `--profile project` for routine checks so you are looking at the directories you actually work in, and reserve
--profile deepfor incidents - Filter by ecosystem with
--ecosystemwhen you are chasing a specific advisory, instead of reading the full inventory - Use an exposure catalog with `--findings-only` so the default output is empty and any output at all is meaningful
- Triage by blast radius, starting with MCP configs and extensions rather than with the largest ecosystem
If you do end up writing suppressions, pin them to a package, a version, and a path. A suppression by package name alone will still be active on the day a compromised version of that package arrives.
Should I run this in CI or only on developer machines?
Both, for different reasons. CI catches a bad dependency before it merges, and developer machines catch everything CI never sees: global installs, editor extensions, browser extensions, and MCP servers, none of which appear in a repository checkout.
| CI scan | Endpoint scan | |
|---|---|---|
| Scope | The repository checkout and its installed tree | The whole machine across all ecosystems and tooling |
| Catches | A compromised dependency entering through a pull request | Anything already installed, however it arrived |
| Misses | Global installs, extensions, MCP configs | A bad dependency in a branch nobody has checked out locally |
| Timing | Before merge, blocking | Scheduled or on demand, reporting |
| Best profile | project | baseline for routine runs, deep for incidents |
If you can only do one to start with, do CI, because it prevents new exposure rather than just measuring existing exposure. Add endpoint scanning as soon as you can, since the extension and MCP surface is invisible to CI entirely.
If the scan comes back clean, am I actually safe?
A clean scan means something precise and narrower than it sounds: none of the packages your catalog currently lists as bad are present on this disk right now. It is not a statement about whether the code you have is safe.
Three gaps survive a clean result. A compromise published in the last few days may not be in any catalog yet. A malicious tarball published under a normal package name at a normal version looks identical to a clean one in an inventory. And a payload that ran during install and removed itself leaves nothing left to find.
Pair the scan with controls that work at different layers: disable install scripts by default, review lockfile diffs like real code, and verify provenance for anything you load from a third party. The source map exposure problem is a good reminder that what actually ships often diverges from what you think you published.
Related Articles
GitHub Actions Security: 7 Misconfigurations to Avoid
The 7 GitHub Actions misconfigurations behind real supply chain attacks: weak GITHUB_TOKEN scope, pull_request_target, unpinned actions, script injection.
WordPress CDN Supply Chain Attack 2026: What Happened and How to Check Your Site
The OptinMonster, TrustPulse, and PushEngage supply chain attack (June 2026) hit 1.2M sites. Here's exactly how it worked, how to check if you were compromised, and how to recover.
How to Audit Your VS Code Extensions for Security
The GitHub breach happened through a VS Code extension. Here's how to check what you have installed and reduce your exposure in 10 minutes.