@bookedsolid/rea 0.32.0 → 0.34.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/dist/cli/hook.js +49 -0
- package/dist/hooks/_lib/payload.d.ts +38 -0
- package/dist/hooks/_lib/payload.js +79 -0
- package/dist/hooks/_lib/segments.d.ts +127 -0
- package/dist/hooks/_lib/segments.js +628 -16
- package/dist/hooks/architecture-review-gate/index.d.ts +58 -0
- package/dist/hooks/architecture-review-gate/index.js +250 -0
- package/dist/hooks/changeset-security-gate/index.d.ts +71 -0
- package/dist/hooks/changeset-security-gate/index.js +330 -0
- package/dist/hooks/dangerous-bash-interceptor/index.d.ts +103 -0
- package/dist/hooks/dangerous-bash-interceptor/index.js +669 -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/local-review-gate/index.d.ts +145 -0
- package/dist/hooks/local-review-gate/index.js +374 -0
- package/dist/hooks/secret-scanner/index.d.ts +143 -0
- package/dist/hooks/secret-scanner/index.js +404 -0
- package/hooks/architecture-review-gate.sh +92 -77
- package/hooks/changeset-security-gate.sh +114 -149
- package/hooks/dangerous-bash-interceptor.sh +168 -386
- package/hooks/dependency-audit-gate.sh +115 -156
- package/hooks/env-file-protection.sh +130 -97
- package/hooks/local-review-gate.sh +523 -410
- package/hooks/secret-scanner.sh +210 -200
- package/package.json +1 -1
- package/templates/architecture-review-gate.dogfood-staged.sh +116 -0
- package/templates/changeset-security-gate.dogfood-staged.sh +137 -0
- package/templates/dangerous-bash-interceptor.dogfood-staged.sh +196 -0
- package/templates/dependency-audit-gate.dogfood-staged.sh +138 -0
- package/templates/env-file-protection.dogfood-staged.sh +157 -0
- package/templates/local-review-gate.dogfood-staged.sh +573 -0
- package/templates/secret-scanner.dogfood-staged.sh +240 -0
|
@@ -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,196 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: dangerous-bash-interceptor.sh
|
|
3
|
+
# 0.34.0+ — Node-binary shim for `rea hook dangerous-bash-interceptor`.
|
|
4
|
+
#
|
|
5
|
+
# Pre-0.34.0 the gate's full body lived here as bash (414 LOC, every
|
|
6
|
+
# refusal class H1-H17 + M1 plus their bypass-corpus regressions). The
|
|
7
|
+
# migration to the parser-backed Node binary moves all of that into
|
|
8
|
+
# `src/hooks/dangerous-bash-interceptor/index.ts`. This shim is the
|
|
9
|
+
# Claude Code dispatcher's view of the hook — it forwards stdin to
|
|
10
|
+
# the CLI and exits with whatever the CLI returns.
|
|
11
|
+
#
|
|
12
|
+
# Behavioral contract is preserved byte-for-byte: exit 0 on
|
|
13
|
+
# pass-through / MEDIUM-only advisory, exit 2 on HALT / HIGH rule
|
|
14
|
+
# match / malformed payload (fail-closed).
|
|
15
|
+
#
|
|
16
|
+
# # CLI-resolution trust boundary
|
|
17
|
+
#
|
|
18
|
+
# Mirrors the 0.32.0 final shim shape (round-8 of the codex iteration
|
|
19
|
+
# on the three Phase 1 pilots). The resolved CLI MUST live INSIDE
|
|
20
|
+
# realpath(CLAUDE_PROJECT_DIR) AND have an ancestor `package.json`
|
|
21
|
+
# whose `name` is `@bookedsolid/rea`. Defends against symlink-out and
|
|
22
|
+
# tarball-replacement attacks on the resolved CLI.
|
|
23
|
+
#
|
|
24
|
+
# # Fail-closed posture
|
|
25
|
+
#
|
|
26
|
+
# dangerous-bash-interceptor is the agent-runaway gate — the pre-0.34.0
|
|
27
|
+
# bash body refused destructive commands without any compiled CLI. The
|
|
28
|
+
# early-exit branches (CLI missing, node missing, sandbox failed,
|
|
29
|
+
# version skew) fail closed AFTER the relevance pre-gate passes.
|
|
30
|
+
# Irrelevant Bash calls exit 0 regardless of CLI state.
|
|
31
|
+
#
|
|
32
|
+
# # Relevance pre-gate
|
|
33
|
+
#
|
|
34
|
+
# 0.34.0 round-7 P1 fix: the pre-0.34.0 bash body refused destructive
|
|
35
|
+
# commands without any compiled CLI. The round-0 shim preserved that
|
|
36
|
+
# fail-closed-on-CLI-missing posture for ALL Bash, but that's stricter
|
|
37
|
+
# than the pre-0.34.0 body which only refused commands matching the
|
|
38
|
+
# destructive catalog. On a fresh / unbuilt install (`npx rea init`,
|
|
39
|
+
# pre-`pnpm build` checkout) the shim blocked benign Bash like `ls`,
|
|
40
|
+
# `mkdir`, `pnpm install` — defeating the install path itself.
|
|
41
|
+
#
|
|
42
|
+
# Fix: substring pre-gate over the EXTRACTED command (not raw payload —
|
|
43
|
+
# the local-review-gate round-2 lesson). When CLI is missing AND no
|
|
44
|
+
# destructive-keyword appears in the extracted command, exit 0 (the
|
|
45
|
+
# pre-0.34.0 bash body would have done the same — there's no rule to
|
|
46
|
+
# match). When CLI is missing AND a destructive-keyword DOES appear,
|
|
47
|
+
# preserve the original fail-closed posture (we'd rather refuse than
|
|
48
|
+
# silently allow a destructive command).
|
|
49
|
+
#
|
|
50
|
+
# The keyword list is coarse — it over-triggers (e.g. `git status` hits
|
|
51
|
+
# `git` substring) but that's fine: the CLI does the real evaluation
|
|
52
|
+
# and lets benign forms through. Over-trigger costs one node-spawn;
|
|
53
|
+
# under-trigger is the bypass we MUST avoid. Same posture as the
|
|
54
|
+
# 0.32.0 secret-scanner `gh issue create` substring fix.
|
|
55
|
+
|
|
56
|
+
set -uo pipefail
|
|
57
|
+
|
|
58
|
+
# 1. HALT check.
|
|
59
|
+
# shellcheck source=_lib/halt-check.sh
|
|
60
|
+
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
61
|
+
check_halt
|
|
62
|
+
REA_ROOT=$(rea_root)
|
|
63
|
+
|
|
64
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
65
|
+
|
|
66
|
+
# 2. Capture stdin once. The CLI consumes it via stdin pipe below.
|
|
67
|
+
INPUT=$(cat)
|
|
68
|
+
|
|
69
|
+
# 3. Resolve the rea CLI through the fixed 2-tier sandboxed order.
|
|
70
|
+
REA_ARGV=()
|
|
71
|
+
RESOLVED_CLI_PATH=""
|
|
72
|
+
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
73
|
+
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
74
|
+
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
75
|
+
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
76
|
+
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
77
|
+
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# 3b. Relevance pre-gate (round-7 P1). Only used when the CLI is
|
|
81
|
+
# missing — when present, every Bash call goes through the CLI.
|
|
82
|
+
# Extract the command string from the payload, then substring-scan
|
|
83
|
+
# it for destructive-catalog keywords. Mirrors the H1-H17 + M1
|
|
84
|
+
# rule heads.
|
|
85
|
+
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
86
|
+
CLI_MISSING_CMD=""
|
|
87
|
+
if command -v jq >/dev/null 2>&1; then
|
|
88
|
+
# Match the CLI's payload schema: tool_input.command. tostring so
|
|
89
|
+
# a non-string value (object/number) doesn't blow up jq.
|
|
90
|
+
CLI_MISSING_CMD=$(printf '%s' "$INPUT" | jq -r '
|
|
91
|
+
(.tool_input.command // "") | tostring
|
|
92
|
+
' 2>/dev/null || true)
|
|
93
|
+
else
|
|
94
|
+
# jq missing — fall back to scanning the raw payload. Over-trigger
|
|
95
|
+
# by design (the CLI is the source of truth; this is fail-closed
|
|
96
|
+
# only when keywords match). Substring scan still catches the
|
|
97
|
+
# destructive forms in JSON-string-encoded payloads.
|
|
98
|
+
CLI_MISSING_CMD="$INPUT"
|
|
99
|
+
fi
|
|
100
|
+
# If we couldn't extract a command, treat as relevant (fail closed).
|
|
101
|
+
CLI_MISSING_RELEVANT=0
|
|
102
|
+
if [ -z "$CLI_MISSING_CMD" ]; then
|
|
103
|
+
# Empty command (or non-Bash payload). The pre-0.34.0 bash body
|
|
104
|
+
# would have exited 0 here — no command, no rule match.
|
|
105
|
+
exit 0
|
|
106
|
+
fi
|
|
107
|
+
# Substring scan. Keywords cover every rule head H1-H17 + M1. Coarse
|
|
108
|
+
# by design — we're a safety net, not the source of truth. The CLI
|
|
109
|
+
# does the precise per-rule evaluation when reachable.
|
|
110
|
+
case "$CLI_MISSING_CMD" in
|
|
111
|
+
*"git "*) CLI_MISSING_RELEVANT=1 ;;
|
|
112
|
+
*"git "*) CLI_MISSING_RELEVANT=1 ;; # tab after git
|
|
113
|
+
*"rm "*|*"rm "*) CLI_MISSING_RELEVANT=1 ;;
|
|
114
|
+
*"psql"*|*"pgcli"*) CLI_MISSING_RELEVANT=1 ;;
|
|
115
|
+
*"DROP "*|*"DROP "*) CLI_MISSING_RELEVANT=1 ;;
|
|
116
|
+
*"kill "*|*"kill "*|*"killall"*) CLI_MISSING_RELEVANT=1 ;;
|
|
117
|
+
*"HUSKY="*) CLI_MISSING_RELEVANT=1 ;;
|
|
118
|
+
*"curl"*|*"wget"*) CLI_MISSING_RELEVANT=1 ;;
|
|
119
|
+
*"REA_BYPASS"*) CLI_MISSING_RELEVANT=1 ;;
|
|
120
|
+
*"alias "*|*"function "*) CLI_MISSING_RELEVANT=1 ;;
|
|
121
|
+
*"core.hooksPath"*|*"core.hookspath"*) CLI_MISSING_RELEVANT=1 ;;
|
|
122
|
+
*"npm "*|*"pnpm "*|*"yarn "*) CLI_MISSING_RELEVANT=1 ;;
|
|
123
|
+
*"--no-verify"*|*"--force"*) CLI_MISSING_RELEVANT=1 ;;
|
|
124
|
+
esac
|
|
125
|
+
if [ "$CLI_MISSING_RELEVANT" -eq 0 ]; then
|
|
126
|
+
# No destructive-keyword in the extracted command. The pre-0.34.0
|
|
127
|
+
# bash body would have allowed this — exit 0 to preserve install-
|
|
128
|
+
# path / unbuilt-checkout workflows.
|
|
129
|
+
exit 0
|
|
130
|
+
fi
|
|
131
|
+
# Keyword matched. Preserve fail-closed posture — the pre-0.34.0
|
|
132
|
+
# bash body would have evaluated this command and potentially refused.
|
|
133
|
+
printf 'rea: dangerous-bash-interceptor cannot run — the rea CLI is not built.\n' >&2
|
|
134
|
+
printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
|
|
135
|
+
printf 'This shim fails closed because the pre-0.34.0 bash body enforced destructive-command refusal without a CLI.\n' >&2
|
|
136
|
+
exit 2
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# 4. Realpath sandbox check.
|
|
140
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
141
|
+
printf 'rea: dangerous-bash-interceptor cannot run — `node` is not on PATH.\n' >&2
|
|
142
|
+
printf 'Install Node 22+ (engines.node) to restore destructive-command refusal.\n' >&2
|
|
143
|
+
exit 2
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
sandbox_check=$(node -e '
|
|
147
|
+
const fs = require("fs");
|
|
148
|
+
const path = require("path");
|
|
149
|
+
const cli = process.argv[1];
|
|
150
|
+
const projDir = process.argv[2];
|
|
151
|
+
let real, realProj;
|
|
152
|
+
try { real = fs.realpathSync(cli); } catch (e) {
|
|
153
|
+
process.stdout.write("bad:realpath"); process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
156
|
+
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
const sep = path.sep;
|
|
159
|
+
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
160
|
+
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
161
|
+
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
164
|
+
let found = false;
|
|
165
|
+
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
166
|
+
const pj = path.join(cur, "package.json");
|
|
167
|
+
if (fs.existsSync(pj)) {
|
|
168
|
+
try {
|
|
169
|
+
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
170
|
+
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
171
|
+
} catch (e) { /* keep walking */ }
|
|
172
|
+
}
|
|
173
|
+
cur = path.dirname(cur);
|
|
174
|
+
}
|
|
175
|
+
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
176
|
+
process.stdout.write("ok");
|
|
177
|
+
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
178
|
+
|
|
179
|
+
if [ "$sandbox_check" != "ok" ]; then
|
|
180
|
+
printf 'rea: dangerous-bash-interceptor FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
|
|
181
|
+
exit 2
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
# 5. Version-probe.
|
|
185
|
+
probe_out=$("${REA_ARGV[@]}" hook dangerous-bash-interceptor --help 2>&1)
|
|
186
|
+
probe_status=$?
|
|
187
|
+
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'dangerous-bash-interceptor'; then
|
|
188
|
+
printf 'rea: this shim requires the `rea hook dangerous-bash-interceptor` subcommand (introduced in 0.34.0).\n' >&2
|
|
189
|
+
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
190
|
+
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
|
|
191
|
+
exit 2
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
# 6. Forward stdin (already captured up-front).
|
|
195
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook dangerous-bash-interceptor
|
|
196
|
+
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 $?
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: env-file-protection.sh
|
|
3
|
+
# 0.33.0+ — Node-binary shim for `rea hook env-file-protection`.
|
|
4
|
+
#
|
|
5
|
+
# Pre-0.33.0 the gate's full body lived here as bash (124 LOC, the
|
|
6
|
+
# segment splitter + `source`/`cp` anchor patterns + utility-vs-.env
|
|
7
|
+
# co-occurrence check). The migration to the parser-backed Node binary
|
|
8
|
+
# moves all of that into `src/hooks/env-file-protection/index.ts`. This
|
|
9
|
+
# shim 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
|
+
# pass-through / no-match, exit 2 on HALT / .env access detected /
|
|
14
|
+
# malformed payload (fail-closed).
|
|
15
|
+
#
|
|
16
|
+
# # CLI-resolution trust boundary
|
|
17
|
+
#
|
|
18
|
+
# Mirrors the 0.32.0 final shim shape (round-8 of the codex iteration
|
|
19
|
+
# on the three Phase 1 pilots). The resolved CLI MUST live INSIDE
|
|
20
|
+
# realpath(CLAUDE_PROJECT_DIR) AND have an ancestor `package.json`
|
|
21
|
+
# whose `name` is `@bookedsolid/rea`. Defends against symlink-out and
|
|
22
|
+
# tarball-replacement attacks on the resolved CLI.
|
|
23
|
+
#
|
|
24
|
+
# # Fail-closed posture
|
|
25
|
+
#
|
|
26
|
+
# env-file-protection is a BLOCKING-tier gate — the pre-0.33.0 bash
|
|
27
|
+
# body refused on .env access without a compiled CLI. The early-exit
|
|
28
|
+
# branches (CLI missing, node missing, sandbox failed, version skew)
|
|
29
|
+
# fail closed AFTER the relevance pre-gate passes. Irrelevant Bash
|
|
30
|
+
# calls exit 0 regardless of CLI state.
|
|
31
|
+
|
|
32
|
+
set -uo pipefail
|
|
33
|
+
|
|
34
|
+
# 1. HALT check.
|
|
35
|
+
# shellcheck source=_lib/halt-check.sh
|
|
36
|
+
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
37
|
+
check_halt
|
|
38
|
+
REA_ROOT=$(rea_root)
|
|
39
|
+
|
|
40
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
41
|
+
|
|
42
|
+
# 2. Relevance pre-gate. Capture stdin + check for `.env` substring
|
|
43
|
+
# BEFORE any CLI/sandbox/probe work so unrelated Bash calls
|
|
44
|
+
# (`ls`, `pnpm test`, `git status`, …) exit 0 even when the CLI
|
|
45
|
+
# is missing/stale/sandboxed-out.
|
|
46
|
+
#
|
|
47
|
+
# 2026-05-15 codex round-2 P2 fix: the substring scan MUST run
|
|
48
|
+
# against `tool_input.command` ONLY, not the raw JSON payload —
|
|
49
|
+
# otherwise a benign `git commit -m "stop reading .env"` (where
|
|
50
|
+
# `.env` appears inside the commit message ARG, NOT as a file
|
|
51
|
+
# target) would hit the fail-closed branch on a fresh checkout
|
|
52
|
+
# where the CLI is unbuilt. Pre-fix the raw scan saw the substring
|
|
53
|
+
# inside the payload's "command" string-quoted body and refused.
|
|
54
|
+
#
|
|
55
|
+
# Strategy: extract `tool_input.command` via `jq` (already required
|
|
56
|
+
# by 5 other hooks; trust assumption is consistent). When `jq` is
|
|
57
|
+
# not installed, fall back to scanning the raw payload — the cost
|
|
58
|
+
# is the same over-trigger the bash original had, NOT a new
|
|
59
|
+
# regression. When `jq` IS installed (the common case), the
|
|
60
|
+
# pre-gate is field-scoped.
|
|
61
|
+
INPUT=$(cat)
|
|
62
|
+
RELEVANT=0
|
|
63
|
+
PROBE=""
|
|
64
|
+
if command -v jq >/dev/null 2>&1; then
|
|
65
|
+
PROBE=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || true)
|
|
66
|
+
if printf '%s' "$PROBE" | grep -qE '\.env'; then
|
|
67
|
+
RELEVANT=1
|
|
68
|
+
fi
|
|
69
|
+
else
|
|
70
|
+
# jq-less fallback — match the pre-0.33.0 over-trigger posture.
|
|
71
|
+
if printf '%s' "$INPUT" | grep -qE '\.env'; then
|
|
72
|
+
RELEVANT=1
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
75
|
+
if [ "$RELEVANT" -eq 0 ]; then
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# 3. Resolve the rea CLI through the fixed 2-tier sandboxed order.
|
|
80
|
+
REA_ARGV=()
|
|
81
|
+
RESOLVED_CLI_PATH=""
|
|
82
|
+
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
83
|
+
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
84
|
+
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
85
|
+
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
86
|
+
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
87
|
+
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
91
|
+
# Blocking-tier: fail closed. The pre-0.33.0 bash body enforced
|
|
92
|
+
# .env protection without a CLI. Refuse and tell the operator how
|
|
93
|
+
# to restore protection.
|
|
94
|
+
printf 'rea: env-file-protection cannot run — the rea CLI is not built.\n' >&2
|
|
95
|
+
printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
|
|
96
|
+
printf 'This shim fails closed because the pre-0.33.0 bash body enforced .env protection without a CLI.\n' >&2
|
|
97
|
+
exit 2
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# 4. Realpath sandbox check.
|
|
101
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
102
|
+
printf 'rea: env-file-protection cannot run — `node` is not on PATH.\n' >&2
|
|
103
|
+
printf 'Install Node 22+ (engines.node) to restore .env protection.\n' >&2
|
|
104
|
+
exit 2
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
sandbox_check=$(node -e '
|
|
108
|
+
const fs = require("fs");
|
|
109
|
+
const path = require("path");
|
|
110
|
+
const cli = process.argv[1];
|
|
111
|
+
const projDir = process.argv[2];
|
|
112
|
+
let real, realProj;
|
|
113
|
+
try { real = fs.realpathSync(cli); } catch (e) {
|
|
114
|
+
process.stdout.write("bad:realpath"); process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
117
|
+
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
const sep = path.sep;
|
|
120
|
+
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
121
|
+
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
122
|
+
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
125
|
+
let found = false;
|
|
126
|
+
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
127
|
+
const pj = path.join(cur, "package.json");
|
|
128
|
+
if (fs.existsSync(pj)) {
|
|
129
|
+
try {
|
|
130
|
+
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
131
|
+
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
132
|
+
} catch (e) { /* keep walking */ }
|
|
133
|
+
}
|
|
134
|
+
cur = path.dirname(cur);
|
|
135
|
+
}
|
|
136
|
+
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
137
|
+
process.stdout.write("ok");
|
|
138
|
+
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
139
|
+
|
|
140
|
+
if [ "$sandbox_check" != "ok" ]; then
|
|
141
|
+
printf 'rea: env-file-protection FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
|
|
142
|
+
exit 2
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# 5. Version-probe.
|
|
146
|
+
probe_out=$("${REA_ARGV[@]}" hook env-file-protection --help 2>&1)
|
|
147
|
+
probe_status=$?
|
|
148
|
+
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'env-file-protection'; then
|
|
149
|
+
printf 'rea: this shim requires the `rea hook env-file-protection` subcommand (introduced in 0.33.0).\n' >&2
|
|
150
|
+
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
151
|
+
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
|
|
152
|
+
exit 2
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
# 6. Forward stdin (already captured up-front for the relevance gate).
|
|
156
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook env-file-protection
|
|
157
|
+
exit $?
|