@aldegad/safedeps 2.4.1 → 2.5.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
@@ -43,7 +43,7 @@ safedeps owns security gates at two distinct moments, under one skill. It absorb
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.
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` runs two checks. The secret scan (`safedeps scan secrets --staged`) runs on every commit and is fail-closed: an unresolvable `safedeps` or a missing scanner blocks rather than skipping silently. The npm dependency audit (`safedeps audit npm`) also runs on every commit in a repo with an npm lockfile — not only when the lockfile changes — so a CVE disclosed *after* a package was installed is caught at the next commit by re-querying the advisory DB. The audit separates the security verdict from an availability failure via meaningful exit codes (0 clean / 1 vulnerable / 2 could-not-run): a real finding **blocks** (fail-closed), while an unreachable advisory DB makes the hook **warn and allow the commit** — an explicit, observable availability failover (per the no-silent-fallback invariant: it is logged to the commit output and does not change canonical truth), with CI and the daily re-check re-covering what the offline commit could not verify.
47
47
 
48
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.
49
49
 
package/README.ko.md CHANGED
@@ -1,18 +1,41 @@
1
- # Safedeps
1
+ # safedeps
2
2
 
3
- > **모든 install 미확정 블록으로 본다 `safedeps` 안전한 것만 승인하고, 그렇지 않으면 reorg 한다.**
3
+ > **AI 코딩 에이전트가 취약하거나 미승인된 의존성을 설치하지 못하게 막고, 빠져나간 롤백한다.**
4
4
  >
5
- > OSV / CISA KEV / GitHub Advisory 의존성 spec사전 승인하고, Claude Code Codex CLI hook 에서 실제 설치 closure 를 강제하며, 승인과 어긋난 install 자동으로 마지막 안전 snapshot 으로 롤백한다.
5
+ > `safedeps` Claude Code·Codex CLI 에이전트가 실행하는 모든 의존성 install게이트한다. 패키지를 OSV / CISA KEV / GitHub Advisory 사전 승인하고, 실제로 lockfile 에 깔린 closure 를 다시 검증하며, 어긋난 자동으로 롤백한다. 전부 로컬에서 돌고 런타임 의존성은 0.
6
+
7
+ - **사전 승인** — 모든 `pkg@version` 과 (npm 은) 그 전체 transitive closure 를 설치 *전에* OSV(정본)·CISA KEV·GitHub Advisory 로 검사한다.
8
+ - **실제 effect 강제** — 설치 후 실제 `package-lock.json` closure 를 다시 확인하므로, 래핑·난독화된 명령도 게이트를 못 빠져나간다.
9
+ - **롤백** — 미승인·신규 취약 패키지는 마지막 확정 안전 snapshot 으로 되돌린다. Claude Code 에서는 install 이 inert(`--ignore-scripts`)로 돌아 거부된 패키지의 lifecycle script 가 아예 실행되지 않는다.
10
+
11
+ ## Quickstart
12
+
13
+ ```bash
14
+ # 1. CLI 설치 — npm 패키지는 scoped, @aldegad/ 접두사 주의
15
+ npm install -g @aldegad/safedeps
16
+
17
+ # 2. Claude Code / Codex 에 hook 연결 (idempotent)
18
+ cd "$(npm root -g)/@aldegad/safedeps" && node scripts/install/install-safedeps-hooks.mjs
19
+
20
+ # 3. 끝 — 이제 에이전트가 실행하는 모든 의존성 install 이 자동으로 게이트된다.
21
+ ```
22
+
23
+ > `safedeps` 는 CLI 명령어이고, npm 패키지는 **`@aldegad/safedeps`** 다 — npm 의 unscoped `safedeps` 는 무관한 남의 패키지. 전체 skill 소스 트리를 원하면 [설치](#설치) 참고.
6
24
 
7
25
  *Detailed reference → [README.md](./README.md) (영문, SSoT)*
8
26
 
9
27
  ---
10
28
 
11
- ## "reorg" 는 뭐고 왜 그 비유인가
29
+ ## 배포 모델
12
30
 
13
- 블록체인에서 **reorg (재편성)** 은 미확정 블록 시퀀스를 무효화하고 마지막 확정된 안전 상태로 체인을 되돌린다. `safedeps` 같은 원리를 `node_modules` 적용한다 — 모든 install 은 일련의 공급망 보안 검사를 통과하기 전까지는 **미확정 블록 후보** 로 취급된다. 의심스러우면 도구가 **reorg** 를 수행한다 — lock 파일, `package.json`, `node_modules` 를 마지막 확정된 안전 snapshot 으로 되돌린다.
31
+ safedeps 에는 배포 surface 있다:
14
32
 
15
- 빠른 advisory 피드백, 관측 가능한 rollback, silent fallback 없음. command guard best-effort UX 이고, 설치 결과 effectbackstop 이다.
33
+ 1. **Agent skill + hooks (canonical)** -- repo 자체가 skill folder 다. `SKILL.md`, hook script, provider/ledger library, install helper 디렉터리에 함께 있다.
34
+ 2. **npm package (CLI convenience)** -- `@aldegad/safedeps` 는 `safedeps` command 를 설치한다. npm 설치만으로 Claude Code 나 Codex 가 skill 을 자동 discover 하지는 않는다. npm 설치 후에도 hook/skill installer 를 실행하거나 skill folder 를 수동 등록해야 한다.
35
+
36
+ 전체 skill/hook source tree 를 canonical artifact 로 원하면 GitHub release 를 쓴다. versioned global CLI 가 주목적이면 npm 을 쓴다.
37
+
38
+ 용어: safedeps 는 Claude/Codex hook 과 local CLI 로 동작하는 agent security skill 이다. plugin manifest 로 감싸기 전까지는 Codex plugin bundle 이 아니다.
16
39
 
17
40
  ---
18
41
 
@@ -23,7 +46,7 @@
23
46
  - **install-time** (이 README 의 초점) — advisory check + approved-spec ledger + 빠른 PreToolUse guard + PostToolUse effect enforcement + post-install reorg. 패키지 단위, install 명령과 실제 lockfile effect 주변.
24
47
  - **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
48
 
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-별) 참고.
49
+ 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`)을 돌리고 npm repo 면 매 커밋 의존성 audit(`safedeps audit npm`)도 돌린다 — 실제 취약점은 차단(fail-closed)하고, 어드바이저리 DB 도달 불가 시에는 경고만 하고 커밋을 통과시킨다(관측 가능한 오프라인 failover). [secret 누출 lane (repo 별)](#secret-누출-lane-repo-별) 참고.
27
50
 
28
51
  ---
29
52
 
@@ -106,7 +129,11 @@ lane 구성 요소:
106
129
 
107
130
  - **`safedeps hooks init`** 가 시작용 `.gitleaks.toml`(private repo 면 `.gitleaks.private.toml`)과 `.githooks/pre-commit` 을 scaffold 한다. 기존 파일은 덮지 않고 유지 — policy 는 repo 가 소유한다.
108
131
  - **`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` 뿐이다.
132
+ - **pre-commit hook 은 검사를 돌린다**:
133
+ - **비밀키 스캔**(`safedeps scan secrets --staged`)을 매 커밋, **fail-closed**. scanner(로컬 `gitleaks` 또는 Docker)가 못 돌면 silent skip 이 아니라 커밋을 막는다.
134
+ - **npm 의존성 audit**(`safedeps audit npm`)을 npm lockfile 이 있는 repo 면 **매 커밋**. 취약한 직접·*transitive* 의존성을 잡는다 — 패키지를 깐 *뒤에* 공개된 CVE("그땐 안전해 보였는데 지금 발견됨")까지 포함해서, 사람이 손으로 절대 못 보는 그것. lockfile 이 바뀔 때만이 아니라 매 커밋 돌리는 게 핵심이다: 어드바이저리 DB 를 다시 조회하니까 이미 깔린 의존성에 *새로* 뜬 CVE 가 바로 다음 커밋에 드러난다. 보안 판정과 가용성 실패는 구분된다 — 실제 취약점은 **차단**(fail-closed)하지만, 어드바이저리 DB 가 **도달 불가**(오프라인/레지스트리 오류)면 **경고만 하고 커밋을 통과**시킨다(관측 가능한 가용성 failover, silent skip 아님). 오프라인 커밋이 못 본 건 CI 와 데일리 re-check 가 다시 메운다.
135
+
136
+ 의도된 우회는 사람이 소유하는 `git commit --no-verify` 뿐이다.
110
137
 
111
138
  scaffold 된 `.gitleaks.toml` 은 **네가 손보는 시작점**이다: gitleaks 기본 ruleset 을 extend 하고, 값이 할당된 `.env` 커밋을 잡는 rule 을 더하며(`.env.example`/`.sample`/`.template` 변형은 allowlist), fixture 용 repo-owned `[allowlist]` 블록을 남겨둔다. safedeps 는 *실행* — `safedeps scan secrets` 로 gitleaks 구동 — 만 소유하고 policy 내용은 소유하지 않는다.
112
139
 
@@ -176,6 +203,14 @@ node scripts/install/install-safedeps-recheck-agent.mjs install --hour 9 --minut
176
203
 
177
204
  ---
178
205
 
206
+ ## "reorg" 는 뭐고 왜 그 비유인가
207
+
208
+ 블록체인에서 **reorg (재편성)** 은 미확정 블록 시퀀스를 무효화하고 마지막 확정된 안전 상태로 체인을 되돌린다. `safedeps` 는 모든 install 을 똑같이 본다 — 일련의 공급망 보안 검사를 통과하기 전까지는 미확정 블록 후보다. 설치된 effect 가 어긋나면 도구가 **reorg** 를 수행한다 — lock 파일, `package.json`, `node_modules` 를 마지막 확정된 안전 snapshot 으로 되돌린다.
209
+
210
+ 하지만 reorg 는 **최전선이 아니라 backstop 이다.** 나쁜 install 의 대부분은 여기까지 오지도 않는다 — 사전 승인 게이트가 미승인·플래그된 패키지를 실행 전에 *거부* 하고, Claude Code 에서는 install 이 **inert (`--ignore-scripts`)** 로 돌아 closure 가 깨끗하다고 검증되기 전까지 lifecycle script 가 실행되지 않는다. reorg 가 발동하는 건 잔여 케이스뿐이다 — 승인된 직접 패키지가 미승인·취약 transitive 를 끌어오거나, 래핑된 명령이 advisory 계층을 빠져나간 경우 — 그리고 그때조차 실행되지도 못한 파일을 되돌린다.
211
+
212
+ 빠른 advisory 피드백, 관측 가능한 rollback, silent fallback 없음. command guard 는 best-effort UX 이고, 설치 결과 effect 가 backstop 이다.
213
+
179
214
  ## 다른 도구와 뭐가 다른가
180
215
 
181
216
  `safedeps` 는 **AI 에이전트가 코딩 중 install 명령을 작성하는 순간**에 끼어드는 도구다. CI 스캔, PR 권장, runtime sandbox 처럼 다른 시점에서 동작하는 도구들과 핵심 차별점이 여기 있다.
package/README.md CHANGED
@@ -1,14 +1,28 @@
1
- # Safedeps
1
+ # safedeps
2
2
 
3
- > **Treat every install as an unconfirmed block`safedeps` approves the safe ones, reorgs the rest.**
3
+ > **Stop your AI coding agent from installing vulnerable or unapproved dependencies and roll back the ones that slip through.**
4
4
  >
5
- > Pre-approve dependency installs against OSV / CISA KEV / GitHub Advisory, enforce the installed closure from Claude Code and Codex CLI hooks, and auto-rollback any install that diverges from the approved closure. *(한국어 README → [README.ko.md](./README.ko.md))*
5
+ > `safedeps` gates every dependency install your Claude Code or Codex CLI agent runs. It pre-approves packages against OSV / CISA KEV / GitHub Advisory, re-verifies the closure that actually lands in your lockfile, and auto-rolls-back anything that diverges. Local-only, with zero runtime dependencies. *(한국어 README → [README.ko.md](./README.ko.md))*
6
6
 
7
- ## Why "reorg"?
7
+ - **Pre-approve** — every `pkg@version`, plus its full transitive closure for npm, is cleared against OSV (canonical), CISA KEV, and GitHub Advisory *before* it installs.
8
+ - **Enforce the real effect** — after the install, the actual `package-lock.json` closure is re-checked, so a wrapped or obfuscated command can't sneak a package past the gate.
9
+ - **Roll back** — anything unapproved or newly-vulnerable is reverted to the last confirmed safe snapshot. On Claude Code the install runs inert (`--ignore-scripts`), so a rejected package's lifecycle scripts never run.
8
10
 
9
- In blockchain networks, a **reorganization (reorg)** invalidates a sequence of blocks and reverts the chain to a previously confirmed safe state. `safedeps` applies the same principle to your `node_modules`: every install is treated as an unconfirmed block candidate until it passes a battery of supply-chain security checks. If anything looks wrong, the tool performs a **reorg** -- rolling back lock files, `package.json`, and `node_modules` to the last confirmed safe snapshot.
11
+ ## Quickstart
10
12
 
11
- Fast advisory feedback, observable rollback, and no hidden fallback. The command guard is best-effort UX; the installed effect is the backstop.
13
+ ```bash
14
+ # 1. Install the CLI — the npm package is scoped, note the @aldegad/ prefix
15
+ npm install -g @aldegad/safedeps
16
+
17
+ # 2. Wire the hooks into Claude Code / Codex (idempotent)
18
+ cd "$(npm root -g)/@aldegad/safedeps" && node scripts/install/install-safedeps-hooks.mjs
19
+
20
+ # 3. Done — every dependency install your agent runs is now gated.
21
+ ```
22
+
23
+ > `safedeps` is the CLI command; the npm package is **`@aldegad/safedeps`** — the unscoped `safedeps` on npm is an unrelated package. Prefer the full skill source tree? See [Installation](#installation).
24
+
25
+ <!-- TODO(demo): add a 15-20s asciinema/VHS recording of safedeps catching a malicious install live (inert -> reorg). Highest-leverage conversion asset per launch review. -->
12
26
 
13
27
  ## Distribution Model
14
28
 
@@ -19,6 +33,8 @@ Safedeps has two distribution surfaces:
19
33
 
20
34
  Use the GitHub release when you want the full skill/hook source tree as the canonical artifact. Use npm when you mainly want a versioned global CLI.
21
35
 
36
+ Terminology: safedeps is an agent security skill backed by Claude/Codex hooks and a local CLI. It is not a Codex plugin bundle unless it is later wrapped with a plugin manifest.
37
+
22
38
  ## Two Lanes
23
39
 
24
40
  `safedeps` owns two security lanes (full design in [`ARCHITECTURE.md`](./ARCHITECTURE.md) §1):
@@ -26,7 +42,7 @@ Use the GitHub release when you want the full skill/hook source tree as the cano
26
42
  - **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
43
  - **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
44
 
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).
45
+ 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 runs a secret scan (`safedeps scan secrets --staged`) plus, on every commit in an npm repo, a dependency audit (`safedeps audit npm`): a real finding blocks (fail-closed), while an unreachable advisory DB only warns and lets the commit through (observable offline failover). See [Secret-Leak Lane (per-repo)](#secret-leak-lane-per-repo).
30
46
 
31
47
  ## How It Works
32
48
 
@@ -113,6 +129,14 @@ After the install command completes, the verify hook analyzes what changed. For
113
129
  4. The event is logged to `~/.safedeps/reorg.log`.
114
130
  5. Claude Code receives a system message detailing the detected threats and rollback actions.
115
131
 
132
+ ## Why "reorg"?
133
+
134
+ The name borrows from blockchain, where a **reorganization (reorg)** invalidates a sequence of unconfirmed blocks and reverts the chain to its last confirmed safe state. `safedeps` treats every install the same way: an unconfirmed block candidate until it passes a battery of supply-chain checks. If the installed effect diverges, the tool performs a **reorg** -- rolling the lock file, `package.json`, and `node_modules` back to the last confirmed safe snapshot.
135
+
136
+ But the reorg is the **backstop, not the front line.** Most bad installs never reach it: the pre-approval gate *denies* an unapproved or flagged package before it runs, and on Claude Code the install runs **inert** (`--ignore-scripts`) so lifecycle scripts do not execute until the closure verifies clean. The reorg fires for the residual case -- an approved direct package that pulls in an unapproved or vulnerable transitive, or a wrapped command that slips past the advisory layer -- and even then it rolls back files that never got to run.
137
+
138
+ Fast advisory feedback, observable rollback, and no hidden fallback. The command guard is best-effort UX; the installed effect is the backstop.
139
+
116
140
  ## The Blockchain Analogy
117
141
 
118
142
  | Blockchain Concept | Safedeps Equivalent |
@@ -175,7 +199,11 @@ What the lane is made of:
175
199
 
176
200
  - **`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
201
  - **`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.
202
+ - The **pre-commit hook runs two checks**:
203
+ - **Secret scan** (`safedeps scan secrets --staged`) on every commit, **fail-closed**. If the scanner (local `gitleaks` or Docker) cannot run, it blocks the commit instead of skipping silently.
204
+ - **npm dependency audit** (`safedeps audit npm`) on **every commit** in a repo that has an npm lockfile. This catches a vulnerable direct *or transitive* dependency — including a CVE that was published *after* you installed the package ("looked safe then, flagged now"), the kind of thing a human never reviews by hand. Running it every commit (not only when the lockfile changes) is the point: it re-queries the advisory DB so a newly-disclosed CVE on an already-installed dependency surfaces at the very next commit. The verdict and an availability failure are kept apart: a real finding **blocks** (fail-closed), but if the advisory DB is **unreachable** (offline / registry error) the hook **warns and lets the commit through** — an observable availability failover, never a silent skip. (CI and the daily re-check then re-cover what the offline commit could not verify.)
205
+
206
+ The only intentional bypass is `git commit --no-verify`, which the human owns.
179
207
 
180
208
  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
209
 
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.4.1).
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.5.0).
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
 
@@ -140,6 +140,24 @@ The pending state PreToolUse hands to PostToolUse was a single global `current_s
140
140
 
141
141
  ---
142
142
 
143
+ ## v2.5 — pre-commit dependency audit (shipped)
144
+
145
+ Status: shipped as v2.5.0.
146
+
147
+ ### What changed
148
+
149
+ - **Pre-commit dependency audit** — the scaffolded `.githooks/pre-commit` now runs `safedeps audit npm` on **every commit** in a repo with an npm lockfile, alongside the secret scan. It catches a vulnerable direct or *transitive* dependency — including a CVE disclosed *after* the package was installed ("looked safe then, flagged now") — at the next commit, by re-querying the advisory DB instead of waiting for the daily re-check. Real usage drove it: a transitive `hono` advisory that Dependabot missed was caught exactly this way.
150
+ - **Meaningful `audit npm` exit codes** — `0` clean / `1` vulnerable / `2` could-not-run (no lockfile, npm/jq missing, advisory DB unreachable). This separates the **security verdict** from an **availability failure**; npm audit collapses both into exit 1 on its own.
151
+ - **Observable offline failover** — when the advisory DB is unreachable the hook **warns and allows** the commit (exit 2) rather than fail-closing, so a network outage never blocks an offline commit; a real finding (exit 1) still **blocks**. Per the no-silent-fallback invariant the failover is loud (printed to the commit output), and CI / the daily re-check re-cover what the offline commit could not verify.
152
+
153
+ ### Verification
154
+
155
+ - `audit npm` exit-code contract (clean=0 / vulnerable=1 / unreachable=2), deterministic via a fake npm
156
+ - pre-commit blocks a commit carrying a vulnerable dependency; warns + allows when the advisory DB is unreachable
157
+ - existing secret-lane + smoke + e2e regression suite remains green
158
+
159
+ ---
160
+
143
161
  ## v3 (future)
144
162
 
145
163
  ### Ledger tamper resistance
package/SKILL.md CHANGED
@@ -10,7 +10,9 @@ hooks:
10
10
 
11
11
  # Safedeps
12
12
 
13
- Two gates, one skill. You (the agent) are the primary user drive both:
13
+ Two gates, one skill. Safedeps is an agent security skill backed by Claude/Codex hooks and a local CLI. It is not a Codex plugin bundle unless it is later wrapped with a plugin manifest.
14
+
15
+ You (the agent) are the primary user — drive both:
14
16
 
15
17
  - **Install-time gate** — clear every dependency install through an OSV-backed advisory check before it runs.
16
18
  - **Secret-leak gate** — stop a secret or a real `.env` from being committed (per-repo).
@@ -71,7 +73,7 @@ safedeps doctor --fix # scaffold the policy + activate the hooks (non-destruc
71
73
  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
74
  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
75
 
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`.
76
+ The pre-commit hook runs two checks: a secret scan (`safedeps scan secrets --staged`) on every commit (fail-closed), and an npm dependency audit (`safedeps audit npm`) on every commit in an npm repo — so a CVE published *after* you installed a package is caught at the next commit, not weeks later. `audit npm` exits 0 clean / 1 vulnerable / 2 could-not-run; the hook **blocks** on a real finding (1) but **warns and allows** when the advisory DB is unreachable (2 — observable offline failover). No secret scanner → blocks. The only bypass is the human's `git commit --no-verify`.
75
77
 
76
78
  ---
77
79
 
package/bin/safedeps CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  set -euo pipefail
9
9
 
10
- SAFEDEPS_VERSION="2.4.1"
10
+ SAFEDEPS_VERSION="2.5.1"
11
11
 
12
12
  # ---- repo / lib bootstrap ----------------------------------------------------
13
13
 
@@ -3,7 +3,17 @@ set -euo pipefail
3
3
 
4
4
  # safedeps audit npm — generic npm lockfile audit.
5
5
  # Absorbed from kuma-studio scripts/security/run-npm-audit.sh.
6
- # Missing lockfile stays fail-closed (no reproducible verdict without it).
6
+ #
7
+ # Exit codes are meaningful so a caller (e.g. the pre-commit hook) can tell a
8
+ # security verdict apart from an availability problem — npm audit collapses both
9
+ # into exit 1 on its own:
10
+ # 0 clean — no advisory at or above the audit level
11
+ # 1 vulnerable — at least one advisory at or above the level (BLOCK)
12
+ # 2 could not produce a verdict — no lockfile, npm/jq missing, or the npm
13
+ # advisory database is unreachable (offline/registry error). This is an
14
+ # AVAILABILITY failure, not a clean bill of health; the caller decides
15
+ # whether to fail-closed or warn-and-continue.
16
+ # 64 usage error
7
17
 
8
18
  REPO_ROOT=""
9
19
  AUDIT_LEVEL="${SAFEDEPS_NPM_AUDIT_LEVEL:-${KUMA_NPM_AUDIT_LEVEL:-moderate}}"
@@ -26,11 +36,53 @@ if [ -z "$REPO_ROOT" ]; then REPO_ROOT="$(pwd)"; fi
26
36
  REPO_ROOT="$(cd "$REPO_ROOT" && pwd)"
27
37
  cd "$REPO_ROOT"
28
38
 
29
- if [ ! -f package-lock.json ]; then
30
- cat >&2 <<'EOF'
31
- ERROR: package-lock.json is missing, so npm audit cannot produce a reproducible dependency verdict.
32
- EOF
33
- exit 1
39
+ if [ ! -f package-lock.json ] && [ ! -f npm-shrinkwrap.json ]; then
40
+ printf 'safedeps audit: no package-lock.json/npm-shrinkwrap.json — cannot produce a reproducible verdict.\n' >&2
41
+ exit 2
42
+ fi
43
+
44
+ if ! command -v npm >/dev/null 2>&1; then
45
+ printf 'safedeps audit: npm not found — cannot produce a dependency verdict.\n' >&2
46
+ exit 2
34
47
  fi
35
48
 
36
- exec npm audit --audit-level="$AUDIT_LEVEL"
49
+ # Without jq we cannot reliably separate an offline failure from a real finding,
50
+ # so degrade to a plain, fail-closed audit: any non-zero exit blocks (safe).
51
+ if ! command -v jq >/dev/null 2>&1; then
52
+ npm audit --audit-level="$AUDIT_LEVEL"
53
+ exit $?
54
+ fi
55
+
56
+ # One JSON run. npm audit shares exit 1 between "vulnerable" and "could not run",
57
+ # so we read the verdict from the payload, not the exit code.
58
+ audit_json="$(npm audit --json 2>/dev/null || true)"
59
+
60
+ # Unreachable advisory DB / setup error → npm emits a top-level {"error":...},
61
+ # or no/invalid JSON. Availability failure, not a verdict.
62
+ if [ -z "$audit_json" ] \
63
+ || ! printf '%s' "$audit_json" | jq -e . >/dev/null 2>&1 \
64
+ || printf '%s' "$audit_json" | jq -e 'has("error")' >/dev/null 2>&1; then
65
+ printf 'safedeps audit: could not reach the npm advisory database (offline or registry error).\n' >&2
66
+ exit 2
67
+ fi
68
+
69
+ # Count advisories at or above the configured level.
70
+ n_at_level="$(printf '%s' "$audit_json" | jq --arg lvl "$AUDIT_LEVEL" '
71
+ (.metadata.vulnerabilities // {}) as $v
72
+ | ["low","moderate","high","critical"] as $order
73
+ | (($order | index($lvl)) // 1) as $min
74
+ | reduce $order[$min:][] as $s (0; . + ($v[$s] // 0))
75
+ ')"
76
+
77
+ if [ "${n_at_level:-0}" -gt 0 ]; then
78
+ printf '%s' "$audit_json" | jq -r '
79
+ (.metadata.vulnerabilities // {}) as $v
80
+ | " severities: " +
81
+ ([ "critical","high","moderate","low" ]
82
+ | map(select(($v[.] // 0) > 0) | "\($v[.]) \(.)") | join(", "))
83
+ ' >&2
84
+ printf '%s' "$audit_json" | jq -r '(.vulnerabilities // {}) | keys[] | " - " + .' 2>/dev/null | head -20 >&2 || true
85
+ printf 'safedeps audit: %s advisory(ies) at or above "%s" — blocking. Run `npm audit` for the full report.\n' "$n_at_level" "$AUDIT_LEVEL" >&2
86
+ exit 1
87
+ fi
88
+ exit 0
@@ -1,11 +1,17 @@
1
1
  #!/bin/bash
2
- # .githooks/pre-commit — safedeps secret-leak gate (scaffolded by `safedeps hooks init`).
2
+ # .githooks/pre-commit — safedeps repo gate (scaffolded by `safedeps hooks init`).
3
3
  #
4
- # Blocks a commit when gitleaks finds a secret in the STAGED changes, so a key
5
- # or a real .env never enters the repo's history. The detection POLICY lives in
6
- # this repo's .gitleaks.toml (public) / .gitleaks.private.toml (private);
7
- # safedeps owns execution. This hook delegates to one canonical scanner path:
8
- # `safedeps scan secrets --staged`.
4
+ # Two checks:
5
+ # 1. Secret scan (always, fail-closed) blocks a commit when gitleaks finds a
6
+ # secret in the STAGED changes, so a key or a real .env never enters
7
+ # history. The detection POLICY lives in this repo's .gitleaks.toml
8
+ # (public) / .gitleaks.private.toml (private); safedeps owns execution.
9
+ # 2. Dependency audit (npm) — on every commit when this repo has an npm
10
+ # lockfile. Catches a vulnerable direct or *transitive* dependency,
11
+ # including a CVE published AFTER you installed it ("looked safe then,
12
+ # flagged now"). A real finding blocks (fail-closed); only the advisory DB
13
+ # being unreachable (offline) warns and lets the commit through — an
14
+ # observable availability failover, never a silent skip.
9
15
  #
10
16
  # Intentional bypass (use sparingly — you own the risk): git commit --no-verify
11
17
  set -euo pipefail
@@ -43,7 +49,30 @@ EOF
43
49
  exit 1
44
50
  fi
45
51
 
46
- # Delegate to the single canonical scanner. A non-zero exit means either a
47
- # secret was found OR no scanner (gitleaks/docker) is available — both
48
- # fail-closed and block the commit.
49
- exec "${sd}" scan secrets --staged --root "${repo_root}"
52
+ # 1. Secret scan (always). A non-zero exit means either a secret was found OR
53
+ # no scanner (gitleaks/docker) is available — both fail-closed, block commit.
54
+ "${sd}" scan secrets --staged --root "${repo_root}"
55
+
56
+ # 2. Dependency audit (npm) — every commit when this repo has an npm lockfile.
57
+ # `safedeps audit npm` exit codes: 0 clean / 1 vulnerable / 2 could-not-run.
58
+ # - vulnerable (1) → BLOCK (fail-closed on the security verdict).
59
+ # - unreachable (2) → WARN and ALLOW (availability failover, observable):
60
+ # an offline commit is never silently dropped, and CI / the daily
61
+ # re-check still cover what this commit could not verify.
62
+ if [ -f "${repo_root}/package-lock.json" ] || [ -f "${repo_root}/npm-shrinkwrap.json" ]; then
63
+ if "${sd}" audit npm --root "${repo_root}"; then
64
+ : # clean — no advisory at or above the audit level.
65
+ else
66
+ audit_rc=$?
67
+ if [ "${audit_rc}" -eq 2 ]; then
68
+ {
69
+ printf '\n'
70
+ printf 'safedeps pre-commit: dependency audit could not reach the advisory DB.\n'
71
+ printf ' -> commit ALLOWED without a fresh dependency verdict (offline failover).\n'
72
+ printf ' -> re-run `safedeps audit npm` once online; CI / the daily re-check also cover this.\n'
73
+ } >&2
74
+ else
75
+ exit "${audit_rc}" # vulnerable (or unknown failure) -> fail-closed.
76
+ fi
77
+ fi
78
+ fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aldegad/safedeps",
3
- "version": "2.4.1",
3
+ "version": "2.5.1",
4
4
  "description": "Dependency install safety gate with OSV-backed advisory checks, approved-spec ledger enforcement, and reorg rollback hooks",
5
5
  "main": "bin/safedeps",
6
6
  "bin": {
@@ -219,6 +219,51 @@ function unlinkBin() {
219
219
  removeSymlink(join(HOME, ".local", "bin", "safedeps"));
220
220
  }
221
221
 
222
+ function safedepsOnPath() {
223
+ const dirs = (process.env.PATH || "").split(":").filter(Boolean);
224
+ return dirs.some((d) => {
225
+ try { return existsSync(join(d, "safedeps")); } catch { return false; }
226
+ });
227
+ }
228
+
229
+ // The dependency-install gate is global and now active. The rest of the
230
+ // surface (secret-leak lane, dep audit) is per-repo and opt-in — its policy
231
+ // lives in each repo. Nudge with a recommended setup; never auto-write.
232
+ function printRecommendedSetup() {
233
+ const line = "─".repeat(58);
234
+ const out = [];
235
+ out.push("");
236
+ out.push("Recommended setup");
237
+ out.push(line);
238
+ out.push(" 1. Dependency-install gate ........ ✓ active now (global, all repos)");
239
+ out.push(" Every agent install is checked; for npm the installed");
240
+ out.push(" closure is reorged (rolled back) if it diverges.");
241
+ out.push("");
242
+ out.push(" The rest is per-repo and opt-in. Run these INSIDE a repo:");
243
+ out.push("");
244
+ out.push(" 2. Pre-commit gate ............... recommended");
245
+ out.push(" safedeps doctor # diagnose this repo (read-only)");
246
+ out.push(" safedeps doctor --fix # scaffold .gitleaks.toml + pre-commit, then activate");
247
+ out.push(" → every commit: blocks a secret / real .env (fail-closed).");
248
+ out.push(" → every commit (npm repos): audits deps for vulnerable transitives;");
249
+ out.push(" a real finding blocks, an offline advisory DB only warns + allows.");
250
+ out.push("");
251
+ out.push(" 3. Release / CI gate ............. optional");
252
+ out.push(" safedeps gates run # secret scan + npm dep audit + hook/CI check");
253
+ out.push(" → full-repo sweep for CI / pre-release: scans the whole tree (not just");
254
+ out.push(" the staged diff) and verifies the hooks themselves are installed.");
255
+ out.push("");
256
+ out.push(" Docs: README → \"Two Lanes\"");
257
+ if (!safedepsOnPath()) {
258
+ out.push("");
259
+ out.push(" Note: `safedeps` is not on your PATH. To run the commands above:");
260
+ out.push(" - re-run this installer with --link-bin (adds ~/.local/bin/safedeps), or");
261
+ out.push(" - use the full path: ~/.claude/skills/safedeps/bin/safedeps");
262
+ }
263
+ out.push("");
264
+ console.log(out.join("\n"));
265
+ }
266
+
222
267
  function main() {
223
268
  if (!existsSync(REPO_PRE_HOOK) || !existsSync(REPO_POST_HOOK)) {
224
269
  throw new Error(`hook scripts not found at ${REPO_PRE_HOOK} / ${REPO_POST_HOOK}`);
@@ -242,9 +287,7 @@ function main() {
242
287
  log("uninstall done.");
243
288
  } else {
244
289
  log("install done. New hook events fire on the next session start.");
245
- // The dependency-install gate is global. The secret-leak lane is per-repo
246
- // and stays opt-in (its policy lives in each repo). Nudge, do not auto-write.
247
- log("secret-leak lane is per-repo — in a repo run: safedeps doctor (then `safedeps doctor --fix` to scaffold + activate).");
290
+ printRecommendedSetup();
248
291
  }
249
292
  }
250
293
 
@@ -190,6 +190,50 @@ EOF
190
190
  grep -qx 'rebuild' "${tmp_root}/npm-calls.log" || fail "post hook runs npm rebuild after verified injected install"
191
191
  pass "post hook rebuilds after verified inert install"
192
192
 
193
+ # Reorg must actually revert the on-disk lockfile, not just print the message. The
194
+ # missing-transitive test below proves the systemMessage; this proves the stronger
195
+ # claim — a tampered lockfile is restored byte-for-byte to the last confirmed safe
196
+ # snapshot on disk. Regression guard so a future change cannot break the rollback
197
+ # while keeping the message green. Stub npm keeps `npm ci` from rewriting the file.
198
+ revert_project="${tmp_root}/revert-project"
199
+ mkdir -p "${revert_project}"
200
+ printf '{"dependencies":{"fixture-parent":"1.0.0"}}\n' > "${revert_project}/package.json"
201
+ cat > "${revert_project}/package-lock.json" <<'EOF'
202
+ {
203
+ "name": "revert-project",
204
+ "lockfileVersion": 3,
205
+ "packages": {
206
+ "": {"dependencies": {"fixture-parent": "1.0.0"}},
207
+ "node_modules/fixture-parent": {"version": "1.0.0", "dependencies": {"fixture-child": "1.0.0"}},
208
+ "node_modules/fixture-child": {"version": "1.0.0"}
209
+ }
210
+ }
211
+ EOF
212
+ cp "${revert_project}/package-lock.json" "${tmp_root}/revert-safe-lock.json"
213
+ scripts/safedeps-pre-guard.sh > /dev/null <<EOF
214
+ {"tool_name":"Bash","tool_input":{"command":"npm install fixture-parent@1.0.0"},"cwd":"${revert_project}"}
215
+ EOF
216
+ cat > "${revert_project}/package-lock.json" <<'EOF'
217
+ {
218
+ "name": "revert-project",
219
+ "lockfileVersion": 3,
220
+ "packages": {
221
+ "": {"dependencies": {"fixture-parent": "1.0.0"}},
222
+ "node_modules/fixture-parent": {"version": "1.0.0", "dependencies": {"fixture-child": "1.0.0"}},
223
+ "node_modules/fixture-child": {"version": "1.0.0"},
224
+ "node_modules/fixture-evil": {"version": "6.6.6", "resolved": "git://evil.example.com/fixture-evil.git"}
225
+ }
226
+ }
227
+ EOF
228
+ revert_post=$(
229
+ PATH="${stub_bin}:${PATH}" scripts/safedeps-post-verify.sh <<EOF
230
+ {"tool_name":"Bash","tool_input":{"command":"npm install fixture-parent@1.0.0"},"cwd":"${revert_project}"}
231
+ EOF
232
+ )
233
+ grep -q '의심스러운 패키지 변경 감지' <<< "${revert_post}" || fail "reorg fires on a tampered lockfile"
234
+ cmp -s "${revert_project}/package-lock.json" "${tmp_root}/revert-safe-lock.json" || fail "reorg restores the exact safe lockfile content on disk"
235
+ pass "reorg reverts a tampered lockfile to safe content on disk"
236
+
193
237
  export SAFEDEPS_HOME="${tmp_root}/safe-missing-transitive"
194
238
  export SAFEDEPS_OSV_API_URL="http://127.0.0.1:${port}/osv/v1/query"
195
239
  export SAFEDEPS_OSV_BATCH_API_URL="http://127.0.0.1:${port}/osv/v1/querybatch"
@@ -225,7 +269,13 @@ EOF
225
269
  )
226
270
  grep -q '의심스러운 패키지 변경 감지' <<< "${missing_post}" || fail "post hook reorgs unapproved transitive package"
227
271
  grep -q 'fixture-child@1.0.0' <<< "${missing_post}" || fail "post hook names unapproved transitive package"
228
- pass "post hook reorgs unapproved transitive package"
272
+ # Not just the message — the unapproved transitive must be gone from the on-disk
273
+ # lockfile. (Reorg removes the tampered lockfile; a no-network reinstall may recreate
274
+ # an empty one, so assert fixture-child is absent rather than the file itself.)
275
+ if grep -q 'fixture-child' "${missing_project}/package-lock.json" 2>/dev/null; then
276
+ fail "post hook reorg leaves the unapproved transitive in the on-disk lockfile"
277
+ fi
278
+ pass "post hook reorgs unapproved transitive package (verified on disk)"
229
279
 
230
280
  export SAFEDEPS_HOME="${tmp_root}/safe"
231
281
  export SAFEDEPS_OSV_API_URL="http://127.0.0.1:${port}/osv/v1/query"
@@ -328,4 +378,64 @@ else
328
378
  printf 'ok - pre-commit gate behavior SKIPPED (needs gitleaks + openssl)\n'
329
379
  fi
330
380
 
381
+ # --- Dependency audit gate (npm) — v2.5.0 -----------------------------------
382
+ # A fake `npm` makes the crucial distinction deterministic and offline: a
383
+ # vulnerable verdict (block) must never be confused with an unreachable advisory
384
+ # DB (warn + allow). If those two collapsed, an offline failover would silently
385
+ # let real vulnerabilities through.
386
+ fakebin="${tmp_root}/fakebin"
387
+ mkdir -p "${fakebin}"
388
+ cat > "${fakebin}/npm" <<'FAKE'
389
+ #!/bin/bash
390
+ [ "${1:-}" = "audit" ] || exit 0
391
+ case "${FAKE_NPM_MODE:-clean}" in
392
+ clean) printf '%s\n' '{"auditReportVersion":2,"vulnerabilities":{},"metadata":{"vulnerabilities":{"info":0,"low":0,"moderate":0,"high":0,"critical":0,"total":0}}}'; exit 0 ;;
393
+ vuln) printf '%s\n' '{"auditReportVersion":2,"vulnerabilities":{"hono":{"name":"hono","severity":"moderate","via":[{"title":"JWT"}]}},"metadata":{"vulnerabilities":{"info":0,"low":0,"moderate":4,"high":0,"critical":0,"total":4}}}'; exit 1 ;;
394
+ offline) printf '%s\n' '{"error":{"code":"ENOTFOUND","summary":"registry unreachable"}}'; exit 1 ;;
395
+ esac
396
+ FAKE
397
+ chmod +x "${fakebin}/npm"
398
+
399
+ if command -v jq >/dev/null 2>&1; then
400
+ audit_repo="${tmp_root}/audit-repo"
401
+ mkdir -p "${audit_repo}"
402
+ printf '{"name":"a","lockfileVersion":3}\n' > "${audit_repo}/package-lock.json"
403
+ run_audit() {
404
+ PATH="${fakebin}:${PATH}" FAKE_NPM_MODE="$1" \
405
+ "${ROOT_DIR}/bin/safedeps" audit npm --root "${audit_repo}" >/dev/null 2>&1
406
+ }
407
+ run_audit clean && rc=0 || rc=$?; [ "${rc}" = "0" ] || fail "audit exit 0 on a clean lockfile (got ${rc})"
408
+ run_audit vuln && rc=0 || rc=$?; [ "${rc}" = "1" ] || fail "audit exit 1 on a vulnerable lockfile (got ${rc})"
409
+ run_audit offline && rc=0 || rc=$?; [ "${rc}" = "2" ] || fail "audit exit 2 when the advisory DB is unreachable (got ${rc})"
410
+ pass "audit npm exit-code contract: clean=0 / vulnerable=1 / unreachable=2"
411
+ else
412
+ printf 'ok - audit exit-code contract SKIPPED (needs jq)\n'
413
+ fi
414
+
415
+ if command -v gitleaks >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
416
+ dep_repo="${tmp_root}/dep-repo"
417
+ mkdir -p "${dep_repo}"
418
+ git -C "${dep_repo}" init -q
419
+ git -C "${dep_repo}" config user.email t@safedeps.test
420
+ git -C "${dep_repo}" config user.name safedeps-e2e
421
+ HOME="${tmp_root}/doc-home" "${ROOT_DIR}/bin/safedeps" doctor --fix --root "${dep_repo}" >/dev/null
422
+ printf '{"name":"a","lockfileVersion":3}\n' > "${dep_repo}/package-lock.json"
423
+ git -C "${dep_repo}" add package-lock.json
424
+
425
+ # Threat: a vulnerable dependency must BLOCK the commit (fail-closed verdict).
426
+ if PATH="${fakebin}:${PATH}" FAKE_NPM_MODE=vuln SAFEDEPS_BIN="${ROOT_DIR}/bin/safedeps" \
427
+ git -C "${dep_repo}" commit -q -m "vuln" 2>/dev/null; then
428
+ fail "pre-commit blocks a commit carrying a vulnerable dependency"
429
+ fi
430
+
431
+ # Availability failover: an unreachable advisory DB must WARN and ALLOW.
432
+ offline_out="$(PATH="${fakebin}:${PATH}" FAKE_NPM_MODE=offline SAFEDEPS_BIN="${ROOT_DIR}/bin/safedeps" \
433
+ git -C "${dep_repo}" commit -m "offline" 2>&1)" \
434
+ || fail "pre-commit allows the commit when the advisory DB is unreachable (offline failover)"
435
+ grep -q "offline failover" <<< "${offline_out}" || fail "offline failover prints an observable warning"
436
+ pass "pre-commit dep gate: blocks on vuln, warns+allows when offline"
437
+ else
438
+ printf 'ok - pre-commit dep gate SKIPPED (needs gitleaks + jq)\n'
439
+ fi
440
+
331
441
  printf 'e2e passed\n'