@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.
- package/.husky/prepare-commit-msg +80 -6
- package/MIGRATING.md +24 -15
- package/dist/cli/hook.js +60 -22
- package/dist/hooks/_lib/halt-check.d.ts +78 -0
- package/dist/hooks/_lib/halt-check.js +106 -0
- package/dist/hooks/_lib/payload.d.ts +124 -0
- package/dist/hooks/_lib/payload.js +245 -0
- package/dist/hooks/_lib/segments.d.ts +125 -0
- package/dist/hooks/_lib/segments.js +766 -0
- package/dist/hooks/architecture-review-gate/index.d.ts +58 -0
- package/dist/hooks/architecture-review-gate/index.js +250 -0
- package/dist/hooks/attribution-advisory/index.d.ts +72 -0
- package/dist/hooks/attribution-advisory/index.js +233 -0
- package/dist/hooks/bash-scanner/protected-scan.js +14 -2
- package/dist/hooks/changeset-security-gate/index.d.ts +71 -0
- package/dist/hooks/changeset-security-gate/index.js +330 -0
- package/dist/hooks/dependency-audit-gate/index.d.ts +91 -0
- package/dist/hooks/dependency-audit-gate/index.js +294 -0
- package/dist/hooks/env-file-protection/index.d.ts +55 -0
- package/dist/hooks/env-file-protection/index.js +159 -0
- package/dist/hooks/pr-issue-link-gate/index.d.ts +91 -0
- package/dist/hooks/pr-issue-link-gate/index.js +127 -0
- package/dist/hooks/security-disclosure-gate/index.d.ts +91 -0
- package/dist/hooks/security-disclosure-gate/index.js +502 -0
- package/hooks/_lib/protected-paths.sh +10 -3
- package/hooks/architecture-review-gate.sh +92 -77
- package/hooks/attribution-advisory.sh +139 -131
- package/hooks/changeset-security-gate.sh +114 -149
- package/hooks/dependency-audit-gate.sh +115 -156
- package/hooks/env-file-protection.sh +130 -97
- package/hooks/pr-issue-link-gate.sh +114 -45
- package/hooks/security-disclosure-gate.sh +148 -316
- package/hooks/settings-protection.sh +13 -9
- package/package.json +1 -1
- package/templates/architecture-review-gate.dogfood-staged.sh +116 -0
- package/templates/attribution-advisory.dogfood-staged.sh +170 -0
- package/templates/changeset-security-gate.dogfood-staged.sh +137 -0
- package/templates/dependency-audit-gate.dogfood-staged.sh +138 -0
- package/templates/env-file-protection.dogfood-staged.sh +157 -0
- package/templates/pr-issue-link-gate.dogfood-staged.sh +134 -0
- package/templates/prepare-commit-msg.husky.sh +80 -6
- package/templates/security-disclosure-gate.dogfood-staged.sh +171 -0
- 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 $?
|