@aldegad/safedeps 2.2.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ARCHITECTURE.md CHANGED
@@ -39,10 +39,12 @@ safedeps owns security gates at two distinct moments, under one skill. It absorb
39
39
  ```
40
40
 
41
41
  - **Install-time lane** (sections 2–13 below) — advisory check, fast command guard, and the npm effect gate + reorg. Per-package and proactive.
42
- - **Release-time lane** — the repo-tree checks from `security-release-gates` (secret scan, dependency audit, repo hook install/check, privacy profile), exposed under the `safedeps scan|audit|hooks|git` command namespace. Repo-specific policy (`.gitleaks.toml`, lockfiles) stays in the target repo; safedeps owns execution, install, and verification.
42
+ - **Release-time lane** — the repo-tree checks from `security-release-gates` (secret scan, dependency audit, repo hook install/check, privacy profile), exposed under the `safedeps scan|audit|hooks|doctor` command namespace. Repo-specific policy (`.gitleaks.toml`, lockfiles) stays in the target repo; safedeps owns execution, install, and verification.
43
43
 
44
44
  The two lanes differ in timing and scope (one package's effect before/after install vs. the whole repo before a release). They live under one umbrella but stay separated by command namespace.
45
45
 
46
+ **The secret-leak side of the release-time lane is per-repo and opt-in.** Its detection policy lives in the target repo, not in safedeps, so it does nothing until the repo provides a `.gitleaks` config and an active `.githooks/pre-commit`. `safedeps doctor` is the repo-entry diagnostic that closes that gap: it reports each piece of the secret-leak lane (`.gitleaks` policy, `pre-commit`, `core.hooksPath`, scanner availability) plus the global install-time gate, and exits non-zero when the per-repo lane has gaps. `safedeps doctor --fix` (= `safedeps hooks init` then `safedeps hooks install`) scaffolds a starter policy from `lib/gates/templates/` and activates the hooks. The scaffold is **non-destructive** — an existing repo-owned config is never overwritten — preserving the invariant that safedeps owns *execution*, not *policy*. The scaffolded `pre-commit` delegates to `safedeps scan secrets --staged` (a single canonical scanner path) and is fail-closed: an unresolvable `safedeps` or missing scanner blocks the commit rather than skipping silently.
47
+
46
48
  **The effect-primary model is npm-only.** `pip`, `cargo`, `go`, `gem`, `maven`, and `nuget` stay on the v2.1 command-gate + reorg model until their closure resolvers land; they are not described as having PostToolUse closure authority.
47
49
 
48
50
  ### Install-time flow
@@ -332,6 +334,8 @@ OSV normalizes ecosystem names, so one API path covers all of them at advisory-c
332
334
  | `lib/providers/` | OSV / KEV / GHSA (and optional NVD / deps.dev / Snyk) adapters behind one query interface. |
333
335
  | `lib/ledger/` | Approved-spec ledger I/O — atomic write, hashing, TTL checks. |
334
336
  | `lib/npm/closure.sh` | npm closure resolution from a lockfile. |
337
+ | `lib/gates/` | Release-time repo lane — `scan.sh` (gitleaks runner), `audit.sh` (npm lockfile audit), `hooks.sh` (`install`/`check`/`init`), `doctor.sh` (posture diagnose + `--fix`), `repo-profile.sh` (public/private resolution). Owns *execution*; the repo owns *policy*. |
338
+ | `lib/gates/templates/` | Starter `.gitleaks[.private].toml` + `.githooks/pre-commit`, scaffolded by `hooks init`. Seeds the repo owns and tunes — never overwritten on re-run. |
335
339
 
336
340
  ---
337
341
 
package/README.ko.md CHANGED
@@ -23,6 +23,8 @@
23
23
  - **install-time** (이 README 의 초점) — advisory check + approved-spec ledger + 빠른 PreToolUse guard + PostToolUse effect enforcement + post-install reorg. 패키지 단위, install 명령과 실제 lockfile effect 주변.
24
24
  - **release-time** — `safedeps gates run`, `safedeps scan secrets [--repo|--worktree|--staged]`, `safedeps audit npm`, `safedeps hooks install|check`. repo 트리 secret scan, 의존성 audit, repo-local git hook 설치/검사 (push/release 전). repo-specific policy(gitleaks config, privacy 경로)는 대상 repo 에 남고 safedeps 는 실행 owner. *(옛 `security-release-gates` 흡수.)*
25
25
 
26
+ release-time lane 의 secret 누출 쪽은 **repo 별이고 opt-in** 이다. `safedeps doctor` 가 그 repo-entry 점검이다 — repo 의 `.gitleaks` policy, `.githooks/pre-commit`, 활성 `core.hooksPath`, scanner 가용성을 진단하고(전역 install-time gate 상태도 같이 보고), `safedeps doctor --fix` 가 시작 policy 를 scaffold(`safedeps hooks init`)하고 활성화(`safedeps hooks install`)한다. scaffold 는 비파괴적이라 repo 가 소유한 기존 `.gitleaks.toml` 은 덮어쓰지 않으며, pre-commit hook 은 `safedeps scan secrets --staged` 에 위임하고 fail-closed 다. [secret 누출 lane (repo 별)](#secret-누출-lane-repo-별) 참고.
27
+
26
28
  ---
27
29
 
28
30
  ## 어떻게 동작
@@ -72,6 +74,46 @@
72
74
 
73
75
  ---
74
76
 
77
+ ## secret 누출 lane (repo 별)
78
+
79
+ install-time gate 는 전역이지만, secret 이나 진짜 `.env` 가 커밋되는 걸 막는 건 **repo 별이고 opt-in** 이다 — 탐지 policy 가 safedeps 가 아니라 각 repo 에 있기 때문이다. `safedeps doctor` 가 그 빈틈을 메우는 진입점이다.
80
+
81
+ ```bash
82
+ # 이 repo 의 자세 진단 (read-only). secret lane 에 gap 이 있으면 non-zero 로 끝난다.
83
+ $ safedeps doctor
84
+ safedeps doctor — repo security posture
85
+ repo: /path/to/repo
86
+ profile: public
87
+
88
+ Secret-leak lane (per-repo)
89
+ ✓ git worktree
90
+ ✗ gitleaks config (.gitleaks.toml) → safedeps hooks init --root "/path/to/repo"
91
+ ✗ .githooks/pre-commit (present) → safedeps hooks init --root "/path/to/repo"
92
+ ✗ git hooks active (core.hooksPath=<unset>) → safedeps hooks install --root "/path/to/repo"
93
+ ✓ secret scanner available (gitleaks)
94
+
95
+ Dependency-install gate (global, all repos)
96
+ ✓ dependency-install gate installed (~/.claude/skills/safedeps)
97
+
98
+ 3 gap(s) in the secret-leak lane.
99
+ Fix all at once: safedeps doctor --fix --root "/path/to/repo"
100
+
101
+ # 시작 policy scaffold + hook 활성화 (비파괴적).
102
+ $ safedeps doctor --fix
103
+ ```
104
+
105
+ lane 구성 요소:
106
+
107
+ - **`safedeps hooks init`** 가 시작용 `.gitleaks.toml`(private repo 면 `.gitleaks.private.toml`)과 `.githooks/pre-commit` 을 scaffold 한다. 기존 파일은 덮지 않고 유지 — policy 는 repo 가 소유한다.
108
+ - **`safedeps hooks install`** 이 repo-local hook 을 활성화한다(`core.hooksPath = .githooks`).
109
+ - **pre-commit hook 은 `safedeps scan secrets --staged` 에 위임**하고 **fail-closed** 다: scanner(로컬 `gitleaks` 또는 Docker)가 못 돌면 silent skip 이 아니라 커밋을 막는다. 의도된 우회는 사람이 소유하는 `git commit --no-verify` 뿐이다.
110
+
111
+ scaffold 된 `.gitleaks.toml` 은 **네가 손보는 시작점**이다: gitleaks 기본 ruleset 을 extend 하고, 값이 할당된 `.env` 커밋을 잡는 rule 을 더하며(`.env.example`/`.sample`/`.template` 변형은 allowlist), fixture 용 repo-owned `[allowlist]` 블록을 남겨둔다. safedeps 는 *실행* — `safedeps scan secrets` 로 gitleaks 구동 — 만 소유하고 policy 내용은 소유하지 않는다.
112
+
113
+ `safedeps doctor --json` 은 `{ command, repo, profile, gaps, ok, checks[] }` 를 돌려준다; `gaps`/`ok` 는 repo 별 secret 누출 lane 만 반영한다.
114
+
115
+ ---
116
+
75
117
  ## 설치
76
118
 
77
119
  ### 1) GitHub clone (skill 본체 설치, canonical)
package/README.md CHANGED
@@ -26,6 +26,8 @@ Use the GitHub release when you want the full skill/hook source tree as the cano
26
26
  - **Install-time** (the focus of this README) — advisory check + approved-spec ledger + fast PreToolUse guard + PostToolUse effect enforcement + post-install reorg. Per-package, around the install command and its actual lockfile effect.
27
27
  - **Release-time** — `safedeps gates run`, `safedeps scan secrets [--repo|--worktree|--staged]`, `safedeps audit npm`, `safedeps hooks install|check`. Repo-tree secret scan, dependency audit, and repo-local git hook install/check before push/release. Repo-specific policy (gitleaks config, privacy paths) stays in the target repo; safedeps owns execution. *(Absorbed the former `security-release-gates`.)*
28
28
 
29
+ The secret-leak side of the release-time lane is **per-repo and opt-in**. `safedeps doctor` is its repo-entry check: it diagnoses the repo's `.gitleaks` policy, `.githooks/pre-commit`, the active `core.hooksPath`, and scanner availability (and reports the global install-time gate too), then `safedeps doctor --fix` scaffolds a starter policy (`safedeps hooks init`) and activates it (`safedeps hooks install`). The scaffold is non-destructive — an existing repo-owned `.gitleaks.toml` is never overwritten — and the pre-commit hook delegates to `safedeps scan secrets --staged`, fail-closed. See [Secret-Leak Lane (per-repo)](#secret-leak-lane-per-repo).
30
+
29
31
  ## How It Works
30
32
 
31
33
  `safedeps` works in two moves around every install:
@@ -141,6 +143,44 @@ After the install command completes, the verify hook analyzes what changed. For
141
143
  | Dependency confusion | >50 new dependencies in a single install | PostToolUse effect verify | **Reorg** |
142
144
  | Native binaries | Compiled executables in `node_modules/.bin/` | PostToolUse effect verify | **Reorg** |
143
145
 
146
+ ## Secret-Leak Lane (per-repo)
147
+
148
+ The install-time gate is global, but stopping a secret or a real `.env` from being committed is **per-repo** and stays opt-in — its detection policy lives in each repo, not in safedeps. `safedeps doctor` is the entry point that closes that gap.
149
+
150
+ ```bash
151
+ # Diagnose this repo's posture (read-only). Exits non-zero if the secret lane has gaps.
152
+ $ safedeps doctor
153
+ safedeps doctor — repo security posture
154
+ repo: /path/to/repo
155
+ profile: public
156
+
157
+ Secret-leak lane (per-repo)
158
+ ✓ git worktree
159
+ ✗ gitleaks config (.gitleaks.toml) → safedeps hooks init --root "/path/to/repo"
160
+ ✗ .githooks/pre-commit (present) → safedeps hooks init --root "/path/to/repo"
161
+ ✗ git hooks active (core.hooksPath=<unset>) → safedeps hooks install --root "/path/to/repo"
162
+ ✓ secret scanner available (gitleaks)
163
+
164
+ Dependency-install gate (global, all repos)
165
+ ✓ dependency-install gate installed (~/.claude/skills/safedeps)
166
+
167
+ 3 gap(s) in the secret-leak lane.
168
+ Fix all at once: safedeps doctor --fix --root "/path/to/repo"
169
+
170
+ # Scaffold the starter policy + activate the hooks (non-destructive).
171
+ $ safedeps doctor --fix
172
+ ```
173
+
174
+ What the lane is made of:
175
+
176
+ - **`safedeps hooks init`** scaffolds a starter `.gitleaks.toml` (or `.gitleaks.private.toml` for a private repo) and a `.githooks/pre-commit`. Existing files are kept, never overwritten — the repo owns the policy.
177
+ - **`safedeps hooks install`** activates the repo-local hooks (`core.hooksPath = .githooks`).
178
+ - The **pre-commit hook delegates to `safedeps scan secrets --staged`** and is **fail-closed**: if the scanner (local `gitleaks` or Docker) cannot run, it blocks the commit instead of skipping silently. The only intentional bypass is `git commit --no-verify`, which the human owns.
179
+
180
+ The scaffolded `.gitleaks.toml` is a **starter you tune**: it extends gitleaks' default ruleset, adds a rule for a committed `.env` with an assigned secret (the `.env.example`/`.sample`/`.template` variants are allowlisted), and leaves a repo-owned `[allowlist]` block for your fixtures. safedeps owns *execution* — running gitleaks via `safedeps scan secrets` — not the policy content.
181
+
182
+ `safedeps doctor --json` returns `{ command, repo, profile, gaps, ok, checks[] }`; `gaps`/`ok` reflect the per-repo secret-leak lane only.
183
+
144
184
  ## Installation
145
185
 
146
186
  ### Prerequisites
@@ -307,6 +347,8 @@ safedeps/
307
347
  lib/
308
348
  providers/ # OSV / CISA KEV / GHSA adapters
309
349
  ledger/ # approved-spec ledger
350
+ npm/ # lockfile closure resolver
351
+ gates/ # repo-tree lane: scan / audit / hooks / doctor + templates/
310
352
  scripts/
311
353
  safedeps-pre-guard.sh # PreToolUse hook -- advisory ledger UX + snapshots
312
354
  safedeps-post-verify.sh # PostToolUse hook -- npm primary effect verification + reorg
package/ROADMAP.md CHANGED
@@ -56,7 +56,7 @@ The internal engine keeps the v1 `reorg-guard` assets.
56
56
 
57
57
  ### Release notes
58
58
 
59
- - The npm package version in `package.json` is the single source of truth. `bin/safedeps` `SAFEDEPS_VERSION` tracks it and the smoke test reads `package.json` to compare (current: v2.2.0).
59
+ - The npm package version in `package.json` is the single source of truth. `bin/safedeps` `SAFEDEPS_VERSION` tracks it and the smoke test reads `package.json` to compare (current: v2.4.1).
60
60
  - `npm test` runs the release smoke suite; the full fixture E2E lives under `v2.1-tests`.
61
61
  - The daily re-check uses no LLM tokens. It is opt-in: a macOS `launchd` user agent runs `safedeps re-check --json` daily, installed atomically by `install-safedeps-recheck-agent.mjs`. It writes `~/.safedeps/recheck.log` and `~/.safedeps/recheck-alerts.jsonl` and raises a macOS notification on a new CVE/KEV/revoke/provider-skip. Network is used only for OSV / CISA / GHSA queries.
62
62
 
@@ -87,7 +87,56 @@ This phase covers npm lockfile closure only. pip / cargo / go / gem / maven / nu
87
87
 
88
88
  ### Current focus
89
89
 
90
- 1. `v2.2.0-release`: merge `safedeps-security-hardening`, tag `v2.2.0`, GitHub release, `npm publish`.
90
+ 1. `v2.2.0-release`: merged `safedeps-security-hardening`, tagged `v2.2.0` (GitHub release + `npm publish`).
91
+
92
+ ---
93
+
94
+ ## v2.3 — secret-leak lane doctor + scaffold (shipped)
95
+
96
+ Status: shipped as v2.3.0.
97
+
98
+ ### What changed
99
+
100
+ - **`safedeps doctor`** — a repo-entry posture check. It diagnoses the per-repo secret-leak lane (`.gitleaks` policy, `.githooks/pre-commit`, active `core.hooksPath`, scanner availability) and reports the global install-time gate too. Read-only by default, `--json` for agents, exits non-zero when the secret-leak lane has gaps.
101
+ - **`safedeps doctor --fix` / `safedeps hooks init`** — scaffolds a starter `.gitleaks.toml` (or `.gitleaks.private.toml`) and `.githooks/pre-commit` from `lib/gates/templates/`, then activates the hooks. Non-destructive: an existing repo-owned policy is never overwritten.
102
+ - **Agent-as-security-role framing** — `SKILL.md` makes `safedeps doctor` a repo-entry step so the agent, not a later leak, closes the secret-lane gap. The installer prints a per-repo nudge (no auto-write into repos — the policy boundary stays with the repo).
103
+ - **Fail-closed delegation** — the scaffolded `pre-commit` delegates to `safedeps scan secrets --staged` (one canonical scanner path); an unresolvable `safedeps` or a missing scanner blocks the commit rather than skipping silently.
104
+
105
+ ### Design decisions
106
+
107
+ - `doctor` is holistic but **secret-lane-centric**: its exit code reflects the per-repo lane only; the global dependency gate is reported (`deps` check) but does not gate the repo result.
108
+ - safedeps owns **execution**, the repo owns **policy**. Templates are seeds the repo tunes, consistent with the existing Two Lanes invariant.
109
+
110
+ ### Verification
111
+
112
+ - `safedeps doctor` flags gaps on an unconfigured repo and reports clean after `--fix`
113
+ - `hooks init` is non-destructive across a re-run (repo edits survive)
114
+ - pre-commit gate denies a committed secret, passes clean and `.env.example` placeholder commits (bypass harness + regression)
115
+ - existing smoke + fixture E2E regression suite remains green
116
+
117
+ ---
118
+
119
+ ## v2.4 — fail-closed hooks + supply-chain hardening (shipped)
120
+
121
+ Status: shipped as v2.4.0.
122
+
123
+ ### What changed
124
+
125
+ - **Fail-closed gate** — the PreToolUse/PostToolUse hooks no longer `exit 0` (silent pass) when they cannot run. A lock-unavailable install now **denies** fail-closed; an unavoidable `jq`-missing case becomes an **explicit allow-with-warning**; every such outcome is recorded in `~/.safedeps/advisory.log` (observable, per the no-silent-fallback invariant). The PostToolUse path records an un-runnable gate as **UNVERIFIED** rather than a clean pass.
126
+ - **`SECURITY.md`** — vulnerability disclosure policy, supported versions, scope, and the by-design security properties (no SaaS, zero deps, no silent fallback).
127
+ - **CI hardening** — `actions/*` pinned to commit SHA; the gitleaks download is checksum-verified; a ShellCheck gate (error-clean); a macOS + Linux matrix (the v2.3 `stat` fix proved cross-OS coverage matters); and an `npm pack` step that keeps the zero-dependency property honest.
128
+
129
+ ### Verification
130
+
131
+ - lock-unavailable install denies fail-closed and logs to `advisory.log`
132
+ - jq-missing denies a likely install (best-effort fail-closed) and logs it; only non-install commands fall through
133
+ - a missing ledger library denies fail-closed instead of falling through to allow
134
+ - ShellCheck (`--severity=error`) is clean across all shell sources
135
+ - existing smoke + e2e regression suite remains green on both Linux and macOS
136
+
137
+ ### v2.4.1 — concurrent-install race fix (#5)
138
+
139
+ The pending state PreToolUse hands to PostToolUse was a single global `current_state` file, so two installs overlapping in one project could clobber each other and the effect gate could verify the wrong install (or skip one). Pending state is now keyed **per install** — `dir_hash` + a hash of the command with the inert-install rewrite normalized out — so PreToolUse and PostToolUse of the same install agree on a key while concurrent installs stay isolated. A concurrency harness (two installs → two pending files; a post consumes only its own) guards it.
91
140
 
92
141
  ---
93
142
 
package/SECURITY.md ADDED
@@ -0,0 +1,45 @@
1
+ # Security Policy
2
+
3
+ safedeps is a security tool, so it holds itself to the posture it enforces: local-only, no SaaS, **zero runtime npm dependencies**, fail-closed gates, and observable bypasses.
4
+
5
+ ## Supported versions
6
+
7
+ | Version | Supported |
8
+ |---|---|
9
+ | 2.x (latest minor) | ✅ |
10
+ | < 2.0 (`npm-reorg-guard`) | ❌ — migrate with `safedeps migrate` |
11
+
12
+ Fixes land on the latest minor. Pin to a released tag for reproducibility.
13
+
14
+ ## Reporting a vulnerability
15
+
16
+ **Do not open a public issue for a vulnerability.** Report it privately:
17
+
18
+ 1. **Preferred** — GitHub private advisory: the repo's **Security → Report a vulnerability** tab (<https://github.com/aldegad/safedeps/security/advisories/new>).
19
+ 2. **Email** — `aldegad@gmail.com` with `[safedeps security]` in the subject.
20
+
21
+ Please include: affected version (`safedeps --json version`), a minimal reproduction, the impact (e.g. a gate bypass, a fail-open path, a reorg that misses a threat), and any logs from `~/.safedeps/advisory.log` / `~/.safedeps/reorg.log`.
22
+
23
+ ### What counts
24
+
25
+ In scope — anything that defeats a gate or weakens its guarantees, for example:
26
+
27
+ - a dependency-install command that bypasses the PreToolUse guard or the PostToolUse effect gate;
28
+ - a malicious package whose closure or install scripts pass verification when they should reorg;
29
+ - a **fail-open** path (a gate that silently passes when it cannot run);
30
+ - a secret-leak lane bypass (a committed secret the scaffolded gate misses);
31
+ - ledger tampering that grants an unapproved spec (note: the ledger is a same-user convenience cache, **not** a boundary against a same-user attacker until signing lands — that limit is documented, not a vuln).
32
+
33
+ Out of scope — issues requiring an already-root/same-user attacker beyond the documented threat model, or third-party advisory-database accuracy (OSV/KEV/GHSA own their data).
34
+
35
+ ## Response
36
+
37
+ - Acknowledgement within ~72 hours.
38
+ - A fix or mitigation plan for confirmed reports, coordinated before public disclosure.
39
+ - Credit in the release notes if you'd like it.
40
+
41
+ ## Security properties (by design)
42
+
43
+ - **No SaaS** — local CLI + public databases (OSV / CISA KEV / GHSA) only.
44
+ - **Zero npm dependencies** — the tool ships no runtime deps; this is a deliberate security property, kept enforced by `npm pack` review in CI.
45
+ - **No silent fallback** — a provider/scanner miss is fail-closed (the install is denied). The one unavoidable exception is `jq` being absent — it is needed to parse the hook payload — and even then the guard reads the raw payload and denies anything that looks like a dependency install, falling back to an explicit allow-with-warning only for non-install commands. Every such outcome is recorded in `~/.safedeps/advisory.log`.
package/SKILL.md CHANGED
@@ -10,72 +10,97 @@ hooks:
10
10
 
11
11
  # Safedeps
12
12
 
13
- Safedeps protects development dependency installs with a three-phase flow:
13
+ Two gates, one skill. You (the agent) are the primary user — drive both:
14
14
 
15
- 1. **Phase 1Advisory gate (`safedeps check`)**: query OSV (canonical advisory truth), CISA KEV (overlay), and GitHub Advisory (enrichment) before install. For npm, resolve the full dependency closure with a script-free lockfile probe and OSV `/v1/querybatch`; write the approved direct spec plus `transitive_specs` to `~/.safedeps/approved-specs/<hash>.json` with a 30-day TTL.
16
- 2. **Phase 2Fast command guard (`scripts/safedeps-pre-guard.sh`)**: the PreToolUse hook does not call providers. It checks the approved-spec ledger for package/version tokens in the about-to-run install command and snapshots dependency truth. Miss or expired → block with a structured message that names the exact `safedeps check` command to run next. On Claude Code it also rewrites an npm install to add `--ignore-scripts` (hook `updatedInput`), so the install runs inert until verified; Codex CLI lacks `updatedInput`, so it falls back to detect-and-rollback.
17
- 3. **Phase 3 — Effect enforcement + reorg (`scripts/safedeps-post-verify.sh`)**: PostToolUse is the primary enforcement surface for npm. It compares the actual `package-lock.json` closure against direct ledger entries and their `transitive_specs`, re-checks the closure with OSV batch, and rolls back when any package is unapproved/vulnerable or install scripts look suspicious. After a verified inert install it runs `npm rebuild` so the now-trusted lifecycle scripts execute.
15
+ - **Install-time gate**clear every dependency install through an OSV-backed advisory check before it runs.
16
+ - **Secret-leak gate**stop a secret or a real `.env` from being committed (per-repo).
18
17
 
19
- > **Release-time lane**: the `security-release-gates` orchestrator is absorbed into `safedeps gates`. It runs the repo-tree gates (secret scan, dependency audit, repo git-hook/CI check, install-guard presence) from one entry point, detecting and orchestrating the repo's `security:*` npm scripts, `scripts/security/*`, and gitleaks/npm-audit fallback. Splitting the individual `scan`/`audit`/`hooks`/`git` commands out and fully migrating a target repo's `scripts/security/*` is follow-up work. Design SSoT: [`ARCHITECTURE.md`](./ARCHITECTURE.md) §1 (Two Lanes).
18
+ ---
20
19
 
21
- ## CLI Reference
20
+ ## Install-time gate
22
21
 
22
+ The hooks enforce this; you just run `check` first.
23
+
24
+ - **PreToolUse** blocks an install whose spec is not approved and quotes the exact `safedeps check` to run. On Claude Code it also rewrites an npm install with `--ignore-scripts`, so it runs **inert** until verified; Codex CLI uses detect-and-rollback.
25
+ - **PostToolUse** is the npm enforcement authority: it reads the real `package-lock.json` closure, checks every package against the ledger + an OSV batch, and **reorgs** (rolls back) anything unapproved, vulnerable, or with suspicious install scripts. Effect-primary is **npm-only**; pip/cargo/go/gem/maven/nuget use the command-gate + reorg model.
26
+
27
+ ### Before every install, run
28
+
29
+ ```bash
30
+ safedeps check <ecosystem> <pkg>@<range> --json
23
31
  ```
24
- safedeps check <ecosystem> <pkg>@<version|range> [--json]
25
- safedeps ledger [--json]
26
- safedeps revoke <hash> | <ecosystem> <pkg>@<version> | <pkg>@<version> [--reason <r>] [--json]
27
- safedeps re-check [--json]
28
- safedeps migrate [--keep-legacy]
29
- safedeps gates [run] [--root <repo>] [--strict] [--no-run]
30
- safedeps help [command]
31
- safedeps version
32
- ```
33
32
 
34
- **Ecosystems** (OSV-normalized): `npm`, `pypi`, `crates.io`, `go`, `rubygems`, `maven`, `nuget`.
33
+ - Ecosystems: `npm`, `pypi`, `crates.io`, `go`, `rubygems`, `maven`, `nuget`.
34
+ - Not on PATH? Use `~/.claude/skills/safedeps/bin/safedeps` (Claude) or `~/.codex/skills/safedeps/bin/safedeps` (Codex). Block messages already quote a runnable path.
35
+
36
+ ### Then act on `result`
35
37
 
36
- **Exit codes**:
38
+ | `result` | Action |
39
+ |---|---|
40
+ | `clean` / `already_approved` | Install. Use `install_hint` verbatim — it pins the approved version. |
41
+ | `patched_available` | Install `suggested_spec` instead (e.g. `^14.0.0` → `14.0.5`). |
42
+ | `cve_unpatched` | Do not install. Surface the CVEs, propose an alternative package. |
43
+ | `kev_hard_block` | Do not install. Recommend an alternative — actively exploited in the wild. |
44
+ | `provider_unavailable` | Do not install. OSV unreachable, no fresh cache. Retry later or tell the human. |
45
+ | `error` | Fix the spec (e.g. an unpublished version) and retry. |
37
46
 
38
- | Code | Meaning |
39
- |---:|---|
40
- | 0 | clean — spec approved, install is safe |
41
- | 2 | CVE found, no patched version available — install blocked, human decision required |
42
- | 3 | CISA KEV match — hard block, install must not proceed |
43
- | 4 | input error or provider unavailable (fail-closed) |
47
+ Install only after approval. If the approved version differs from your argument, the hook blocks again — re-narrow and retry.
44
48
 
45
- **Global flags**: `--json` (machine-readable, stable schema, no color, no spinner), `--no-color` (disable ANSI colors in human mode).
49
+ ### Rules
46
50
 
47
- ## For AI Agents
51
+ - Never bypass the gate. A provider miss is **fail-closed**, never fail-open.
52
+ - `kev_hard_block` is non-negotiable — recommend an alternative, never ask the human to override.
53
+ - Use `install_hint` / `suggested_spec` verbatim — a fresh range defeats the spec lock.
48
54
 
49
- You are the primary user of this skill when you propose `npm install`, `pip install`, `cargo add`, etc. Treat `safedeps check` as a mandatory pre-step. The PreToolUse hook will block the install if you skip it.
55
+ ### When the hook blocks you
50
56
 
51
- ### Workflow
57
+ The block message is `permissionDecisionReason`. The command quoted in backticks is runnable as-is — run it **verbatim**, parse the `--json` output, and retry the install with the approved version.
52
58
 
53
- 1. **Before issuing any install command**, call:
59
+ ---
54
60
 
55
- ```bash
56
- safedeps check <ecosystem> <pkg>@<range> --json
57
- ```
61
+ ## Secret-leak gate (per-repo)
58
62
 
59
- Use `--json` so the output is parseable. Read the `result` field.
63
+ The install-time gate is global; this one is per-repo and opt-in. Make it part of entering a repo, so the agent — not a later leak — is on guard.
60
64
 
61
- If `safedeps` is not on your PATH, invoke it at the skill path instead — `~/.claude/skills/safedeps/bin/safedeps` (Claude Code) or `~/.codex/skills/safedeps/bin/safedeps` (Codex CLI). The hook's block messages already name a runnable path, so when blocked you never have to resolve this yourself.
65
+ ```bash
66
+ safedeps doctor # diagnose; exits non-zero if the secret lane has gaps
67
+ safedeps doctor --fix # scaffold the policy + activate the hooks (non-destructive)
68
+ ```
69
+
70
+ 1. On repo entry, run `safedeps doctor` (`--json` for the `checks` array).
71
+ 2. Gaps? Run `safedeps doctor --fix`. It scaffolds `.gitleaks.toml` (or `.gitleaks.private.toml`) and `.githooks/pre-commit`, then activates them. Existing repo files are never overwritten.
72
+ 3. Tune the scaffolded `.gitleaks.toml` for the repo — allowlist fixtures, add rules. You own the policy; safedeps runs it (gitleaks via `safedeps scan secrets`).
73
+
74
+ The pre-commit hook delegates to `safedeps scan secrets --staged` and is **fail-closed**: no scanner → it blocks the commit. The only bypass is the human's `git commit --no-verify`.
75
+
76
+ ---
77
+
78
+ ## CLI reference
79
+
80
+ ```
81
+ safedeps check <ecosystem> <pkg>@<version|range> [--json]
82
+ safedeps ledger [--json]
83
+ safedeps revoke <hash> | <ecosystem> <pkg>@<version> | <pkg>@<version> [--reason <r>] [--json]
84
+ safedeps re-check [--json]
85
+ safedeps doctor [--root <repo>] [--fix] [--json]
86
+ safedeps hooks <install|check|init> [--root <repo>]
87
+ safedeps scan secrets [--repo|--worktree|--staged] [--root <repo>]
88
+ safedeps audit [npm] [--root <repo>]
89
+ safedeps gates [run] [--root <repo>] [--strict] [--no-run]
90
+ safedeps migrate [--keep-legacy]
91
+ safedeps help [command]
92
+ safedeps version
93
+ ```
62
94
 
63
- 2. **Decide from `result`**:
95
+ **Global flags**: `--json` (stable machine-readable schema), `--no-color`.
64
96
 
65
- | `result` | Action |
66
- |---|---|
67
- | `clean` / `already_approved` | Proceed with the install. Use `install_hint` verbatim if present (it pins to the exact approved version). |
68
- | `patched_available` | Approved spec narrowed to a safe version. Replace your install argument with `suggested_spec`. Example: `^14.0.0` → `14.0.5`. |
69
- | `cve_unpatched` | Do **not** install. Surface the CVE list to the human, propose an alternative package. |
70
- | `kev_hard_block` | Do **not** install. Recommend an alternative module — the package is actively exploited in the wild. |
71
- | `provider_unavailable` | OSV is unreachable and there is no fresh cache. Do not install. Retry later or tell the human. |
72
- | `error` | Argument parsing or version/closure resolution failed (e.g. an unpublished version). Fix the spec and retry. |
97
+ **Exit codes**: `0` clean/approved · `2` CVE, no patch (human decision) · `3` CISA KEV hard block · `4` input error or provider unavailable (fail-closed).
73
98
 
74
- 3. **Issue the install** only after the spec is approved. The hook re-checks the ledger; if the approved version differs from your install argument, the hook will block again — re-narrow and retry.
99
+ ---
75
100
 
76
- ### JSON schema (stable, agent-facing)
101
+ ## JSON schemas (agent-facing)
77
102
 
78
- `check` result envelope, common fields:
103
+ `check`:
79
104
 
80
105
  ```json
81
106
  {
@@ -92,115 +117,27 @@ You are the primary user of this skill when you propose `npm install`, `pip inst
92
117
  }
93
118
  ```
94
119
 
95
- `patched_available` adds `"suggested_spec": "1.2.5"`. `kev_hard_block` retains the full `vulnerabilities` and `kev.matches` arrays for evidence. `cve_unpatched` retains `vulnerabilities`.
96
-
97
- `ledger` returns `{ "command": "ledger", "count": N, "specs": [...] }` where each spec has `hash`, `ecosystem`, `package`, `version`, `version_range`, `approved_at`, `expires_at`, `approved_by`, `expired` (bool), `revoked` (bool).
98
-
99
- `revoke` returns `{ "command": "revoke", "revoked": true, "reason": "...", "spec": {...} }`.
100
-
101
- `re-check` returns `{ "command": "re-check", "checked": N, "still_clean": N, "newly_vulnerable": [...], "kev_hit": [...], "revoked": [...] }`.
102
-
103
- `migrate` returns `{ "migrated": bool, "legacyRoot": "...", "targetRoot": "...", "copied": N, "skipped": N, "archivedAs": "..." }`.
104
-
105
- ### When the hook blocks you
120
+ - `patched_available` adds `suggested_spec`. `kev_hard_block` keeps `vulnerabilities` + `kev.matches`. `cve_unpatched` keeps `vulnerabilities`.
106
121
 
107
- `scripts/safedeps-pre-guard.sh` emits a Claude Code / Codex CLI hook decision using the modern PreToolUse schema:
122
+ `doctor`:
108
123
 
109
124
  ```json
110
125
  {
111
- "hookSpecificOutput": {
112
- "hookEventName": "PreToolUse",
113
- "permissionDecision": "deny",
114
- "permissionDecisionReason": "safedeps: install not approved (ecosystem=npm) — run `safedeps check npm @scope/pkg@^1.2.0` first, then retry the install using the approved version (see install_hint in the check output)."
115
- }
126
+ "command": "doctor",
127
+ "repo": "/path/to/repo",
128
+ "profile": "public | private",
129
+ "gaps": 0,
130
+ "ok": true,
131
+ "checks": [
132
+ { "lane": "secret | deps", "status": "ok | gap | na", "label": "...", "remedy": "safedeps hooks init ..." }
133
+ ]
116
134
  }
117
135
  ```
118
136
 
119
- Both engines surface `permissionDecisionReason` back to you as the block message. The command quoted inside backticks is already runnable as-is — bare `safedeps` when the CLI is on PATH, otherwise an absolute path. Run it **verbatim** (do not rewrite a full path back down to a bare `safedeps`), parse the `--json` output, and retry the install with the approved version.
120
-
121
- ### Hard rules
122
-
123
- - Never bypass the advisory gate. There is no silent fallback. If the provider is unreachable, fail-closed is the correct outcome.
124
- - KEV (`result: "kev_hard_block"`) is non-negotiable. Recommend an alternative; do not ask the human to override.
125
- - Use `install_hint` or `suggested_spec` verbatim. Do not rewrite the version with a fresh range — that defeats the spec lock.
126
-
127
- ## For Humans
128
-
129
- Default output is Korean + ANSI color + braille spinner during long calls. Designed for terminal use, no flags needed.
130
-
131
- ```bash
132
- $ safedeps check npm "@scope/pkg@^14.0.0"
133
- · 버전 해석 중 (@scope/pkg@^14.0.0)
134
- · 취약점 조회 중 (OSV / KEV / GHSA)
135
- ⚠ @scope/pkg@14.0.3 에 2 개 CVE — 안전 버전 14.0.5 으로 좁혀 재조회합니다.
136
- · 14.0.5 재조회 중
137
- ✓ @scope/pkg@14.0.5 승인 (until 2026-06-17T...)
138
- · ledger: sha256:abc123...
139
-
140
- $ safedeps ledger
141
- STATE ECOSYSTEM PACKAGE VERSION APPROVED EXPIRES HASH
142
- ACTIVE npm @scope/pkg 14.0.5 2026-05-18T... 2026-06-17T... sha256:abc...
143
-
144
- $ safedeps revoke @scope/pkg@14.0.5 --reason "team policy change"
145
- ✓ 취소: npm @scope/pkg@14.0.5
146
-
147
- $ safedeps re-check
148
- · 재검증 npm left-pad@1.3.0
149
- · 검증 완료: 1 개 중 1 개 clean
150
- ```
151
-
152
- ### Color legend
137
+ - `gaps` / `ok` reflect the per-repo secret-leak lane only; a missing global gate is a `deps` check but does not change `ok`.
153
138
 
154
- - gray` info / progress
155
- - `✓ green` — safe, approved
156
- - `⚠ yellow` — warning, manual decision required
157
- - `✗ red` — hard error or KEV block
139
+ `ledger`, `revoke`, `re-check`, `migrate` each return a `{ "command": "...", ... }` envelope; run `safedeps help <command>` for the fields.
158
140
 
159
- Disable color with `--no-color` or `NO_COLOR=1`. Non-TTY pipes also strip color automatically.
160
-
161
- ## Current Components
162
-
163
- - `bin/safedeps` — CLI (this entry point). Bash. Source-loads providers + ledger libs.
164
- - `lib/providers/providers.sh` — OSV / CISA KEV / GitHub Advisory adapters with a single query interface and 24h local cache under `~/.safedeps/cache/`.
165
- - `lib/ledger/ledger.sh` — approved spec JSON ledger I/O under `~/.safedeps/approved-specs/`, deterministic spec hash, TTL checks, atomic writes.
166
- - `scripts/safedeps-pre-guard.sh` — PreToolUse hook. v1 command pattern guard + ledger lookup for install commands.
167
- - `scripts/safedeps-post-verify.sh` — PostToolUse hook. v1 rollback engine + npm effect gate (lockfile closure vs ledger + OSV batch).
168
- - `lib/npm/closure.sh` — npm lockfile closure extraction and script-free temp lockfile resolver.
169
- - `scripts/install/install-safedeps-hooks.mjs` — cross-engine installer. Symlinks `~/.claude/skills/safedeps` and `~/.codex/skills/safedeps`, idempotently patches `~/.claude/settings.json` and `~/.codex/hooks.json`. `--uninstall` removes both.
170
-
171
- ## Provider Failure Policy
172
-
173
- - OSV.dev is primary. Fresh cache may be used for 24h; cache miss or stale cache on OSV failure is fail-closed (`safedeps check` exits 4, hook blocks).
174
- - CISA KEV is an overlay. Stale or unavailable catalog is surfaced as a warning/status, not hidden.
175
- - GitHub Advisory is enrichment. Failure is fail-open only with an observable skipped status and advisory log entry.
176
-
177
- ## Installation
178
-
179
- The cross-engine installer registers the skill + hooks for Claude Code and Codex CLI in one shot. It is idempotent and backs up `settings.json` / `hooks.json` before writing.
180
-
181
- ```bash
182
- node scripts/install/install-safedeps-hooks.mjs # install
183
- node scripts/install/install-safedeps-hooks.mjs --uninstall # remove
184
- ```
185
-
186
- What it does:
187
-
188
- - Symlink the repo at `~/.claude/skills/safedeps` (when `~/.claude` exists) and `~/.codex/skills/safedeps` (when `~/.codex` exists).
189
- - Patch `~/.claude/settings.json` `hooks.PreToolUse[matcher=Bash]` and `hooks.PostToolUse[matcher=Bash]` with the canonical script paths.
190
- - Patch `~/.codex/hooks.json` with the same matcher and paths.
191
- - Optionally symlink `~/.local/bin/safedeps -> bin/safedeps` (via `--link-bin`) for a clean `safedeps` on PATH. **Not required** — the hook block messages and this skill fall back to the absolute skill-relative path, so the gate is fully self-contained without any PATH setup.
192
-
193
- Manual registration is also supported — see [Claude Code Hooks reference](https://code.claude.com/docs/en/hooks) and [Codex CLI Hooks](https://developers.openai.com/codex/hooks). The canonical script paths are:
194
-
195
- - PreToolUse: `<skills-root>/safedeps/scripts/safedeps-pre-guard.sh`
196
- - PostToolUse: `<skills-root>/safedeps/scripts/safedeps-post-verify.sh`
197
-
198
- ## State
199
-
200
- - Approved specs: `~/.safedeps/approved-specs/`
201
- - Provider cache: `~/.safedeps/cache/`
202
- - Reorg snapshots: `~/.safedeps/snapshots/`
203
- - Advisory log: `~/.safedeps/advisory.log`
204
- - Reorg log: `~/.safedeps/reorg.log`
141
+ ---
205
142
 
206
- `safedeps` is the canonical skill name, state namespace, and repository name.
143
+ Human terminal guide, install steps, and internal design: [`README.md`](./README.md), [`ARCHITECTURE.md`](./ARCHITECTURE.md). `safedeps` is the canonical skill name, state namespace, and repository name.
package/bin/safedeps CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  set -euo pipefail
9
9
 
10
- SAFEDEPS_VERSION="2.2.0"
10
+ SAFEDEPS_VERSION="2.4.1"
11
11
 
12
12
  # ---- repo / lib bootstrap ----------------------------------------------------
13
13
 
@@ -1027,6 +1027,11 @@ cmd_hooks() {
1027
1027
  exec bash "${SAFEDEPS_REPO_DIR}/lib/gates/hooks.sh" "$@"
1028
1028
  }
1029
1029
 
1030
+ cmd_doctor() {
1031
+ exec env SAFEDEPS_JSON_MODE="${SAFEDEPS_JSON_MODE}" \
1032
+ bash "${SAFEDEPS_REPO_DIR}/lib/gates/doctor.sh" "$@"
1033
+ }
1034
+
1030
1035
  # ---- help / version ----------------------------------------------------------
1031
1036
 
1032
1037
  cmd_version() {
@@ -1081,6 +1086,33 @@ safedeps migrate [--keep-legacy]
1081
1086
 
1082
1087
  Migrate legacy ~/.npm-reorg-guard state into ~/.safedeps.
1083
1088
  By default the legacy directory is archived to remove the old active truth path.
1089
+ EOF
1090
+ ;;
1091
+ doctor)
1092
+ cat <<'EOF'
1093
+ safedeps doctor [--root <repo>] [--fix] [--json]
1094
+
1095
+ Diagnose this repo's security posture: the per-repo secret-leak lane
1096
+ (.gitleaks policy + .githooks/pre-commit + active core.hooksPath + an
1097
+ available scanner) plus the global dependency-install gate. Read-only by
1098
+ default; exits 1 when the secret-leak lane has gaps.
1099
+
1100
+ --fix Scaffold the missing policy (hooks init) and activate it (hooks
1101
+ install). Non-destructive: existing repo-owned files are kept.
1102
+
1103
+ The scaffolded .gitleaks.toml is a STARTER you own and tune; safedeps owns
1104
+ execution (running gitleaks via `safedeps scan secrets`), not the policy.
1105
+ EOF
1106
+ ;;
1107
+ hooks)
1108
+ cat <<'EOF'
1109
+ safedeps hooks <install|check|init> [--root <repo>] [--hooks-path <dir>] [--auto]
1110
+
1111
+ init Scaffold the repo's secret-leak policy from starter templates
1112
+ (.gitleaks[.private].toml + .githooks/pre-commit). Non-destructive.
1113
+ install Activate repo-local git hooks (sets core.hooksPath). Requires the
1114
+ hook file to exist — run `init` first if the repo has none.
1115
+ check Verify the hooks are active, executable, and a scanner is available.
1084
1116
  EOF
1085
1117
  ;;
1086
1118
  *)
@@ -1099,7 +1131,8 @@ COMMANDS
1099
1131
  gates [run] Release-time repo gates: secret scan, dep audit, hook/CI check.
1100
1132
  scan secrets [--repo|--worktree|--staged] Run gitleaks secret scan (repo profile aware).
1101
1133
  audit [npm] Run npm lockfile audit.
1102
- hooks <install|check> Install/verify repo-local git hooks.
1134
+ hooks <install|check|init> Install/verify/scaffold repo-local git hooks.
1135
+ doctor [--fix] Diagnose this repo's secret-leak lane (--fix scaffolds + activates).
1103
1136
  help [command] Show help.
1104
1137
  version Print version.
1105
1138
 
@@ -1163,6 +1196,7 @@ main() {
1163
1196
  scan) cmd_scan "$@" ;;
1164
1197
  audit) cmd_audit "$@" ;;
1165
1198
  hooks) cmd_hooks "$@" ;;
1199
+ doctor) cmd_doctor "$@" ;;
1166
1200
  help) cmd_help "$@" ;;
1167
1201
  version) cmd_version ;;
1168
1202
  *)