Perplexity's Bumblebee: a read-only supply-chain check for the developer laptop
A read-only inventory check for the developer laptop supply chain
In ~7 mins: what Mini Shai-Hulud did to 324 npm packages in two weeks, why Perplexity open-sourced Bumblebee to triage the fallout, the 8 on-disk surfaces it reads, the 6-step scan loop that drives it, and the 5 rough edges in v0.1.1. Appendix: full how-to reference at the end.
The Mini Shai-Hulud worm chewed through hundreds of npm packages in the last two weeks.
Mini Shai-Hulud hit 324 antv npm packages across 643 malicious versions on May 19, dropped malicious tanstack releases on May 11 with valid SLSA Build Level 3 provenance, and crossed into PyPI through lightning 2.6.2 and 2.6.3 on April 30. Microsoft, Snyk, Socket, and Wiz all published incident reports inside three weeks.
Bumblebee is the read-only scanner Perplexity open-sourced on May 22 to handle exactly that scenario. Apache-2.0, written in Go, no third-party dependencies, single static binary.
The narrow question it answers: when an advisory names a bad package, which developer laptops in the fleet still show it on disk right now?
Where Bumblebee fits
SBOMs describe what shipped into a build. EDRs describe what ran in a process. Neither describes what is still sitting in a lockfile on a developer’s laptop while the responder is on the call.
That gap matters more in 2026 than it did two years ago. Shai-Hulud 2.0 spread through Zapier, ENS, PostHog, and Postman in November 2025. Mini Shai-Hulud hit SAP npm packages, PyTorch Lightning, and the AntV npm scope across late April and May 2026.
The malicious code reached developer machines through normal npm install flows long before any production SBOM updated.
The repo is moving fast. Bumblebee crossed 2,900 GitHub stars and 1,600+ release binary downloads in the four days between v0.1.1 shipping on May 22 and now. Open issues and PRs already cover Windows defaults, NuGet, Homebrew, OSV.dev integration, and a human-readable terminal mode.
What it actually does in plain English
Bumblebee walks a list of known on-disk metadata locations, normalizes what it finds into NDJSON records, and (optionally) cross-checks those records against an exposure catalog the operator supplies.
It does not call npm ls. It does not call pip show, go list, bundle list, or composer show. It does not read source files, fetch threat intel at runtime, or watch processes. It does not ship a built-in advisory feed.
That last rule is the design pivot. During a fresh compromise, running the same package manager that just shipped the bad release is the worst move a responder can make. Bumblebee is built around never doing that.
Eight ecosystems are covered in v0.1.1:
The MCP, editor, and browser surfaces are the interesting part. Most existing supply-chain tools stop at language registries. Bumblebee treats Claude Desktop configs, Cursor extensions, and Chrome add-ons as part of the same surface the developer is exposed to.
How the scan loop works
The scanner runs six steps per invocation:
Resolve safe scan roots based on the selected profile.
Walk the file tree, skipping symlinks and over 100 sensitive or noisy directory patterns (.ssh, .aws, .kube, .gnupg, Library/Keychains, Library/Mail, Library/Cookies, browser cache subtrees).
Dispatch each recognized basename to an ecosystem-specific parser.
Normalize names (npm lowercase, PyPI PEP 503).
Emit one NDJSON package record per identity.
If --exposure-catalog was supplied, do exact (ecosystem, name, version) matching and emit a finding record per hit. Close the run with a scan_summary.
Three scan profiles control how much of the disk gets walked:
baseline and project refuse a bare-home root by default. Only deep walks one.
Exact-match-only catalog logic is a deliberate choice. CVE scanners running on version ranges produce noise during a fresh worm wave because the advisory itself is still being scoped. Bumblebee answers a tighter question: did the exact compromised version land on this machine? That maps directly to the incident channel where someone just said “anyone with lightning@2.6.2 raise your hand.”
Safety properties are baked in below the parser layer:
No package-manager execution and no source-file reads.
No bundled threat intel and no network calls during the scan.
MCP env values and key names are dropped, so credentials sitting in Claude Desktop configs never leave the host.
.env and .envrc are skipped even when they fall inside a walked root.
Remote MCP server URLs are reduced to scheme://host before being recorded, so embedded path-segment credentials cannot leak.
The repo ships 161 Go test functions across 23 test files, plus a CI matrix on Ubuntu and macOS that runs go vet, go test -race, a fresh build, bumblebee selftest, and govulncheck.
How to get started
Three commands cover the smoke test on a clean machine:
bash
go install github.com/perplexityai/bumblebee/cmd/bumblebee@v0.1.1
bumblebee selftest
bumblebee scan --profile baseline > inventory.ndjsonRequires Go 1.25+. Zero non-stdlib dependencies.
bumblebee selftest extracts embedded fake-package fixtures to a tempdir, runs the scanner against an embedded exposure catalog, and asserts a fixed finding count. A non-zero exit means the local install can no longer detect what it should.
The baseline scan writes one NDJSON object per line to inventory.ndjson and diagnostics to stderr. The last line is a scan_summary record. Promote a snapshot into a downstream system only when that summary has status=complete. Partial or errored runs are evidence, not deletion signals.
The release binaries, the controlled left-pad fixture, the HTTP sink, and the threat_intel/ catalog runs are all in the appendix at the end of this article.
AlphaSignal Take
Bumblebee is a sharp v0.1, but it ships with five rough edges worth naming.
No Windows release. The v0.1.1 assets are macOS and Linux only, amd64 and arm64. Issue #2 and PRs #4 and #16 cover default root discovery and full Windows support, none merged yet.
No live OSV.dev source. Open issue #21 asks for it. Catalog matching today is limited to the eight files in threat_intel/ (654 total entries) and whatever JSON the operator writes by hand.
Exact-match only. Version-range solving is out of scope by design. The trade-off is real: upstream catalog quality determines accuracy. A wrong version string in a PR-submitted catalog produces a wrong finding rate fleet-wide.
NDJSON-first output. PR #24 adds opt-in terminal output, but main still expects jq for any human reading. Issue #22 is open on that too.
One small documentation gap. The README still prints selftest OK (2 findings in 1ms). The source in cmd/bumblebee/selftest.go asserts expectedSelftestFindings = 3. Tiny issue, but the kind of thing a responder notices the first time they actually run the binary.
The architecture itself is the bet. Operator-supplied catalogs scale faster than vendor-curated advisory feeds in the first hour of a worm wave, when the question is “which laptops have this exact version” and the answer needs to be in Slack before lunch.
Where do you draw the line on endpoint security: the SBOM, or the laptop itself?
All source links are in the first reply. Full breakdown of recent updates + daily signals in our newsletter (link in bio).
Appendix: full how-to reference
Install from the release tarball
macOS Apple Silicon:
bash
VERSION=v0.1.1
curl -L -o checksums.txt "https://github.com/perplexityai/bumblebee/releases/download/${VERSION}/checksums.txt"
curl -L -o bumblebee.tar.gz "https://github.com/perplexityai/bumblebee/releases/download/${VERSION}/bumblebee_0.1.1_darwin_arm64.tar.gz"
shasum -a 256 -c checksums.txt --ignore-missing
tar -xzf bumblebee.tar.gz
./bumblebee version
Linux amd64:
bash
VERSION=v0.1.1
curl -L -o checksums.txt "https://github.com/perplexityai/bumblebee/releases/download/${VERSION}/checksums.txt"
curl -L -o bumblebee.tar.gz "https://github.com/perplexityai/bumblebee/releases/download/${VERSION}/bumblebee_0.1.1_linux_amd64.tar.gz"
sha256sum -c checksums.txt --ignore-missing
tar -xzf bumblebee.tar.gz
./bumblebee versionPreview what a profile will walk
bash
bumblebee roots --profile baselinePrints <root_kind>\t<path> lines without walking anything.
Run a controlled project scan with a custom catalog
bash
mkdir -p /tmp/bee-demo && cd /tmp/bee-demo
cat > package-lock.json <<'JSON'
{
"name": "bee-demo",
"lockfileVersion": 3,
"packages": {
"": { "dependencies": { "left-pad": "1.3.0" } },
"node_modules/left-pad": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz"
}
}
}
JSON
cat > exposure-catalog.json <<'JSON'
{
"schema_version": "0.1.0",
"entries": [
{
"id": "demo-left-pad-1.3.0",
"name": "Demo left-pad exposure",
"ecosystem": "npm",
"package": "left-pad",
"versions": ["1.3.0"],
"severity": "low",
"source": "local test fixture"
}
]
}
JSON
bumblebee scan \
--profile project \
--root "$PWD" \
--exposure-catalog exposure-catalog.json > project.ndjson 2> project.diag.ndjson
jq 'select(.record_type=="finding")' project.ndjsonExpected output: one package record, one finding record, one scan_summary with status=complete.
Suppress packages, keep findings
bash
bumblebee scan \
--profile project \
--root "$PWD" \
--exposure-catalog exposure-catalog.json \
--findings-only > findings.ndjson--findings-only requires --exposure-catalog. It keeps finding, scan_summary, and diagnostics. It drops package records.
Scan against the shipped threat-intel catalogs
From a source checkout or the release archive:
bash
bumblebee scan \
--profile deep \
--root "$HOME/code" \
--exposure-catalog threat_intel \
--findings-only \
--max-duration 10m > threat-findings.ndjsonThe threat_intel/ directory contains 8 catalog files and 654 entries at the studied commit, covering Mini Shai-Hulud npm + PyPI, the AntV / Mini Shai-Hulud npm wave, GemStuffer RubyGems, Laravel Lang Packagist, node-ipc credential stealer, the nx-console VS Code compromise, shopsprint/decimal Go typosquat, and the TrapDoor crypto stealer.
Ship results over HTTP
bash
export BUMBLEBEE_TOKEN="..."
export BUMBLEBEE_DEVICE_ID="laptop-001"
bumblebee scan \
--profile baseline \
--output http \
--http-url https://inventory.example.com/v1/ingest \
--http-auth bearer \
--http-token-env BUMBLEBEE_TOKEN \
--http-gzip \
--device-id-env BUMBLEBEE_DEVICE_IDNDJSON body. Content-Type: application/x-ndjson. HTTPS required for non-loopback hosts. HMAC mode (X-Inventory-Signature: sha256=<hex>) is also available. Signature input is the raw post body, or <timestamp>.<body> when X-Inventory-Timestamp is set. Compression happens before HMAC signing.
Common error modes worth knowing
deep without --root is rejected.
--findings-only without --exposure-catalog is rejected.
--ecosystem cargo (and anything outside the eight supported values) is rejected.
Binary bun.lockb emits a diagnostic only. Text bun.lock is parsed.
macOS deep scans hitting TCC-protected paths produce diagnostics, not findings.









https://github.com/perplexityai/bumblebee