@bookedsolid/rea 0.31.0 → 0.33.0

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.
Files changed (43) hide show
  1. package/.husky/prepare-commit-msg +80 -6
  2. package/MIGRATING.md +24 -15
  3. package/dist/cli/hook.js +60 -22
  4. package/dist/hooks/_lib/halt-check.d.ts +78 -0
  5. package/dist/hooks/_lib/halt-check.js +106 -0
  6. package/dist/hooks/_lib/payload.d.ts +124 -0
  7. package/dist/hooks/_lib/payload.js +245 -0
  8. package/dist/hooks/_lib/segments.d.ts +125 -0
  9. package/dist/hooks/_lib/segments.js +766 -0
  10. package/dist/hooks/architecture-review-gate/index.d.ts +58 -0
  11. package/dist/hooks/architecture-review-gate/index.js +250 -0
  12. package/dist/hooks/attribution-advisory/index.d.ts +72 -0
  13. package/dist/hooks/attribution-advisory/index.js +233 -0
  14. package/dist/hooks/bash-scanner/protected-scan.js +14 -2
  15. package/dist/hooks/changeset-security-gate/index.d.ts +71 -0
  16. package/dist/hooks/changeset-security-gate/index.js +330 -0
  17. package/dist/hooks/dependency-audit-gate/index.d.ts +91 -0
  18. package/dist/hooks/dependency-audit-gate/index.js +294 -0
  19. package/dist/hooks/env-file-protection/index.d.ts +55 -0
  20. package/dist/hooks/env-file-protection/index.js +159 -0
  21. package/dist/hooks/pr-issue-link-gate/index.d.ts +91 -0
  22. package/dist/hooks/pr-issue-link-gate/index.js +127 -0
  23. package/dist/hooks/security-disclosure-gate/index.d.ts +91 -0
  24. package/dist/hooks/security-disclosure-gate/index.js +502 -0
  25. package/hooks/_lib/protected-paths.sh +10 -3
  26. package/hooks/architecture-review-gate.sh +92 -77
  27. package/hooks/attribution-advisory.sh +139 -131
  28. package/hooks/changeset-security-gate.sh +114 -149
  29. package/hooks/dependency-audit-gate.sh +115 -156
  30. package/hooks/env-file-protection.sh +130 -97
  31. package/hooks/pr-issue-link-gate.sh +114 -45
  32. package/hooks/security-disclosure-gate.sh +148 -316
  33. package/hooks/settings-protection.sh +13 -9
  34. package/package.json +1 -1
  35. package/templates/architecture-review-gate.dogfood-staged.sh +116 -0
  36. package/templates/attribution-advisory.dogfood-staged.sh +170 -0
  37. package/templates/changeset-security-gate.dogfood-staged.sh +137 -0
  38. package/templates/dependency-audit-gate.dogfood-staged.sh +138 -0
  39. package/templates/env-file-protection.dogfood-staged.sh +157 -0
  40. package/templates/pr-issue-link-gate.dogfood-staged.sh +134 -0
  41. package/templates/prepare-commit-msg.husky.sh +80 -6
  42. package/templates/security-disclosure-gate.dogfood-staged.sh +171 -0
  43. package/templates/settings-protection.dogfood.patch +58 -0
@@ -0,0 +1,116 @@
1
+ #!/bin/bash
2
+ # PostToolUse hook: architecture-review-gate.sh
3
+ # 0.33.0+ — Node-binary shim for `rea hook architecture-review-gate`.
4
+ #
5
+ # Pre-0.33.0 the gate's full body lived here as bash (101 LOC, policy-
6
+ # driven prefix-match against `architecture_review.patterns`). The
7
+ # migration moves all of that into `src/hooks/architecture-review-gate/
8
+ # index.ts`.
9
+ #
10
+ # Behavioral contract is preserved byte-for-byte: ALWAYS exit 0
11
+ # (advisory-only) except under HALT (exit 2). The hook fires for ALL
12
+ # Write/Edit PostToolUse events, but the Node body short-circuits to
13
+ # exit 0 when patterns are unset/empty — so the cost of running the
14
+ # CLI on every write is bounded.
15
+ #
16
+ # # CLI-resolution trust boundary
17
+ #
18
+ # Realpath sandbox check + version probe. Same shape as the 0.32.0
19
+ # pilots.
20
+ #
21
+ # # Fail-OPEN posture
22
+ #
23
+ # architecture-review-gate is ADVISORY-only — the pre-0.33.0 bash body
24
+ # never refused (exit 0 only). The early-exit branches (CLI missing,
25
+ # node missing, sandbox failed, version skew) all exit 0 silently
26
+ # because there is nothing to "preserve protection" for. The HALT
27
+ # check is the only path to exit 2.
28
+
29
+ set -uo pipefail
30
+
31
+ # 1. HALT check.
32
+ # shellcheck source=_lib/halt-check.sh
33
+ source "$(dirname "$0")/_lib/halt-check.sh"
34
+ check_halt
35
+ REA_ROOT=$(rea_root)
36
+
37
+ proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
38
+
39
+ # 2. No relevance pre-gate — architecture-review-gate fires on every
40
+ # Write/Edit, and the cost of the Node body's early-out (load
41
+ # policy, check patterns array, prefix-match) is well under the
42
+ # cost of a sandbox/probe pair. Capture stdin once.
43
+ INPUT=$(cat)
44
+
45
+ # 3. Resolve the rea CLI. Advisory-tier: exit 0 silently on missing
46
+ # CLI — nothing to enforce.
47
+ REA_ARGV=()
48
+ RESOLVED_CLI_PATH=""
49
+ if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
50
+ REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
51
+ RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
52
+ elif [ -f "$proj/dist/cli/index.js" ]; then
53
+ REA_ARGV=(node "$proj/dist/cli/index.js")
54
+ RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
55
+ fi
56
+
57
+ if [ "${#REA_ARGV[@]}" -eq 0 ]; then
58
+ exit 0
59
+ fi
60
+
61
+ # 4. Realpath sandbox check. Advisory-tier: exit 0 silently on
62
+ # sandbox failure (with a single-line breadcrumb to stderr).
63
+ if ! command -v node >/dev/null 2>&1; then
64
+ exit 0
65
+ fi
66
+
67
+ sandbox_check=$(node -e '
68
+ const fs = require("fs");
69
+ const path = require("path");
70
+ const cli = process.argv[1];
71
+ const projDir = process.argv[2];
72
+ let real, realProj;
73
+ try { real = fs.realpathSync(cli); } catch (e) {
74
+ process.stdout.write("bad:realpath"); process.exit(1);
75
+ }
76
+ try { realProj = fs.realpathSync(projDir); } catch (e) {
77
+ process.stdout.write("bad:realpath-proj"); process.exit(1);
78
+ }
79
+ const sep = path.sep;
80
+ const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
81
+ if (!(real === realProj || real.startsWith(projWithSep))) {
82
+ process.stdout.write("bad:cli-escapes-project"); process.exit(1);
83
+ }
84
+ let cur = path.dirname(path.dirname(path.dirname(real)));
85
+ let found = false;
86
+ for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
87
+ const pj = path.join(cur, "package.json");
88
+ if (fs.existsSync(pj)) {
89
+ try {
90
+ const data = JSON.parse(fs.readFileSync(pj, "utf8"));
91
+ if (data && data.name === "@bookedsolid/rea") { found = true; break; }
92
+ } catch (e) { /* keep walking */ }
93
+ }
94
+ cur = path.dirname(cur);
95
+ }
96
+ if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
97
+ process.stdout.write("ok");
98
+ ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
99
+
100
+ if [ "$sandbox_check" != "ok" ]; then
101
+ printf 'rea: architecture-review-gate skipped (sandbox check: %s)\n' "$sandbox_check" >&2
102
+ exit 0
103
+ fi
104
+
105
+ # 5. Version-probe. Advisory-tier: exit 0 on probe failure.
106
+ probe_out=$("${REA_ARGV[@]}" hook architecture-review-gate --help 2>&1)
107
+ probe_status=$?
108
+ if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'architecture-review-gate'; then
109
+ printf 'rea: this shim requires the `rea hook architecture-review-gate` subcommand (introduced in 0.33.0).\n' >&2
110
+ printf 'Run `pnpm install` (or `npm install`) to sync the CLI; falling through silently.\n' >&2
111
+ exit 0
112
+ fi
113
+
114
+ # 6. Forward stdin.
115
+ printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook architecture-review-gate
116
+ exit $?
@@ -0,0 +1,170 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: attribution-advisory.sh
3
+ # 0.32.0+ — Node-binary shim for `rea hook attribution-advisory`.
4
+ #
5
+ # Pre-0.32.0 the gate's full body lived here as bash (162 LOC,
6
+ # including the AI-attribution pattern catalog and segment-relevance
7
+ # gating). The migration to the parser-backed Node binary moves all
8
+ # of that into `src/hooks/attribution-advisory/index.ts`. This shim
9
+ # is the Claude Code dispatcher's view of the hook — it forwards
10
+ # stdin to the CLI and exits with whatever the CLI returns.
11
+ #
12
+ # Behavioral contract is preserved byte-for-byte: exit 0 on
13
+ # disabled-policy / non-relevant / clean-command, exit 2 on HALT /
14
+ # attribution detected / malformed payload (fail-closed).
15
+ #
16
+ # # CLI-resolution trust boundary
17
+ #
18
+ # Codex round 1 P1 (2026-05-15): realpath sandbox check + version
19
+ # probe. Mirrors delegation-advisory.sh §3. Defends against
20
+ # symlink-out + tarball-replacement attacks on the resolved CLI AND
21
+ # stale-node_modules version skew that would otherwise turn every
22
+ # Bash dispatch into a hard failure.
23
+
24
+ set -uo pipefail
25
+
26
+ # 1. HALT check.
27
+ # shellcheck source=_lib/halt-check.sh
28
+ source "$(dirname "$0")/_lib/halt-check.sh"
29
+ check_halt
30
+ REA_ROOT=$(rea_root)
31
+
32
+ proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
33
+
34
+ # 2. Relevance pre-gate (0.32.0 round-5 P1, round-6 fix). PreToolUse
35
+ # Bash matchers fire on EVERY shell command, but this hook only
36
+ # enforces against `git commit` / `gh pr create|edit`. Capture
37
+ # stdin + check relevance FIRST so unrelated commands (ls,
38
+ # pnpm test, …) exit 0 even when the CLI is missing/stale/
39
+ # sandboxed-out.
40
+ #
41
+ # Match the pattern ANYWHERE in the command string (after the
42
+ # opening quote, then `[^"]*` for any leading shell prefix —
43
+ # `sudo`, `time`, env assignments like `FOO=x git commit …`).
44
+ # Round-6 P1: prior round-5 pattern anchored at the start of the
45
+ # JSON value and missed all prefixed forms.
46
+ INPUT=$(cat)
47
+ # Substring scan (NOT JSON-aware). Round-7 P2: any JSON-aware regex
48
+ # anchored on `"command":"...` gets tripped by escaped quotes in
49
+ # quoted env prefixes (`FOO="two words" git commit …` → the payload
50
+ # carries `\"two words\"` and `[^"]*` stops at the escaped quote).
51
+ # Plain substring match has no such edge: it over-triggers only on
52
+ # the rare case where the pattern appears inside a quoted argument
53
+ # (`echo "gh pr create"`), and the Node body handles that correctly.
54
+ # This hook only fires on `tool_name=Bash`, so we don't risk matching
55
+ # unrelated payload shapes.
56
+ RELEVANT=0
57
+ if printf '%s' "$INPUT" | grep -qE '(git[[:space:]]+commit|gh[[:space:]]+pr[[:space:]]+(create|edit))'; then
58
+ RELEVANT=1
59
+ fi
60
+ if [ "$RELEVANT" -eq 0 ]; then
61
+ # Irrelevant Bash call — nothing the pre-0.32.0 body would have
62
+ # processed. Always exit 0 regardless of CLI state.
63
+ exit 0
64
+ fi
65
+
66
+ # 2b. Policy short-circuit (round-6 P2). The pre-0.32.0 bash body
67
+ # no-op'd when `block_ai_attribution` was absent or false. Without
68
+ # this check, an unbuilt/stale install would refuse `git commit`
69
+ # even on repos that DELIBERATELY disable the attribution gate.
70
+ # Read the policy via a simple grep — the canonical loader
71
+ # handles inline forms but we only need block form here, and a
72
+ # conservative "true-and-only-true counts" rule matches the
73
+ # intent (false / absent / inline-only all → no enforcement).
74
+ POLICY_FILE="$REA_ROOT/.rea/policy.yaml"
75
+ if [ ! -f "$POLICY_FILE" ] || ! grep -qE '^block_ai_attribution:[[:space:]]*true([[:space:]]|$)' "$POLICY_FILE"; then
76
+ # Attribution blocking disabled — pre-0.32.0 bash body would have
77
+ # exited 0 here. Don't refuse on stale-install grounds.
78
+ exit 0
79
+ fi
80
+
81
+ # 3. Resolve the rea CLI.
82
+ REA_ARGV=()
83
+ RESOLVED_CLI_PATH=""
84
+ if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
85
+ REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
86
+ RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
87
+ elif [ -f "$proj/dist/cli/index.js" ]; then
88
+ REA_ARGV=(node "$proj/dist/cli/index.js")
89
+ RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
90
+ fi
91
+
92
+ if [ "${#REA_ARGV[@]}" -eq 0 ]; then
93
+ # 0.32.0 round-4 P2: when `block_ai_attribution: true`, this hook is
94
+ # blocking-tier — the pre-0.32.0 bash body enforced the policy
95
+ # without a compiled CLI. Falling through to exit 0 would silently
96
+ # let AI-attribution patterns through every git commit / gh pr
97
+ # create-or-edit until the operator rebuilds. Fail closed and tell
98
+ # the operator how to restore protection.
99
+ printf 'rea: attribution-advisory cannot run — the rea CLI is not built.\n' >&2
100
+ printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
101
+ printf 'This shim fails closed because the pre-0.32.0 bash body enforced attribution policy without a CLI.\n' >&2
102
+ exit 2
103
+ fi
104
+
105
+ # 3. Realpath sandbox check.
106
+ if ! command -v node >/dev/null 2>&1; then
107
+ printf 'rea: attribution-advisory cannot run — `node` is not on PATH.\n' >&2
108
+ printf 'Install Node 22+ (engines.node) to restore enforcement.\n' >&2
109
+ exit 2
110
+ fi
111
+
112
+ sandbox_check=$(node -e '
113
+ const fs = require("fs");
114
+ const path = require("path");
115
+ const cli = process.argv[1];
116
+ const projDir = process.argv[2];
117
+ let real, realProj;
118
+ try { real = fs.realpathSync(cli); } catch (e) {
119
+ process.stdout.write("bad:realpath"); process.exit(1);
120
+ }
121
+ try { realProj = fs.realpathSync(projDir); } catch (e) {
122
+ process.stdout.write("bad:realpath-proj"); process.exit(1);
123
+ }
124
+ const sep = path.sep;
125
+ const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
126
+ if (!(real === realProj || real.startsWith(projWithSep))) {
127
+ process.stdout.write("bad:cli-escapes-project"); process.exit(1);
128
+ }
129
+ let cur = path.dirname(path.dirname(path.dirname(real)));
130
+ let found = false;
131
+ for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
132
+ const pj = path.join(cur, "package.json");
133
+ if (fs.existsSync(pj)) {
134
+ try {
135
+ const data = JSON.parse(fs.readFileSync(pj, "utf8"));
136
+ if (data && data.name === "@bookedsolid/rea") { found = true; break; }
137
+ } catch (e) { /* keep walking */ }
138
+ }
139
+ cur = path.dirname(cur);
140
+ }
141
+ if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
142
+ process.stdout.write("ok");
143
+ ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
144
+
145
+ if [ "$sandbox_check" != "ok" ]; then
146
+ # 0.32.0 round-4 P2: fail closed (blocking-tier when policy enables —
147
+ # see top-of-file rationale). Sandbox failure means the CLI cannot
148
+ # be authenticated; refuse rather than silently bypass.
149
+ printf 'rea: attribution-advisory FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
150
+ exit 2
151
+ fi
152
+
153
+ # 4. Version-probe: confirm the resolved CLI implements
154
+ # `hook attribution-advisory`. Codex round 1 P1.
155
+ probe_out=$("${REA_ARGV[@]}" hook attribution-advisory --help 2>&1)
156
+ probe_status=$?
157
+ if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'attribution-advisory'; then
158
+ # 0.32.0 round-4 P2: stale/older CLI without the new subcommand is
159
+ # NOT advisory-tier fall-through — the bash body it replaces
160
+ # enforced when policy enabled. Fail closed and tell the operator
161
+ # exactly how to fix.
162
+ printf 'rea: this shim requires the `rea hook attribution-advisory` subcommand (introduced in 0.32.0).\n' >&2
163
+ printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
164
+ printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
165
+ exit 2
166
+ fi
167
+
168
+ # 5. Forward stdin (already captured up-front for the relevance gate).
169
+ printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook attribution-advisory
170
+ exit $?
@@ -0,0 +1,137 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: changeset-security-gate.sh
3
+ # 0.33.0+ — Node-binary shim for `rea hook changeset-security-gate`.
4
+ #
5
+ # Pre-0.33.0 the gate's full body lived here as bash (172 LOC, frontmatter
6
+ # validation + GHSA/CVE scan + MultiEdit-aware tool handling). The
7
+ # migration to the parser-backed Node binary moves all of that into
8
+ # `src/hooks/changeset-security-gate/index.ts`.
9
+ #
10
+ # Behavioral contract is preserved byte-for-byte: exit 0 on
11
+ # pass-through / non-changeset / valid frontmatter, exit 2 on HALT /
12
+ # disclosure leak / malformed frontmatter / malformed payload.
13
+ #
14
+ # # CLI-resolution trust boundary
15
+ #
16
+ # Realpath sandbox check + version probe. Same shape as the 0.32.0
17
+ # pilots.
18
+ #
19
+ # # Fail-closed posture
20
+ #
21
+ # changeset-security-gate is BLOCKING-tier — the pre-0.33.0 bash body
22
+ # refused on GHSA/CVE patterns and on malformed frontmatter. Early-exit
23
+ # branches fail closed AFTER the relevance pre-gate passes.
24
+
25
+ set -uo pipefail
26
+
27
+ # 1. HALT check.
28
+ # shellcheck source=_lib/halt-check.sh
29
+ source "$(dirname "$0")/_lib/halt-check.sh"
30
+ check_halt
31
+ REA_ROOT=$(rea_root)
32
+
33
+ proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
34
+
35
+ # 2. Relevance pre-gate. This is a PreToolUse Write/Edit/MultiEdit/
36
+ # NotebookEdit matcher, so the payload always has a `tool_input.
37
+ # file_path` (or `notebook_path`).
38
+ #
39
+ # 2026-05-15 codex round-2 P2 fix: scan `tool_input.file_path` /
40
+ # `tool_input.notebook_path` ONLY, NOT the raw JSON payload. Pre-fix
41
+ # a Write to `README.md` whose body merely mentions `.changeset/`
42
+ # (e.g. "See .changeset/example.md") tripped the fail-closed branch
43
+ # when the CLI was unbuilt — the substring lived in the
44
+ # tool_input.content blob, not in the target path. The Node body
45
+ # correctly filters by file_path; the shim's pre-gate must match
46
+ # that posture.
47
+ INPUT=$(cat)
48
+ RELEVANT=0
49
+ PROBE=""
50
+ if command -v jq >/dev/null 2>&1; then
51
+ PROBE=$(printf '%s' "$INPUT" | jq -r '(.tool_input.file_path // .tool_input.notebook_path // "")' 2>/dev/null || true)
52
+ if printf '%s' "$PROBE" | grep -qE '\.changeset/'; then
53
+ RELEVANT=1
54
+ fi
55
+ else
56
+ if printf '%s' "$INPUT" | grep -qE '\.changeset/'; then
57
+ RELEVANT=1
58
+ fi
59
+ fi
60
+ if [ "$RELEVANT" -eq 0 ]; then
61
+ exit 0
62
+ fi
63
+
64
+ # 3. Resolve the rea CLI.
65
+ REA_ARGV=()
66
+ RESOLVED_CLI_PATH=""
67
+ if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
68
+ REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
69
+ RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
70
+ elif [ -f "$proj/dist/cli/index.js" ]; then
71
+ REA_ARGV=(node "$proj/dist/cli/index.js")
72
+ RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
73
+ fi
74
+
75
+ if [ "${#REA_ARGV[@]}" -eq 0 ]; then
76
+ printf 'rea: changeset-security-gate cannot run — the rea CLI is not built.\n' >&2
77
+ printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
78
+ exit 2
79
+ fi
80
+
81
+ # 4. Realpath sandbox check.
82
+ if ! command -v node >/dev/null 2>&1; then
83
+ printf 'rea: changeset-security-gate cannot run — `node` is not on PATH.\n' >&2
84
+ exit 2
85
+ fi
86
+
87
+ sandbox_check=$(node -e '
88
+ const fs = require("fs");
89
+ const path = require("path");
90
+ const cli = process.argv[1];
91
+ const projDir = process.argv[2];
92
+ let real, realProj;
93
+ try { real = fs.realpathSync(cli); } catch (e) {
94
+ process.stdout.write("bad:realpath"); process.exit(1);
95
+ }
96
+ try { realProj = fs.realpathSync(projDir); } catch (e) {
97
+ process.stdout.write("bad:realpath-proj"); process.exit(1);
98
+ }
99
+ const sep = path.sep;
100
+ const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
101
+ if (!(real === realProj || real.startsWith(projWithSep))) {
102
+ process.stdout.write("bad:cli-escapes-project"); process.exit(1);
103
+ }
104
+ let cur = path.dirname(path.dirname(path.dirname(real)));
105
+ let found = false;
106
+ for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
107
+ const pj = path.join(cur, "package.json");
108
+ if (fs.existsSync(pj)) {
109
+ try {
110
+ const data = JSON.parse(fs.readFileSync(pj, "utf8"));
111
+ if (data && data.name === "@bookedsolid/rea") { found = true; break; }
112
+ } catch (e) { /* keep walking */ }
113
+ }
114
+ cur = path.dirname(cur);
115
+ }
116
+ if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
117
+ process.stdout.write("ok");
118
+ ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
119
+
120
+ if [ "$sandbox_check" != "ok" ]; then
121
+ printf 'rea: changeset-security-gate FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
122
+ exit 2
123
+ fi
124
+
125
+ # 5. Version-probe.
126
+ probe_out=$("${REA_ARGV[@]}" hook changeset-security-gate --help 2>&1)
127
+ probe_status=$?
128
+ if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'changeset-security-gate'; then
129
+ printf 'rea: this shim requires the `rea hook changeset-security-gate` subcommand (introduced in 0.33.0).\n' >&2
130
+ printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
131
+ printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
132
+ exit 2
133
+ fi
134
+
135
+ # 6. Forward stdin.
136
+ printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook changeset-security-gate
137
+ exit $?
@@ -0,0 +1,138 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: dependency-audit-gate.sh
3
+ # 0.33.0+ — Node-binary shim for `rea hook dependency-audit-gate`.
4
+ #
5
+ # Pre-0.33.0 the gate's full body lived here as bash (179 LOC, the
6
+ # segment splitter + install-pattern detection + per-package
7
+ # `npm view` probe). The migration to the parser-backed Node binary
8
+ # moves all of that into `src/hooks/dependency-audit-gate/index.ts`.
9
+ #
10
+ # Behavioral contract is preserved byte-for-byte: exit 0 on
11
+ # pass-through / all-packages-verified, exit 2 on HALT / any package
12
+ # missing / malformed payload.
13
+ #
14
+ # # CLI-resolution trust boundary
15
+ #
16
+ # Realpath sandbox check + version probe. Same shape as the 0.32.0
17
+ # pilots and the env-file-protection shim above.
18
+ #
19
+ # # Fail-closed posture
20
+ #
21
+ # dependency-audit-gate is BLOCKING-tier — the pre-0.33.0 bash body
22
+ # refused on missing packages. Early-exit branches (CLI missing,
23
+ # node missing, sandbox failed, version skew) fail closed AFTER the
24
+ # relevance pre-gate passes.
25
+
26
+ set -uo pipefail
27
+
28
+ # 1. HALT check.
29
+ # shellcheck source=_lib/halt-check.sh
30
+ source "$(dirname "$0")/_lib/halt-check.sh"
31
+ check_halt
32
+ REA_ROOT=$(rea_root)
33
+
34
+ proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
35
+
36
+ # 2. Relevance pre-gate. Look for any install-pattern keyword.
37
+ #
38
+ # 2026-05-15 codex round-2 P2 fix: scan `tool_input.command` ONLY,
39
+ # not the raw JSON payload. Pre-fix `git commit -m "docs: run pnpm
40
+ # install foo before start"` triggered the fail-closed branch on a
41
+ # fresh checkout (the install-pattern regex hit the substring
42
+ # inside the commit-message ARG of the git command, not a real
43
+ # install invocation). The Node body's segment-anchored matcher
44
+ # correctly distinguishes between the two — the shim's pre-gate
45
+ # must match that posture.
46
+ #
47
+ # `jq`-less fallback preserves the pre-0.33.0 over-trigger shape.
48
+ INPUT=$(cat)
49
+ RELEVANT=0
50
+ PROBE=""
51
+ if command -v jq >/dev/null 2>&1; then
52
+ PROBE=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || true)
53
+ if printf '%s' "$PROBE" | grep -qE '(npm[[:space:]]+(install|i|add)|pnpm[[:space:]]+(add|install|i)|yarn[[:space:]]+add)[[:space:]]'; then
54
+ RELEVANT=1
55
+ fi
56
+ else
57
+ if printf '%s' "$INPUT" | grep -qE '(npm[[:space:]]+(install|i|add)|pnpm[[:space:]]+(add|install|i)|yarn[[:space:]]+add)[[:space:]]'; then
58
+ RELEVANT=1
59
+ fi
60
+ fi
61
+ if [ "$RELEVANT" -eq 0 ]; then
62
+ exit 0
63
+ fi
64
+
65
+ # 3. Resolve the rea CLI.
66
+ REA_ARGV=()
67
+ RESOLVED_CLI_PATH=""
68
+ if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
69
+ REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
70
+ RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
71
+ elif [ -f "$proj/dist/cli/index.js" ]; then
72
+ REA_ARGV=(node "$proj/dist/cli/index.js")
73
+ RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
74
+ fi
75
+
76
+ if [ "${#REA_ARGV[@]}" -eq 0 ]; then
77
+ printf 'rea: dependency-audit-gate cannot run — the rea CLI is not built.\n' >&2
78
+ printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
79
+ exit 2
80
+ fi
81
+
82
+ # 4. Realpath sandbox check.
83
+ if ! command -v node >/dev/null 2>&1; then
84
+ printf 'rea: dependency-audit-gate cannot run — `node` is not on PATH.\n' >&2
85
+ exit 2
86
+ fi
87
+
88
+ sandbox_check=$(node -e '
89
+ const fs = require("fs");
90
+ const path = require("path");
91
+ const cli = process.argv[1];
92
+ const projDir = process.argv[2];
93
+ let real, realProj;
94
+ try { real = fs.realpathSync(cli); } catch (e) {
95
+ process.stdout.write("bad:realpath"); process.exit(1);
96
+ }
97
+ try { realProj = fs.realpathSync(projDir); } catch (e) {
98
+ process.stdout.write("bad:realpath-proj"); process.exit(1);
99
+ }
100
+ const sep = path.sep;
101
+ const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
102
+ if (!(real === realProj || real.startsWith(projWithSep))) {
103
+ process.stdout.write("bad:cli-escapes-project"); process.exit(1);
104
+ }
105
+ let cur = path.dirname(path.dirname(path.dirname(real)));
106
+ let found = false;
107
+ for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
108
+ const pj = path.join(cur, "package.json");
109
+ if (fs.existsSync(pj)) {
110
+ try {
111
+ const data = JSON.parse(fs.readFileSync(pj, "utf8"));
112
+ if (data && data.name === "@bookedsolid/rea") { found = true; break; }
113
+ } catch (e) { /* keep walking */ }
114
+ }
115
+ cur = path.dirname(cur);
116
+ }
117
+ if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
118
+ process.stdout.write("ok");
119
+ ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
120
+
121
+ if [ "$sandbox_check" != "ok" ]; then
122
+ printf 'rea: dependency-audit-gate FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
123
+ exit 2
124
+ fi
125
+
126
+ # 5. Version-probe.
127
+ probe_out=$("${REA_ARGV[@]}" hook dependency-audit-gate --help 2>&1)
128
+ probe_status=$?
129
+ if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'dependency-audit-gate'; then
130
+ printf 'rea: this shim requires the `rea hook dependency-audit-gate` subcommand (introduced in 0.33.0).\n' >&2
131
+ printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
132
+ printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
133
+ exit 2
134
+ fi
135
+
136
+ # 6. Forward stdin.
137
+ printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook dependency-audit-gate
138
+ exit $?