# Safer-dependencies is a security layer for Claude Code that audits dependencies

> Source: <https://github.com/robert-auger/safer-dependencies>
> Published: 2026-08-25 04:39:58+00:00

When AI coding assistants like Claude add packages to your project, they often pick whatever version sounds right — without checking whether it has known security vulnerabilities, whether the package is still actively maintained, or whether the name is a typo away from a malicious lookalike.

safer-dependencies is a security layer for Claude Code: it sits between Claude and your manifest files and runs its security checks automatically: vulnerable installs are denied before they run, and a risky version written to a manifest is corrected on disk right after the write. It detects and fixes risky dependencies — CVEs, typosquats, abandoned packages, and version-age issues, plus a cooldown period on brand-new releases — across npm, PyPI, RubyGems, Maven, Go, Rust, and PHP (Composer). See [CAPABILITIES.md](/robert-auger/safer-dependencies/blob/main/CAPABILITIES.md) for exactly what is and isn't covered.

New here?[GETTING-STARTED.md]takes you from zero to a working install in about five minutes.

Security & privacy:see[SECURITY.md](vulnerability disclosure),[PRIVACY.md](data egress, no telemetry), and[CAPABILITIES.md](what the tool defends against and what it doesn't).

License (source-available — NOT OSI "open source"):Free to use and modify for your own purposes,including for-profit/company internal use and building products you sell. A separate paid license is requiredonlyto monetize the softwareitself— selling it, shipping it inside a product or service that is sold, or offering its functionality to third parties for a fee (including hosted/SaaS/API). Redistribution and derivatives must keep the license and credit this project. See(Section 4 for the commercial restriction); commercial-license requests via[LICENSE][github.com/robert-auger].

[Getting started](#getting-started)— zero to installed in about five minutes[What it does](#what-it-does)[How it works](#how-it-works)[What triggers it](#what-triggers-it)[What's in this repo](#whats-in-this-repo)[Supported ecosystems](#supported-ecosystems)[Install](#install)[Warning levels](#warning-levels)[Audit log](#audit-log)[Requirements](#requirements)[FAQ](#faq)

** GETTING-STARTED.md** takes you from zero to a working install in about five minutes — prerequisites, the interactive install, and verification. For the full install reference (global/project/manual installs, Windows specifics, the

[permissions allowlist](/robert-auger/safer-dependencies/blob/main/INSTALLATION.md#permissions-allowlist), updating, and uninstalling), see

**.**

[INSTALLATION.md](/robert-auger/safer-dependencies/blob/main/INSTALLATION.md)**Everyday use:** once the hooks are installed, there's nothing to run — safer-dependencies works automatically in the background. As Claude adds or installs packages, it **flags risky dependencies and upgrades vulnerable versions to a safe one in place** — and blocks a known-vulnerable install before it even runs — so unsafe packages are caught and corrected without you having to ask. You can still invoke it directly any time: *"is axios@1.2.0 safe?"*,

*"check safer-dependencies setup"*, or

*"show safer-dependencies stats"*.

When Claude is about to add a package to your project, safer-dependencies intercepts and runs 5 checks:

**Provenance**-- official registry, typosquat detection (npm/PyPI/RubyGems/Maven/crates.io), package age** Version age**-- picks the newest stable version published 7+ days ago (cooldown window)** Vulnerability scan**-- OSV API, with ecosystem-native tools (npm audit, pip-audit, bundle audit) when available** Hash-pin integrity**-- for PyPI`requirements.txt`

lines with`--hash=sha256:...`

pins, the declared hash is validated against PyPI's published hashes; mismatch emits a WARNING**Abandoned & stale packages**-- known-abandoned packages (e.g.`paperclip`

,`request`

,`pycrypto`

,`github.com/dgrijalva/jwt-go`

) are hard-blocked immediately with a suggested replacement; packages with no stable release in 2+ years get an advisory`STALE:`

warning. Hard-blocked packages are removed from the manifest and Claude will ask how to proceed; stale-only packages are left in place.

If issues are found, Claude emits warnings and may step back to a safer version. All checks are logged to `~/.claude/safer-dependencies-audit-YYYY-MM.log`

(one file per calendar month).

The skill operates in five modes (summarized below; the deepest design rationale lives in `skills/safer-dependencies.md`

):

When Claude is about to write an `import`

, add a package to a manifest, or update a lock file, the skill runs inline in your session:

- Queries the package registry for stable versions
- Auto-selects the newest version published 7+ days ago (deterministic -- no LLM judgment)
- Checks for known vulnerabilities via ecosystem tools and the OSV API
- Verifies package signatures where available
- Emits warnings if issues are found, pins the exact version
- Logs the result to the audit trail

The version selection is handled by standalone Python scripts bundled with the skill, not by the LLM interpreting rules. The command outputs `SELECTED: <version>`

and Claude uses that version exactly.

Configure `.claude/settings.json`

with a `PostToolUse`

hook to enable automatic, transparent package verification:

- Claude writes a manifest file (e.g.
`package.json`

) with the originally-requested version — the file lands on disk - The
`PostToolUse`

hook fires immediately after the write completes and invokes`safer-dependencies-shim.sh`

- The shim reads the file, parses declared packages, and runs all security checks (typosquat, abandoned, CVE, staleness, hash-pin)
- If corrections are needed, the shim
**rewrites the manifest in place** with safe versions (or removes entries that have no safe version) - The shim emits signals (
`UPDATED:`

,`BLOCKED:`

,`WARNING:`

,`STALE:`

,`MAJOR-UPDATE-CONFIRM:`

,`REFACTOR-REQUIRED:`

,`REGRESSION:`

,`TYPOSQUAT-CONFIRM:`

,`VERIFY:`

,`CLEAN:`

) via`hookSpecificOutput.additionalContext`

on stdout.`REGRESSION:`

precedes a`MAJOR-UPDATE-CONFIRM:`

when the audit log shows the same (file, package) was previously corrected to the same safe target — that is, a subagent or stale plan has re-introduced a known-vulnerable version, and the orchestrator should restore the previously-approved version rather than re-deciding the major bump. - Claude receives those signals as a system-reminder and performs follow-up work (find affected imports, run tests, refactor for breaking changes)

**Design note — Shape C (post-write corrective):** the hook does NOT block writes. Each vulnerable version lands on disk first and is then auto-corrected within the same tool-use cycle. This is a deliberate choice over a `PreToolUse`

blocking design — see [FAQ.md](/robert-auger/safer-dependencies/blob/main/FAQ.md#why-posttooluse-post-write-corrective-instead-of-pretooluse-pre-write-blocking-for-the-manifest-path) for the tradeoffs.

**Example signal:**

```
UPDATED: aiohttp 3.8.5 → 3.9.0 (HIGH: 33 CVEs fixed)
```

The parent agent uses these signals to identify affected code and refactor as needed.

Configure `.claude/settings.json`

with a `PreToolUse:Bash`

hook to enable
pre-flight auditing of package-manager install commands. This complements
(does not replace) Intercept Mode — together they form a layered defense.

- Claude attempts a Bash tool call (e.g.
`npm install lodash@4.17.20`

) - The
`PreToolUse`

hook fires before the call runs and invokes`safer-dependencies-pretooluse-bash.sh`

- A pure-bash early filter short-circuits non-PM commands in ~115 ms
(no Python invocation), so
`git status`

/`ls`

/`npm test`

pay negligible cost on the hot path - For recognized package-manager installs (
`npm`

/`pnpm`

/`yarn`

`install`

/`i`

/`add`

), the helper tokenizes via`shlex`

, extracts each`pkg@version`

argument, and POSTs to OSV - Any vulnerable concrete pin → the hook returns
`permissionDecision: "deny"`

with a per-finding GHSA-id + CVSS + summary, plus a hint to invoke the safer-dependencies skill - The install never runs — no network fetch, no postinstall scripts

**Why this exists in addition to Intercept Mode:** the post-write shim
is blind to Bash. `npm install lodash@4.17.20`

runs to completion (and
postinstall scripts execute) before any audit fires; `npm install -g typosquat-pkg`

writes no project manifest at all. Pre-Install Mode
closes those gaps structurally.

Pre-Install Mode only sees what the user **typed** (`pkg@version`

args on
the command line). It can't see the transitive tree the resolver will
actually install. **Post-Install Mode** (below) audits the lockfile once
the install completes — the two modes are complementary, not redundant.

**Scope:** the package-manager CLIs covered here span five ecosystems
(npm/pnpm/yarn/bun/npx/deno, pip/pip3/pipx/pipenv/uv/uvx/poetry, gem/bundle,
go, cargo), plus Maven via Intercept Mode (Maven dependencies are typically
declared in `pom.xml`

/`build.gradle`

, not added via a CLI verb).

Known gap:the Maven CLI does support direct downloads via`mvn dependency:get -Dartifact=group:art:version`

and`mvn dependency:copy`

. This hook does not yet recognize those invocations. If you use them regularly, the existing post-write shim still catches whatever lands in your manifest, but the pre-fetch protection only applies to the ecosystems listed above. Tracked as a follow-up.

Per-ecosystem syntax recognized:

| PM | Verbs | Concrete-pin syntax |
|---|---|---|
`npm` , `pnpm` , `yarn` , `bun` |
`install` , `i` , `add` (plus `yarn` /`pnpm dlx` , `bun x` , `yarn create` ) |
`pkg@1.2.3` , `@scope/pkg@1.2.3` |
`npx` |
(verbless — package is first positional) | `pkg@1.2.3` |
`deno` |
`add` , `install` |
`npm:pkg@1.2.3` (npm-prefixed specs) |
`pip` , `pip3` , `pipx` , `pipenv` , `uv` , `uvx` , `poetry` |
`install` (pip/pip3/pipx/pipenv) / `add` (uv/poetry) / verbless (uvx) |
`pkg==1.2.3` (extras `pkg[extra]==X` also handled) |
`gem` , `bundle` |
`install` (gem) / `add` |
`-v 1.2.3` , `--version 1.2.3` , `--version=1.2.3` (separate flag) |
`go` |
`get` , `install` |
`pkg@v1.2.3` (must include `v` prefix per Go modules) |
`cargo` |
`add` , `install` |
`crate@1.2.3` |

Range pins (npm `^4.17`

, pip `>=`

, poetry `^`

/`~`

, Go `@latest`

) and
unspecified versions pass through to Intercept Mode after install — the
post-write shim audits whatever the resolver picks. Auto-rewrite to a
safe version is queued as a follow-up.

**Failure mode:** fail-open. Any error (Python missing, network blip,
malformed input) exits 0 with no output, allowing bash to proceed.
Intercept Mode still runs after install, so a failed pre-flight degrades
gracefully to existing protection.

**Example deny:**

```
safer-dependencies pre-flight audit blocked this install.
Vulnerable pinned version(s) detected:
  - lodash@4.17.20 → GHSA-35jh-r3h4-6jhm (CVSS:7.4): Command Injection in lodash
Re-run with a patched version, or invoke the safer-dependencies skill
for a recommended pin.
```

Configure `.claude/settings.json`

with a `PostToolUse:Bash`

hook to enable
post-flight auditing after Bash commands. It runs **three independent scans**
against the command's `cwd`

, each closing a gap the other hooks can't address:

**Scan A — lockfiles.** After a successful install verb (`npm install`

,`bundle install`

,`poetry install`

,`uv sync`

,`go mod tidy`

, etc.), audits freshly-modified lockfiles (`package-lock.json`

,`Gemfile.lock`

,`poetry.lock`

,`uv.lock`

,`go.sum`

,`yarn.lock`

,`pnpm-lock.yaml`

,`Pipfile.lock`

). This closes the**transitive-CVE gap** Pre-Install can't see: the user typed`pkg@version`

, but the resolver may have pulled in dozens of transitives no one named.**Scan B — manifests.** After any Bash command*not*on a read-only denylist (`ls`

,`cat`

,`git status`

, …), audits freshly-modified manifests. This is the**only** fallback for manifest edits made via`sed -i`

,`jq`

, or a script — those bypass the`Write`

/`Edit`

tool that Intercept Mode hooks on.**Scan C — resolved environment.** Plain`pip install`

/`pip install -r requirements.txt`

writes no lockfile, so Scan A never sees the resolved tree. After a pip-shaped install, Scan C re-invokes the same pip with a read-only`list --format=json`

and OSV-checks the full resolved environment (direct + transitive).

How a scan runs:

- Claude runs a Bash tool call
- The
`PostToolUse`

hook fires*after*the command completes and invokes`safer-dependencies-posttooluse-bash.sh`

- A pure-bash early filter short-circuits commands that match no scan gate in
~115 ms (same fast-path convention as Pre-Install), so
`ls`

/`git`

/`cat`

pay negligible cost - Each scan walks
`cwd`

with`find -maxdepth 5`

(covers monorepo layouts; excludes`node_modules`

,`.git`

,`.venv`

,`venv`

) for files modified within the last 60 s — override via`SAFE_DEP_POSTINSTALL_MTIME_WINDOW`

- For each freshly-modified file (Scan A/B), the hook forges a synthetic
`PostToolUse:Write`

payload and pipes it to the existing shim — the shim's lockfile and manifest auditors run unchanged, no duplicated logic - Per-file signals are concatenated and emitted as one
`hookSpecificOutput`

JSON to the parent agent

**What it catches that Pre-Install doesn't:** transitive vulnerabilities.
A clean-looking `bundle install`

can pull `rack@2.2.23`

(CVE-2025-27610)
as a transitive of `sinatra`

— the user never typed `rack`

, so
Pre-Install can't see it, but Post-Install reads the resolved
`Gemfile.lock`

and reports the CVE.

**Scope:** Scan A does not rewrite resolved versions — the auto-correct
contract only applies to manifests Claude wrote directly. For transitive CVEs,
the fix is typically "update the direct dep that owns the transitive," which
needs human judgment. Scan B *does* auto-correct, because it audits manifests
through the same shim path as Intercept Mode. Scan A skips when the
`transitive`

check tier is set to `off`

(`config set checks.transitive off`

).

**Failure mode:** fail-open, same as other hooks. Any error (missing
shim, malformed payload, Python unavailable) exits 0 silently.

**Example WARNING:**

```
WARNING: lodash@4.17.10 in lock file has GHSA-29mw-wpgm-hmr9, GHSA-35jh-r3h4-6jhm
```

The four modes above only fire for **root-session** tool calls. When the root
session dispatches a subagent (via the `Agent`

tool — many skills and slash
commands do this internally), the subagent's Write/Edit/Bash calls bypass all
of them. Post-Agent Mode is the reactive safety net for that gap.

- A
`PreToolUse:Agent`

hook (`safer-dependencies-pretooluse-agent.sh`

) runs immediately before each Agent dispatch and touches a sentinel file at`/tmp/.safer-deps-agent-<PPID>-<session_id>.sentinel`

(falling back to a PPID-only name when no session id is available) - The subagent runs and may write manifests or lockfiles
- A
`PostToolUse:Agent`

hook (`safer-dependencies-posttooluse-agent.sh`

) runs after the Agent call returns,`find`

s every manifest and lockfile newer than the sentinel, and audits each via the same shim path - Findings surface as
`additionalContext`

to the root session's next turn; the sentinel is removed

Nested subagents are covered automatically — the root's `PostToolUse:Agent`

fires only after all of the outer agent's work (including anything *it*
dispatched) is on disk. The one gap is a global install that writes no
manifest or lockfile (`npm install -g …`

): there is nothing to scan. Like the
other hooks, it fails open — any error (missing sentinel, missing shim,
unreadable payload) exits 0 silently. Full design rationale lives in
`skills/safer-dependencies.md`

.

The skill fires automatically when Claude:

**Manifest / install operations**

- Adds or updates a package in
`package.json`

,`requirements.txt`

,`Gemfile`

,`pom.xml`

,`build.gradle`

,`Cargo.toml`

,`go.mod`

, or any other supported manifest - Writes an
`import`

,`require`

, or`use`

for a package not already declared in the manifest - Generates or updates a lock file (checks only new/changed entries)
- Runs a package-manager install via Bash (
`npm install`

,`bundle install`

,`poetry install`

,`uv sync`

,`go mod tidy`

, etc.) — Pre-Install audits the command args, Post-Install audits the resulting lockfile - Writes a
`Dockerfile`

or CI workflow (`.github/workflows/*.yml`

, etc.) that embeds pinned package-manager install steps

**Selection & recommendation questions**

- Library/framework comparisons: "should I use axios or node-fetch?", "moment vs dayjs?", "which is better X or Y?"
- Recommendation requests: "what's a good HTTP client for Python?", "recommend a logging library for Go", "what package handles CSV in Node?"
- Version selection: "what version of Django should I use?", "latest stable Flask?"

**Intent-to-use expressions (pre-add)**

- "I want to use FastAPI for this", "I'm thinking of adding Celery", "we're looking at Prisma as the ORM", "let's use Tailwind"

**Package health and trust questions**

- "Is moment.js still maintained?", "is this gem still active?", "is X abandoned?", "is X EOL?", "can I trust this package?", "when was faker last updated?"

**Scaffolding commands**

`npx create-react-app`

,`npm create vite@latest`

,`django-admin startproject`

,`rails new`

,`cargo new`

+`cargo add`

, "bootstrap a new FastAPI project"

**Implicit package adds (feature requests that imply a new dependency)**

- "Add Redis caching to the app", "connect to Postgres", "add JWT auth", "write code to send emails" — fires when no package for that capability is already in the manifest

**Migration and porting**

- "Migrate from requests to httpx", "move from CRA to Vite", "port from moment to date-fns" — audits the incoming package

It does **not** fire for:

- Standard library imports (
`os`

,`fs`

,`java.util.*`

, etc.) - Already-declared dependencies that aren't being changed
- Academic discussion of how a package works internally ("explain React's reconciler", "how does webpack's module resolution work?") — comparison and selection questions do still fire
- Installing OS-level apps, runtimes, or IDE extensions (Python itself, Docker, Homebrew, VS Code extensions)

This is a **skill + hook bundle**, not a single skill file. A complete install deploys these pieces:

| File | Role |
|---|---|
`skills/safer-dependencies.md` |
The skill (`SKILL.md` once installed). Describes audit procedures and includes management mode for installation/stats. |
`skills/safer-dependencies-shim.sh` |
`PostToolUse:Write` /`Edit` hook — audits manifest + lockfile writes and auto-corrects vulnerable versions in place (Intercept Mode). |
`skills/safer-dependencies-pretooluse-bash.sh` |
`PreToolUse:Bash` hook — pre-flight OSV audit of package-manager install commands; denies vulnerable concrete pins before the install runs (Pre-Install Mode). |
`skills/safer-dependencies-posttooluse-bash.sh` |
`PostToolUse:Bash` hook — post-flight audit after Bash commands; catches transitive CVEs in freshly-written lockfiles, manifests edited via `sed` /`jq` /scripts, and the resolved environment of plain `pip install` (Post-Install Mode). |
`skills/safer-dependencies-pretooluse-agent.sh` + `skills/safer-dependencies-posttooluse-agent.sh` |
`PreToolUse:Agent` + `PostToolUse:Agent` hook pair — closes the subagent coverage gap. Modes 2–4 only fire for root-session tool calls, so any manifest a subagent writes bypasses them. Post-Agent audits whatever the subagent wrote after each Agent tool-call returns (Post-Agent Mode). |
`skills/scripts/` |
Shared Python library (`safedep/` ) and standalone resolver scripts used by all hooks. |
`skills/scripts/safer_dependencies_manager.py` |
Management module for interactive installation, usage stats, and setup validation. |

The skill file alone is not enough — without hooks, automatic invocation depends on Claude deciding to reach for the skill. Install all five pieces for full coverage; many skills and slash commands dispatch subagents internally, so the Post-Agent pair matters even if you never explicitly spawn one. (See [FAQ.md](/robert-auger/safer-dependencies/blob/main/FAQ.md#why-a-skill-alone-is-not-sufficient) for why a skill on its own can't guarantee coverage.)

| Ecosystem | Manifest | Lock file |
|---|---|---|
| npm | `package.json` |
`package-lock.json` , `yarn.lock` , `pnpm-lock.yaml` |
| PyPI | `requirements.txt` , `pyproject.toml` , `Pipfile` , `setup.py` , `setup.cfg` |
`Pipfile.lock` , `poetry.lock` , `uv.lock` |
| RubyGems | `Gemfile` , `*.gemspec` |
`Gemfile.lock` |
| Maven | `pom.xml` , `build.gradle` , `libs.versions.toml` |
-- |
| Go | `go.mod` |
`go.sum` |
| Rust | `Cargo.toml` |
`Cargo.lock` |
| PHP (Composer) | `composer.json` |
`composer.lock` |

New to the project? Start with ** GETTING-STARTED.md**. The short version:

```
git clone https://github.com/robert-auger/safer-dependencies /tmp/safer-dependencies
python3 /tmp/safer-dependencies/skills/scripts/safer_dependencies_manager.py interactive_install
```

The installer prompts for scope (global vs project) and which hooks to enable, then writes `settings.json`

for you — both the hook entries **and** the permissions allowlist that lets the skill's check commands run without an approval prompt on every audit.

Everything else install-related lives in ** INSTALLATION.md**, the single reference for install mechanics: manual file-by-file installs (global and project-level), Windows specifics, Post-Agent hooks, the

[permissions allowlist](/robert-auger/safer-dependencies/blob/main/INSTALLATION.md#permissions-allowlist), verifying the setup, updating, pinning to a release tag, and uninstalling.

After install, day-to-day management works via natural language to Claude — `install safer-dependencies`

(re-run / change hooks), `show safer-dependencies stats`

, `check safer-dependencies setup`

— or the `/safer-dependencies`

menu. Updating is in-session too: `/safer-dependencies update`

applies the latest release (`update --check`

for a dry-run, `update --rollback`

to undo); see [INSTALLATION.md](/robert-auger/safer-dependencies/blob/main/INSTALLATION.md#in-session-self-updater-safer-dependencies-update) for the trust model.

Platform note:macOS, Linux, and Windows are supported. Windows needs Git for Windows (provides bash) and Python 3 on`PATH`

— no WSL required. Hands-on testing to date has focused onmacOS and Windows; Linux support is exercised by the automated CI matrix.

Two things are configurable after install:

**Permissions allowlist**— pre-approves the skill's read-only check commands (the exact-form`npm audit`

/`bundle audit`

rules and the skill's own resolver scripts) so audits run without an approval prompt each time;`curl`

is never pre-approved, and`npm view`

/`pip-audit`

are opt-in via the Convenience profile. The interactive installer writes the core entries for you; manual installs add the full block by hand. Full block and rationale:[INSTALLATION.md → Permissions allowlist](/robert-auger/safer-dependencies/blob/main/INSTALLATION.md#permissions-allowlist).**Security policy**— the release-age cooldown window/mode and a per-check`off`

/`warn`

/`block`

tier for every check type, edited with`/safer-dependencies config`

and stored in`~/.config/safer-dependencies/config.toml`

. Schema and tier semantics:.`skills/references/configuration.md`

| Level | Meaning | Example |
|---|---|---|
| CRITICAL | Stop and ask user | Typosquat detected, tampered signature |
| HIGH | Warn and proceed | Known CVE, package < 30 days old |
| MEDIUM | Warn and proceed | Version < 7 days old, missing signature |
| LOW | Warn and proceed | Unsigned Ruby gem (expected) |

Every check is logged to `~/.claude/safer-dependencies-audit-YYYY-MM.log`

(one file per calendar month, where `YYYY-MM`

is the UTC year-month) as a single JSON line. Override the full path with the `SAFE_DEP_AUDIT_LOG`

environment variable (when set, the date suffix is not appended). Files are also size-rotated when they exceed `SAFE_DEP_LOG_MAX_BYTES`

(default 10 MiB; set to `0`

to disable). Set `SAFE_DEP_MODEL`

to override the model value written to `source.model`

in each entry — useful for A/B comparisons between model versions.

All five modes append to the same file. Each entry carries a ** source block** (schema 2.2) identifying which component wrote it:

`source.component` |
Written by | Trigger |
|---|---|---|
`shim.posttooluse` |
`shim.sh` |
Manifest or lockfile write (Intercept Mode, Post-Install dispatch) |
`shim.install_error` |
`shim.sh` |
Shim preflight install failure |
`bash.pretooluse` |
`pretooluse-bash.sh` |
Bash install command (Pre-Install Mode) |
`bash.posttooluse` |
`posttooluse-bash.sh` |
Post-Install Bash hook itself, when it fail-opens before reaching the shim |
`agent.pretooluse` |
`pretooluse-agent.sh` |
Reserved for Pre-Agent fail-open events (the hook itself is currently silent on success) |
`agent.posttooluse` |
`posttooluse-agent.sh` |
Post-Agent hook fail-open events (e.g. shim missing, python_missing) |
`manual.skill` |
Claude running Normal Mode | Manual audit invoked inline |

`source.model`

records the Claude Code model active in the session (e.g. `"claude-sonnet-4-6"`

). Present in schema 2.1+; entries written by older installs omit the field. The stats command degrades gracefully to `"unknown"`

when it is absent.

Filter by `source.component`

with `jq`

:

```
jq -r '.source.component' audit.log | sort | uniq -c | sort -rn
jq -c 'select(.source.component == "bash.pretooluse")' audit.log

# Surface every silent fail-open across all hooks:
jq -c 'select(.source.mode == "fail_open") | {component: .source.component, reason: .fail_open.reason, ts}' audit.log
```

For easier analysis, ask Claude for usage statistics instead of parsing logs manually:

```
"Show safer-dependencies stats for the last month"
```

This provides human-readable summaries of activity, security impact, and performance metrics extracted from these audit logs.

**Entry shapes (schema 2.2).** Three distinct shapes share the same `ts`

/ `schema`

/ `source`

header:

| Shape | When it's written | Distinguishing fields |
|---|---|---|
Audit entry |
Manifest / lockfile / bash-install audit | `file` , `ecosystem` , `checked` , `findings` , `abandoned` , `stale` , `typosquat` , `unknown` , `signatures` , `notes` , `clean` |
Install-error entry |
Shim preflight install-error (component `shim.install_error` ) |
`install_error` , `shim_dir` , `scripts_dir` |
Fail-open entry |
Any hook entry-point exits early because of `helper_missing` / `shim_missing` / `python_missing` . `source.mode` is `"fail_open"` |
`fail_open: { reason, detail? }` |

Audit entries: Intercept Mode runs the full pipeline (provenance, version age, OSV, abandoned/stale, typosquat, signatures), so all arrays can populate. Pre-Install Mode runs OSV only today, so `abandoned`

/ `stale`

/ `typosquat`

/ `signatures`

are always empty. Post-Install dispatch (lockfile audit) writes under `shim.posttooluse`

with `findings`

populated by `WARNING:`

strings from the lockfile auditors. The `notes`

array carries informational `NOTE:`

signals (e.g. manifest-skipped-because-unpinned).

Schema 2.2 added — additively — four fields to **lockfile** audit entries: `lockfile`

, `manifest_ref`

, `relation_summary`

(a direct/transitive/unknown classification of each flagged package against the sibling manifest), and a `policy`

block recording the `transitive`

tier in force. The bump is backward-compatible: readers of 2.1 entries tolerate the new fields, and the `source.model`

field remains present from 2.1 onward.

```
{
  "ts": "2026-04-19T12:34:56Z",
  "schema": "2.2",
  "source": {
    "component": "shim.posttooluse",
    "script": "shim.sh",
    "hook": "PostToolUse:Write",
    "tool": "Write",
    "mode": "intercept",
    "model": "claude-sonnet-4-6"
  },
  "file": "/path/to/project/package.json",
  "ecosystem": "npm",
  "checked": ["express@4.18.2", "lodash@4.17.21"],
  "findings": ["UPDATED: express 4.18.2 → 4.22.1 (HIGH: 1 CVE fixed)"],
  "abandoned": [],
  "stale": [],
  "typosquat": [],
  "unknown": [],
  "signatures": [],
  "notes": [],
  "clean": ["lodash@4.17.21"]
}
```

Pre-Install Mode example (Bash hook, vulnerable pin denied):

```
{
  "ts": "2026-04-23T06:56:21Z",
  "schema": "2.2",
  "source": {
    "component": "bash.pretooluse",
    "script": "pretooluse-bash.sh",
    "hook": "PreToolUse:Bash",
    "tool": "Bash",
    "mode": "intercept",
    "model": "claude-sonnet-4-6"
  },
  "file": "bash:npm install lodash@4.17.20 ms@2.1.3",
  "ecosystem": "npm",
  "checked": ["lodash@4.17.20", "ms@2.1.3"],
  "findings": [
    "BLOCKED: lodash@4.17.20 GHSA-35jh-r3h4-6jhm (CVSS:3.1/...): Command Injection in lodash"
  ],
  "abandoned": [],
  "stale": [],
  "typosquat": [],
  "unknown": [],
  "signatures": [],
  "notes": [],
  "clean": ["ms@2.1.3"]
}
```

Fail-open Mode example (Post-Install Bash hook called with no shim adjacent — broken install):

```
{
  "ts": "2026-05-03T07:14:11Z",
  "schema": "2.2",
  "source": {
    "component": "bash.posttooluse",
    "script": "safer-dependencies-posttooluse-bash.sh",
    "hook": "PostToolUse",
    "tool": "Bash",
    "mode": "fail_open",
    "model": "claude-sonnet-4-6"
  },
  "fail_open": {
    "reason": "shim_missing",
    "detail": "/home/alice/.claude/skills/safer-dependencies"
  }
}
```

A fail-open entry says: "this hook fired but exited early without auditing because something prerequisite was missing." Use the jq filter above (`select(.source.mode == "fail_open")`

) to surface every silent loss-of-protection event in your log.

When the shim runs in dry-run mode (`SAFE_DEP_DRY_RUN=1`

), entries also include `"mode": "dry_run"`

so post-hoc analysis can filter audit-only invocations.

- Python 3.9+ (the hooks probe for this and fail-open on older interpreters)
`curl`

(for registry API calls and OSV vulnerability checks)- Ecosystem tools (optional, skill falls back to OSV API if missing):
`npm`

for npm packages`pip-audit`

for Python packages`bundle`

for Ruby packages`dependency-check`

for Java packages

Design-decision rationale (why `PostToolUse`

instead of `PreToolUse`

, why signatures aren't verified, why scripts and shim are duplicated, skill-loading gotchas, etc.) is documented in [ FAQ.md](/robert-auger/safer-dependencies/blob/main/FAQ.md).
