One dashboard for every package manager on your Mac: inventory, CVE audits, malware checks and updates for uv, npm, Homebrew, pip, Go, Cargo, Ruby gems, Docker images, and the lockfiles of your own projects.
Screenshots use the built-in demo data (demo.py), not a real machine.
| 📦 One place for everything installed | uv tools, npm globals, Homebrew formulae and casks, pip, Go binaries, Cargo crates, Docker images and stray binaries. It also spots managers it doesn't track yet (pipx, pnpm, yarn, bun, mas, conda). |
| 🆕 New and removed package detection | Every scan is compared with the last one. New installs get a NEW tag for a week, and anything added or removed lands in the inbox. |
| ⬆️ Updates | Update everything, one manager, or one package. Casks whose app is running are skipped, npm stays on its major version, and locally built tools are left alone. |
| 🛡️ CVE audits | pip-audit, npm audit, brew vulns, govulncheck and OSV, merged into one list you can filter by severity, with fixed versions. |
| ⏳ Cooldown period | New releases wait a few days before being installed, because hijacked releases are usually pulled within days. "in cooldown period, 3 days remaining", with an update anyway button. |
| ☠️ Malware checks | Everything installed, dependencies included, is checked against OSV's malicious-packages list on every scan, and again just before an update. Known-bad versions can't be installed. |
| 🔌 Socket.dev (optional) | Socket's verdicts block bad versions too, and each pending update is labelled with what it newly does (install scripts, network access, a maintainer change…). |
| 📁 Projects | The lockfiles of every project in a folder you choose are checked for malware and CVEs. One click rewrites a lockfile to fixed versions without installing anything. |
| 🐳 Docker images | Package lists are read from docker save output (no container is ever started) and checked for malware and CVEs, including Debian, Ubuntu and Alpine packages. |
| 🔔 Schedules and notifications | Scans every 6 h and audits daily, with optional auto-update. macOS notifications for new packages, vulns, malware and failures. |
| 📟 Live job output | Progress bars with time estimates from previous runs. Click a running job to watch its terminal output. |
| 🧭 Menu bar icon | High/critical count at a glance, spins while jobs run, right-click for quick actions. |
Needs macOS 13+ on Apple Silicon and the Xcode Command Line Tools (xcode-select --install), which
provide /usr/bin/python3 and swiftc. Nothing else: the service is standard-library Python.
git clone https://github.com/adamtty/pkgdash.git
cd pkgdash
./install.sh # install, or restart after any edit
./install.sh uninstall # remove (state in ~/.local/share/pkgdash is kept)Then open http://127.0.0.1:7878. The first visit opens the setup wizard:
- Managers: pkgdash detects which package managers are installed; untick any you don't want tracked.
- Schedule & safety: scan and audit intervals, auto-update (off by default), the cooldown period and notifications.
- Projects: whether to check your projects' lockfiles, and which folder they're in (
~/Documents,~/Developer,~/code…). - Socket.dev (optional): paste an API token, which is tested and stored in your login Keychain.
Nothing runs until setup is done. Afterwards everything stays editable on the ./setup page, which can also run the wizard again.
install.sh copies the service to ~/.local/share/pkgdash/app, because launchd agents can't read protected
folders like ~/Documents. It builds the menu bar app into ~/Applications/pkgdash.app and loads the
LaunchAgents com.adam.pkgdash and com.adam.pkgdash.menu. Re-run it after any edit. State, job logs
and service.log live in ~/.local/share/pkgdash.
/usr/bin/python3 -I demo.py # → http://127.0.0.1:7890The real dashboard with made-up packages, findings and projects. Nothing is scanned, installed or sent anywhere, and its state lives in a temporary folder that's deleted when you stop it. It's also how the screenshots are made.
| File | What it is |
|---|---|
server.py |
The service: standard-library Python run by /usr/bin/python3 -I |
index.html |
The dashboard, including the setup wizard (a single page, no build step) |
menubar/PkgdashMenu.swift |
Menu bar icon, plus the reader for project lockfiles |
install.sh |
Install / restart / uninstall |
demo.py |
The demo above |
| Manager | Scan | Audit | Update |
|---|---|---|---|
| uv | uv tool list [--outdated] + receipt pins/sources |
pip-audit per tool env |
uv tool upgrade <tool> one by one (skips tools installed from protected folders) |
| npm | npm ls/outdated -g |
npm audit on a scratch copy of each global |
reinstall all globals @latest (npm kept on its major unless enabled) |
| brew | brew update + list/outdated |
brew vulns (OSV) |
upgrade formulae + casks (skips running apps), cleanup |
| pip | Homebrew Pythons + --user installs in macOS python; flags strays |
pip-audit |
disabled by design (use uv) |
| go | go version -m + go list -m @latest |
govulncheck -mode=binary |
go install <pkg>@latest (rebuilds with current Go) |
| cargo | cargo install --list + crates.io |
OSV (top-level crate only) | cargo install --locked |
| gem | Homebrew Ruby's gems + gem outdated; the ones that ship with Ruby only count as outdated when OSV knows a vulnerability in them |
OSV, every installed version | gem install <gem> --version <scanned> --conservative |
| docker | docker save per image (cached by image id; nothing runs): OS packages, Python, npm and Ruby gems |
OSV, incl. Debian/Ubuntu/Alpine | — |
| other | ~/.local/bin, /usr/local/bin, extra managers | — | — |
Single packages: each outdated package has its own > update button (brew upgrade --formula/--cask <name>,
uv tool upgrade <name>, npm i -g <name>@latest, go install …@latest, cargo install --locked <name>, gem install <name> --version …),
followed by a re-scan. The server only accepts names its last scan found.
Cooldown (cooldown_days, default 5)
Updates wait until a release is that old, since hijacked releases are usually caught and pulled within days.
uv uses --exclude-newer and npm --before, which also cover their dependencies; brew/go/cargo/gem skip packages
still cooling (cargo and gem are pinned to the scanned version). Release dates come from PyPI, the npm registry, the
Go proxy, crates.io and rubygems.org; Homebrew has none, so formulae use the build time of the version's first bottle on
ghcr.io, and casks (no bottles) start the clock when a scan first sees the version. pip packages show no
cooldown, since pkgdash doesn't update them.
Malware check (OSV MAL- advisories)
Every scan asks OSV's malicious-packages list about everything installed, dependencies included (uv envs, the full npm tree, Go module deps, cargo crates), and about each pending version; updates check again just before installing. A flagged pending version is blocked even for "update anyway"; flagged installed packages raise a red banner (click a line for the advisory), an inbox event and a notification. Homebrew isn't covered (OSV has no Homebrew malware data).
Socket.dev (socket_check; npm, PyPI, crates.io)
The malware check also asks Socket's API. Its blocking verdicts (malware, typosquats, protestware…) block
updates the same way, and each pending version gets a note: socket ✓, not analysed yet, or what it starts
doing that the installed version didn't (new: install script · network access). Add the token on the setup
page; it only needs the packages:list scope (pkgdash calls organizations, quota and the batch PURL
lookup, read-only). It's tested against Socket, then stored in the login Keychain (item pkgdash-socket-token) through
security -i on stdin, so it never appears on a command line, and it's never written to pkgdash's own
files. Answers are cached in state.json (installed versions for 3 days, pending ones for a scan) because the
free plan's quota is small (500, 100 per lookup); the settings page shows what's left.
The projects page checks the lockfiles of every project in the folder chosen on the setup page
(package-lock.json, yarn.lock, uv.lock, poetry.lock, Cargo.lock, Gemfile.lock, pinned requirements.txt). It
must be inside your home folder. launchd agents can't read protected folders like ~/Documents, so the
menu bar app (which macOS asks you to allow) finds the lockfiles, skipping node_modules/.git/venvs/
target, and sends new or changed ones to the service hourly or when a scan asks; copies live in
~/.local/share/pkgdash/projects.
- Scan: every dependency against OSV's malware list (Socket gets a couple of lookups per scan, changed projects first).
- Audit: every dependency's CVEs.
- fix lockfile (npm, uv, cargo): the menu bar app shows what it's about to run and asks first, then runs
npm audit fix --package-lock-only --ignore-scripts,uv lock --no-build --upgrade-package …orcargo update …. These rewrite the lockfile only: nothing is installed and no package code runs. Review the result withgit diffin the repo.Gemfile.lockisn't fixed: Bundler would run the project'sGemfile, which is Ruby code, so runbundle update <gem>there yourself. - ignore: for deliberate targets and other people's code. Their CVEs and malware stop counting.
Project counts stay out of the dashboard's headline totals. install.sh only rebuilds the menu bar app when
its source changes, since every rebuild makes macOS ask for folder access again.
pkgdash handles data it doesn't control (package names and versions, advisories, Docker images, other people's projects) and drives tools that install software, so:
- The service binds to loopback only, rejects foreign
Host/Originheaders (DNS-rebinding guard), and needs a per-process token for every POST, so web pages can't trigger anything. Only fixed actions run, never free-form commands, and package and project names must match the last scan. - Nothing runs on a schedule until setup is done, and turned-off managers are never scanned or updated.
- The menu bar app holds macOS permission to read your projects folder, so it's the part kept tightest:
- It asks before every lockfile fix, showing the project, the command and the exact binary. A fix requested through the service by some other program on your Mac still needs you to click.
- It runs exactly three lockfile commands, by absolute path, with a fresh environment (nothing from the
editable LaunchAgent, such as
NODE_OPTIONS, reaches them). Settings that would let a project's own.npmrcor.cargo/config.tomlpick a binary are pinned. - It never runs
gitin a project, since a cloned repo's own config (core.fsmonitor,diff.external) could make git run commands. It compares the lockfile itself instead. - It's signed with the hardened runtime, so injected
DYLD_*libraries are ignored, and the projects folder must be inside your home folder (not~/Library).
- The dashboard escapes everything it shows and only links advisory references that use
http(s). - Registry and OSV requests must be
https://. Notifications pass package names toosascriptas arguments, never as script source. - The Socket.dev token lives only in the macOS Keychain.
The repository is protected as well: every commit on main is signed and outside changes must pass CI
(lint, shellcheck, zizmor, the Swift build and the tests), with CodeQL (Python, JavaScript, Swift and
the workflows), a weekly OpenSSF Scorecard,
SHA-pinned actions on StepSecurity Harden-Runner, Renovate with a 3-day release delay, and secret scanning
with push protection. Found a problem? See SECURITY.md to report it privately.
/usr/bin/python3 -m unittest discover -s tests -vThe tests start real servers from the checkout, each with a throwaway home folder and its own port, so they never touch an installed pkgdash, and setup is never finished in them, so nothing gets scanned or updated. They cover the Host/Origin/token checks, settings validation, turned-off managers, the Socket token check, the settings migration and the demo.
The lockfile parsers read files from other people's projects, so CI also fuzzes them with
Atheris (coverage-guided; tests/fuzz_lockfiles.py, seeded from
tests/fuzz-corpus/), along with the projects-folder check (tests/fuzz_projects_root.py). Atheris
only runs on Linux x86-64; inputs it has found are kept as regression tests in test_server.py.





