KilimcininKorOglu/claude-code-mods/tree/main/plugins/dep-sentinel
dep-sentinel
Checks each package the model installs against its registry and OSV.dev, and stops a missing, brand-new, look-alike, outdated or vulnerable one, naming the latest version.
About this mod
A Claude Code plugin that intercepts package install commands (npm, pnpm, yarn, bun, pip, uv, poetry, go get/install, cargo add, composer require) before they run. It queries the relevant registry and OSV.dev for each package and blocks the install when the package is unknown, a look-alike of a popular name, published less than 7 days ago, pinned to a non-latest version, or has a known vulnerability. Findings are surfaced to the model as command errors and to the user via the sidebar or transcript. In deny mode, git commit/push/merge are also gated while an unchecked package remains. Install via claude plugin marketplace add KilimcininKorOglu/claude-code-mods and claude plugin install dep-sentinel@kilimcininkoroglu-mods. Requires Claude Code 2.1.288+ and reaches the network (L3).
Installation
Check the author's README for the marketplace and plugin name first. Commands may change as the repository evolves.
claude plugin marketplace add KilimcininKorOglu/claude-code-mods claude plugin install dep-sentinel
Original text / README
dep-sentinel
When the model installs a package, it types a name from memory. That name can be misspelled, point to a package that does not exist, belong to a look-alike published last week, or pin a version with known vulnerabilities. This mod checks every package before the install runs: it asks the package's registry and OSV.dev, and it stops the install when something is wrong. The model reads why, and which version is the latest.
What it does
-
It reads the Bash command without a shell and finds the packages it installs:
- npm:
npm i|install|add,pnpm add,yarn add,bun add|install; - PyPI:
pip install,pip3 install,python -m pip install,uv pip install,uv add,poetry add; - Go:
go get,go install; - crates.io:
cargo add; - Packagist:
composer require.
A local path, a URL, a git source, a requirements file (
-r) and an editable install (-e) are not checked. It checks at most 10 packages per command. - npm:
-
For each package it asks the registry: registry.npmjs.org, pypi.org, proxy.golang.org, crates.io or repo.packagist.org. A Go package path is looked up at its module, the nearest parent path the proxy knows. Claude Code hands a mod at most 4 MiB of an answer and cuts the rest without a sign, and an npm document can be larger (webpack 5 MB, vite 39 MB). For such a package the mod runs
npm view <name> time.created dist-tags.latest versions --jsoninstead; without npm on the PATH the package stays unchecked. A larger answer from another registry is reported as unchecked, by name. -
It asks OSV.dev for known vulnerabilities of the version that would be installed: the pinned version, else the latest.
-
It stops the install when:
- no registry knows the package;
- the name is one or two edits away from a popular package name (one edit under 7 characters, none under 4), unless the package is over a year old with 10 or more versions;
- the package was first published less than 7 days ago; a new version of an older package is not stopped;
- an exact pin (
[email protected],requests==2.25.0,tokio@=1.38.0,go get [email protected],vendor/pkg:2.0.0) is not the latest version; the reason names the latest, and also the latest in the same major version when that differs; - OSV.dev lists a known vulnerability for that version; the reason names the ids and the versions that fix them.
-
The model reads the reasons as the command's error, with the instruction to install the latest version or the right name. When you need exactly that package, the model tells you why and runs the command again with the
DEP_SENTINEL_SKIP=1prefix. The mod logs such a skip. -
When a registry or OSV.dev cannot be reached, the install runs, and the model reads which package ran unchecked and why. At the same moment you get one line in the transcript:
dep-sentinel: the install ran unchecked for: lodash (api.osv.dev answered HTTP 503)The note and the line are separate channels: the model never reads the line, and you never read the note.
-
With the sidebar open, the unchecked and the skipped packages go into its stream instead, one line per package, and the transcript stays clean. An unchecked package shows its name red and the reason faint. A package skipped on request is yellow, because you asked for it. An entry stays until newer ones push it off the pane. With the sidebar closed, or not installed, the lines land in the transcript as above.
-
An unchecked finding is never a remembered answer. The check it is owed runs again, so it closes in two ways. A later install of the same package in the same ecosystem checks it (an npm
lodashdoes not close a PyPIlodash), and a guarded git command runs the check itself, in both modes. The entry is cleared and a new one takes its place:dep-sentinel: a later install checked the packages that stayed unchecked: lodash dep-sentinel: the registry and OSV.dev answered for the packages that stayed unchecked: lodashWhatever the late answer has to say gets its own entry, because the install it belongs to already ran. There the pinned old version and
has N known vulnerability(ies)are red, the latest version andfixed in Xgreen, andno fixed version is listedand the age of a new package yellow:dep-sentinel: the check that was owed says: [email protected] has 1 known vulnerability(ies) on OSV.dev: GHSA-29mw-wpgm-hmr9; fixed in 4.17.22With the sidebar closed the same texts are transcript lines. The model reads none of this.
-
The same check runs at the end of each main-loop turn, and the packages still open reach the model as one note with its next prompt:
dep-sentinel: 1 package(s) are still installed unchecked: lodash. Run the install again so the registry and OSV.dev answer, or take the package out.That is one note per turn, not one per prompt. Without it the finding would be said once, at the install, and then sit in the pane while the model forgot it. You read nothing new, because the pane already carries the same finding.
-
In
denymode the mod also stopsgit commit,git pushandgit mergewhile a package stays unchecked. The gate runs the owed check first, so a package that failed only because the network was down opens the gate by itself. A package the registry still does not answer for stops the command. There is no bypass; only you turn the gate off, with/dep-sentinel mode note.notemode is the default and stops no git command, but it runs the same check at a git command, so a settled finding does not stay in the pane. An install is stopped in both modes, as above.
In the live check npm install --dry-run [email protected] was stopped with the latest version 4.18.1 and 6 OSV ids, npm install --dry-run lodahs was stopped as a look-alike of lodash with OSV id MAL-2025-25502, and npm install --dry-run left-pad ran.
Command
/dep-sentinel on or off, the mode, and the packages still unchecked
/dep-sentinel on | off on by default
/dep-sentinel mode note an unchecked package is only reported; the default
/dep-sentinel mode deny a commit, a push and a merge also stop while a package stayed unchecked
Install
claude plugin marketplace add KilimcininKorOglu/claude-code-mods
claude plugin install dep-sentinel@kilimcininkoroglu-mods
Function hooks are early access. Claude Code 2.1.288 and later load them by default, so there is nothing to switch on.
After installing
- Restart Claude Code.
What it can reach
Validated with claude plugin validate on Claude Code 2.1.283:
❯ ./register.ts hooks: session.start, command.run{command=dep-sentinel}, turn.complete, prompt.submit, tool.call{tool=Bash}
❯ ./register.ts calls: $.clock.now, $.command.register, $.http.fetch (via fetchText, osvCheck), $.process.run (via npmView), $.sidebar.clear (via dropEntry), $.sidebar.set (via toPerson), $.store.get (via isEnabled, readSettings), $.store.set (via runCommand, setMode), $.ui.log (via toPerson)
Reach L3: it reaches the network.
1. Reads: the Bash command text
2. Runs: npm view, only for an npm package whose registry document passes the 4 MiB a fetch reads
3. Sends: each package name, and its version, to its public registry and to api.osv.dev, at an install and again at a guarded git command and at each turn's end while a finding is open; a note to the model and one line to the transcript when a check failed, and one more note with the next prompt while a finding stands; nothing else leaves the machine
4. Persists: in $.store, the on/off setting and the mode
5. Hostile input: a package name comes from the model's command; it reaches a registry only inside a URL path or a JSON body, and a registry answer is read as data
Limits
- The popular-name list is fixed in
hooks/popular.ts(about 150 npm and PyPI names, fewer for the other registries). A look-alike of a package not on the list goes unseen. - A version range (
^18,>=4,cargo add [email protected]) is not stopped as old, because the installer resolves it. Its latest version is checked on OSV.dev. - npm package documents are large (16 MB for typescript, measured), so a check can take a few seconds.
- An install that a script, an alias or a lockfile runs (
npm ci,pip install -r) is not checked. - The
denymode has no bypass. When a registry stays unreachable, you turn the gate off with/dep-sentinel mode note. - A git command while a finding is open waits for that check, so the first commit after a failed install takes as long as the registry does.
- The gate reads the command text. A commit through a script or an alias that hides
git commitis not stopped.
Development
make install # eslint, typescript-eslint, typescript
make lint # complexity limit 10, the build fails above it
make typecheck # needs .claude/types/ from /plugin-types
make validate
make test # claude plugin test
