@bookedsolid/rea 0.34.0 → 0.35.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bookedsolid/rea",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "description": "Agentic governance layer for Claude Code — policy enforcement, hook-based safety gates, audit logging, and Codex-integrated adversarial review for AI-assisted projects",
5
5
  "license": "MIT",
6
6
  "author": "Booked Solid Technology <oss@bookedsolid.tech> (https://bookedsolid.tech)",
@@ -0,0 +1,177 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: blocked-paths-bash-gate.sh
3
+ # 0.35.0+ — Node-binary shim for `rea hook blocked-paths-bash-gate`.
4
+ #
5
+ # Pre-0.35.0 this was a thin bash shim over `rea hook scan-bash --mode
6
+ # blocked` (the parser-backed AST walker that closes 9 bypass classes
7
+ # from helix-023 + discord-ops Round 13 — see `src/hooks/bash-scanner/`).
8
+ # The full bash body is preserved at
9
+ # `__tests__/hooks/parity/baselines/blocked-paths-bash-gate.sh.pre-0.35.0`.
10
+ #
11
+ # This shim now resolves the CLI through the same 2-tier sandboxed
12
+ # resolver as the 0.32.0+ pilots and calls `rea hook blocked-paths-
13
+ # bash-gate` directly — eliminating the shim → CLI → scanner-module
14
+ # subprocess hop entirely.
15
+ #
16
+ # Behavioral contract is preserved byte-for-byte: exit 0 on allow,
17
+ # exit 2 on HALT / verdict block / malformed payload / sandbox fail.
18
+ #
19
+ # # CLI-resolution trust boundary
20
+ #
21
+ # Mirrors the 0.32.0 final shim shape. The resolved CLI MUST live
22
+ # INSIDE realpath(CLAUDE_PROJECT_DIR) AND have an ancestor
23
+ # `package.json` whose `name` is `@bookedsolid/rea`. Defends against
24
+ # symlink-out and tarball-replacement attacks on the resolved CLI.
25
+ #
26
+ # # Fail-closed posture
27
+ #
28
+ # blocked-paths-bash-gate is a Tier-1 security gate (PreToolUse Bash).
29
+ # The pre-0.35.0 bash body refused on uncertainty for every failure
30
+ # class. Early-exit branches (CLI missing, node missing, sandbox failed,
31
+ # version skew) fail closed AFTER the relevance pre-gate passes.
32
+ # Irrelevant Bash calls exit 0 regardless of CLI state.
33
+ #
34
+ # # Relevance pre-gate
35
+ #
36
+ # Same posture as 0.34.0 dangerous-bash + secret-scanner. When the CLI
37
+ # is missing, refuse only when the extracted command MENTIONS a path
38
+ # from `policy.blocked_paths`. Empty policy → no enforcement, exit 0.
39
+ # This unblocks the install path itself: `npx rea init`, pre-`pnpm build`
40
+ # checkouts can still run benign Bash like `ls`/`mkdir`/`pnpm install`.
41
+
42
+ set -uo pipefail
43
+
44
+ # 1. HALT check.
45
+ # shellcheck source=_lib/halt-check.sh
46
+ source "$(dirname "$0")/_lib/halt-check.sh"
47
+ check_halt
48
+ REA_ROOT=$(rea_root)
49
+
50
+ proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
51
+
52
+ # 2. Capture stdin once.
53
+ INPUT=$(cat)
54
+
55
+ # 3. Resolve the rea CLI through the fixed 2-tier sandboxed order.
56
+ REA_ARGV=()
57
+ RESOLVED_CLI_PATH=""
58
+ if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
59
+ REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
60
+ RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
61
+ elif [ -f "$proj/dist/cli/index.js" ]; then
62
+ REA_ARGV=(node "$proj/dist/cli/index.js")
63
+ RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
64
+ fi
65
+
66
+ # 3b. Relevance pre-gate. Only used when the CLI is missing.
67
+ if [ "${#REA_ARGV[@]}" -eq 0 ]; then
68
+ CLI_MISSING_CMD=""
69
+ if command -v jq >/dev/null 2>&1; then
70
+ CLI_MISSING_CMD=$(printf '%s' "$INPUT" | jq -r '
71
+ (.tool_input.command // "") | tostring
72
+ ' 2>/dev/null || true)
73
+ else
74
+ CLI_MISSING_CMD="$INPUT"
75
+ fi
76
+ if [ -z "$CLI_MISSING_CMD" ]; then
77
+ # Empty/non-Bash payload → pre-0.35.0 body would have exited 0.
78
+ exit 0
79
+ fi
80
+ # Empty policy.blocked_paths → no enforcement, exit 0.
81
+ POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
82
+ if [ ! -f "$POLICY_FILE" ]; then
83
+ exit 0
84
+ fi
85
+ # Substring scan: does the command mention any blocked_paths entry?
86
+ # Coarse — over-trigger is fine, under-trigger is the bypass we MUST
87
+ # avoid. Strip YAML quotes/comments via a minimal awk filter.
88
+ CLI_MISSING_RELEVANT=0
89
+ while IFS= read -r entry; do
90
+ [ -z "$entry" ] && continue
91
+ case "$CLI_MISSING_CMD" in
92
+ *"$entry"*) CLI_MISSING_RELEVANT=1; break ;;
93
+ esac
94
+ done < <(awk '
95
+ /^blocked_paths:/ { in_block=1; next }
96
+ in_block && /^[[:space:]]*-/ {
97
+ sub(/^[[:space:]]*-[[:space:]]*/, "")
98
+ gsub(/^["'\'']/, "")
99
+ gsub(/["'\'']$/, "")
100
+ print
101
+ next
102
+ }
103
+ in_block && /^[^[:space:]-]/ { in_block=0 }
104
+ ' "$POLICY_FILE" 2>/dev/null)
105
+ if [ "$CLI_MISSING_RELEVANT" -eq 0 ]; then
106
+ exit 0
107
+ fi
108
+ printf 'rea: blocked-paths-bash-gate cannot run — the rea CLI is not built.\n' >&2
109
+ printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
110
+ printf 'This shim fails closed because the pre-0.35.0 bash body enforced blocked_paths refusal without a CLI.\n' >&2
111
+ exit 2
112
+ fi
113
+
114
+ # 4. Realpath sandbox check.
115
+ if ! command -v node >/dev/null 2>&1; then
116
+ printf 'rea: blocked-paths-bash-gate cannot run — `node` is not on PATH.\n' >&2
117
+ printf 'Install Node 22+ (engines.node) to restore blocked_paths refusal.\n' >&2
118
+ exit 2
119
+ fi
120
+
121
+ sandbox_check=$(node -e '
122
+ const fs = require("fs");
123
+ const path = require("path");
124
+ const cli = process.argv[1];
125
+ const projDir = process.argv[2];
126
+ let real, realProj;
127
+ try { real = fs.realpathSync(cli); } catch (e) {
128
+ process.stdout.write("bad:realpath"); process.exit(1);
129
+ }
130
+ try { realProj = fs.realpathSync(projDir); } catch (e) {
131
+ process.stdout.write("bad:realpath-proj"); process.exit(1);
132
+ }
133
+ const sep = path.sep;
134
+ const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
135
+ if (!(real === realProj || real.startsWith(projWithSep))) {
136
+ process.stdout.write("bad:cli-escapes-project"); process.exit(1);
137
+ }
138
+ // Codex round-1 P1 fix: enforce dist/cli/index.js shape (see
139
+ // settings-protection.sh).
140
+ const expectedEnd = path.join("dist", "cli", "index.js");
141
+ if (!real.endsWith(path.sep + expectedEnd) && real !== "/" + expectedEnd) {
142
+ process.stdout.write("bad:cli-shape"); process.exit(1);
143
+ }
144
+ let cur = path.dirname(path.dirname(path.dirname(real)));
145
+ let found = false;
146
+ for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
147
+ const pj = path.join(cur, "package.json");
148
+ if (fs.existsSync(pj)) {
149
+ try {
150
+ const data = JSON.parse(fs.readFileSync(pj, "utf8"));
151
+ if (data && data.name === "@bookedsolid/rea") { found = true; break; }
152
+ } catch (e) { /* keep walking */ }
153
+ }
154
+ cur = path.dirname(cur);
155
+ }
156
+ if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
157
+ process.stdout.write("ok");
158
+ ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
159
+
160
+ if [ "$sandbox_check" != "ok" ]; then
161
+ printf 'rea: blocked-paths-bash-gate FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
162
+ exit 2
163
+ fi
164
+
165
+ # 5. Version-probe.
166
+ probe_out=$("${REA_ARGV[@]}" hook blocked-paths-bash-gate --help 2>&1)
167
+ probe_status=$?
168
+ if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'blocked-paths-bash-gate'; then
169
+ printf 'rea: this shim requires the `rea hook blocked-paths-bash-gate` subcommand (introduced in 0.35.0).\n' >&2
170
+ printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
171
+ printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
172
+ exit 2
173
+ fi
174
+
175
+ # 6. Forward stdin (already captured up-front).
176
+ printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook blocked-paths-bash-gate
177
+ exit $?
@@ -0,0 +1,180 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: blocked-paths-enforcer.sh
3
+ # 0.35.0+ — Node-binary shim for `rea hook blocked-paths-enforcer`.
4
+ #
5
+ # Pre-0.35.0 the gate's full body lived here as bash (284 LOC). The
6
+ # full bash body is preserved at
7
+ # `__tests__/hooks/parity/baselines/blocked-paths-enforcer.sh.pre-0.35.0`.
8
+ #
9
+ # Migration moves the enforcement logic (path normalization, traversal
10
+ # reject, glob/prefix/exact matching, symlink resolution, agent-
11
+ # writable allow-list) into `src/hooks/blocked-paths-enforcer/index.ts`.
12
+ # This shim is the Claude Code dispatcher's view of the hook — it
13
+ # forwards stdin to the CLI and exits with whatever the CLI returns.
14
+ #
15
+ # Behavioral contract is preserved byte-for-byte: exit 0 on allow,
16
+ # exit 2 on HALT / blocked-paths match / malformed payload.
17
+ #
18
+ # # CLI-resolution trust boundary
19
+ #
20
+ # Mirrors the 0.32.0 final shim shape.
21
+ #
22
+ # # Fail-closed posture
23
+ #
24
+ # blocked-paths-enforcer is a Write/Edit/MultiEdit/NotebookEdit tier
25
+ # security gate. The pre-0.35.0 bash body refused on uncertainty.
26
+ # Early-exit branches fail closed AFTER the relevance pre-gate passes.
27
+ #
28
+ # # Relevance pre-gate
29
+ #
30
+ # Extract file_path / notebook_path from the payload, substring-scan
31
+ # against the policy's blocked_paths entries. When CLI is missing AND
32
+ # no policy.blocked_paths entry matches, exit 0. Empty/missing policy
33
+ # → no enforcement, exit 0.
34
+
35
+ set -uo pipefail
36
+
37
+ # 1. HALT check.
38
+ # shellcheck source=_lib/halt-check.sh
39
+ source "$(dirname "$0")/_lib/halt-check.sh"
40
+ check_halt
41
+ REA_ROOT=$(rea_root)
42
+
43
+ proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
44
+
45
+ # 2. Capture stdin once.
46
+ INPUT=$(cat)
47
+
48
+ # 3. Resolve the rea CLI through the fixed 2-tier sandboxed order.
49
+ REA_ARGV=()
50
+ RESOLVED_CLI_PATH=""
51
+ if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
52
+ REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
53
+ RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
54
+ elif [ -f "$proj/dist/cli/index.js" ]; then
55
+ REA_ARGV=(node "$proj/dist/cli/index.js")
56
+ RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
57
+ fi
58
+
59
+ # 3b. Relevance pre-gate. Only used when the CLI is missing.
60
+ if [ "${#REA_ARGV[@]}" -eq 0 ]; then
61
+ CLI_MISSING_FILE_PATH=""
62
+ if command -v jq >/dev/null 2>&1; then
63
+ CLI_MISSING_FILE_PATH=$(printf '%s' "$INPUT" | jq -r '
64
+ (.tool_input.file_path // .tool_input.notebook_path // "") | tostring
65
+ ' 2>/dev/null || true)
66
+ else
67
+ CLI_MISSING_FILE_PATH="$INPUT"
68
+ fi
69
+ if [ -z "$CLI_MISSING_FILE_PATH" ]; then
70
+ exit 0
71
+ fi
72
+ POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
73
+ if [ ! -f "$POLICY_FILE" ]; then
74
+ exit 0
75
+ fi
76
+ CLI_MISSING_RELEVANT=0
77
+ while IFS= read -r entry; do
78
+ [ -z "$entry" ] && continue
79
+ # Substring scan — for directory prefixes the entry ends with /
80
+ # and any file_path under it matches. Glob entries fall back to
81
+ # the same substring test (over-trigger is fine — the CLI does
82
+ # the precise evaluation when reachable).
83
+ base="$entry"
84
+ case "$base" in
85
+ */) base="${base%/}" ;;
86
+ esac
87
+ # Strip glob wildcards for substring testing — `src/*.ts` becomes
88
+ # `src/` + `.ts`. The simplest safe form is to scan the literal
89
+ # part before the first `*`.
90
+ case "$base" in
91
+ *'*'*) base="${base%%\**}" ;;
92
+ esac
93
+ [ -z "$base" ] && continue
94
+ case "$CLI_MISSING_FILE_PATH" in
95
+ *"$base"*) CLI_MISSING_RELEVANT=1; break ;;
96
+ esac
97
+ done < <(awk '
98
+ /^blocked_paths:/ { in_block=1; next }
99
+ in_block && /^[[:space:]]*-/ {
100
+ sub(/^[[:space:]]*-[[:space:]]*/, "")
101
+ gsub(/^["'\'']/, "")
102
+ gsub(/["'\'']$/, "")
103
+ print
104
+ next
105
+ }
106
+ in_block && /^[^[:space:]-]/ { in_block=0 }
107
+ ' "$POLICY_FILE" 2>/dev/null)
108
+ if [ "$CLI_MISSING_RELEVANT" -eq 0 ]; then
109
+ exit 0
110
+ fi
111
+ printf 'rea: blocked-paths-enforcer cannot run — the rea CLI is not built.\n' >&2
112
+ printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
113
+ printf 'This shim fails closed because the pre-0.35.0 bash body enforced blocked_paths refusal without a CLI.\n' >&2
114
+ exit 2
115
+ fi
116
+
117
+ # 4. Realpath sandbox check.
118
+ if ! command -v node >/dev/null 2>&1; then
119
+ printf 'rea: blocked-paths-enforcer cannot run — `node` is not on PATH.\n' >&2
120
+ printf 'Install Node 22+ (engines.node) to restore blocked_paths refusal.\n' >&2
121
+ exit 2
122
+ fi
123
+
124
+ sandbox_check=$(node -e '
125
+ const fs = require("fs");
126
+ const path = require("path");
127
+ const cli = process.argv[1];
128
+ const projDir = process.argv[2];
129
+ let real, realProj;
130
+ try { real = fs.realpathSync(cli); } catch (e) {
131
+ process.stdout.write("bad:realpath"); process.exit(1);
132
+ }
133
+ try { realProj = fs.realpathSync(projDir); } catch (e) {
134
+ process.stdout.write("bad:realpath-proj"); process.exit(1);
135
+ }
136
+ const sep = path.sep;
137
+ const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
138
+ if (!(real === realProj || real.startsWith(projWithSep))) {
139
+ process.stdout.write("bad:cli-escapes-project"); process.exit(1);
140
+ }
141
+ // Codex round-1 P1 fix: enforce dist/cli/index.js shape (see
142
+ // settings-protection.sh).
143
+ const expectedEnd = path.join("dist", "cli", "index.js");
144
+ if (!real.endsWith(path.sep + expectedEnd) && real !== "/" + expectedEnd) {
145
+ process.stdout.write("bad:cli-shape"); process.exit(1);
146
+ }
147
+ let cur = path.dirname(path.dirname(path.dirname(real)));
148
+ let found = false;
149
+ for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
150
+ const pj = path.join(cur, "package.json");
151
+ if (fs.existsSync(pj)) {
152
+ try {
153
+ const data = JSON.parse(fs.readFileSync(pj, "utf8"));
154
+ if (data && data.name === "@bookedsolid/rea") { found = true; break; }
155
+ } catch (e) { /* keep walking */ }
156
+ }
157
+ cur = path.dirname(cur);
158
+ }
159
+ if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
160
+ process.stdout.write("ok");
161
+ ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
162
+
163
+ if [ "$sandbox_check" != "ok" ]; then
164
+ printf 'rea: blocked-paths-enforcer FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
165
+ exit 2
166
+ fi
167
+
168
+ # 5. Version-probe.
169
+ probe_out=$("${REA_ARGV[@]}" hook blocked-paths-enforcer --help 2>&1)
170
+ probe_status=$?
171
+ if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'blocked-paths-enforcer'; then
172
+ printf 'rea: this shim requires the `rea hook blocked-paths-enforcer` subcommand (introduced in 0.35.0).\n' >&2
173
+ printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
174
+ printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
175
+ exit 2
176
+ fi
177
+
178
+ # 6. Forward stdin (already captured up-front).
179
+ printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook blocked-paths-enforcer
180
+ exit $?
@@ -0,0 +1,186 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: protected-paths-bash-gate.sh
3
+ # 0.35.0+ — Node-binary shim for `rea hook protected-paths-bash-gate`.
4
+ #
5
+ # Pre-0.35.0 this was a thin bash shim over `rea hook scan-bash --mode
6
+ # protected` (the parser-backed AST walker that replaces the 536-line
7
+ # pre-0.23.0 regex pipeline). The full bash body is preserved at
8
+ # `__tests__/hooks/parity/baselines/protected-paths-bash-gate.sh.pre-0.35.0`.
9
+ #
10
+ # This shim now resolves the CLI through the same 2-tier sandboxed
11
+ # resolver as the 0.32.0+ pilots and calls `rea hook protected-paths-
12
+ # bash-gate` directly — eliminating the shim → CLI → scanner-module
13
+ # subprocess hop entirely.
14
+ #
15
+ # Behavioral contract is preserved byte-for-byte: exit 0 on allow,
16
+ # exit 2 on HALT / verdict block / malformed payload / sandbox fail.
17
+ #
18
+ # # CLI-resolution trust boundary
19
+ #
20
+ # Mirrors the 0.32.0 final shim shape.
21
+ #
22
+ # # Fail-closed posture
23
+ #
24
+ # protected-paths-bash-gate is a Tier-1 security gate. The pre-0.35.0
25
+ # bash body refused on uncertainty. Early-exit branches fail closed
26
+ # AFTER the relevance pre-gate passes. Irrelevant Bash calls exit 0
27
+ # regardless of CLI state.
28
+ #
29
+ # # Relevance pre-gate
30
+ #
31
+ # Substring scan over the extracted command for any of the protected-
32
+ # path markers: .claude/, .husky/, .rea/policy.yaml, .rea/HALT, the
33
+ # verdict cache paths. When the CLI is missing AND none of these
34
+ # substrings appear, exit 0 (the pre-0.35.0 bash body would have
35
+ # allowed). When the CLI is missing AND a marker DOES match, preserve
36
+ # fail-closed.
37
+
38
+ set -uo pipefail
39
+
40
+ # 1. HALT check.
41
+ # shellcheck source=_lib/halt-check.sh
42
+ source "$(dirname "$0")/_lib/halt-check.sh"
43
+ check_halt
44
+ REA_ROOT=$(rea_root)
45
+
46
+ proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
47
+
48
+ # 2. Capture stdin once.
49
+ INPUT=$(cat)
50
+
51
+ # 3. Resolve the rea CLI through the fixed 2-tier sandboxed order.
52
+ REA_ARGV=()
53
+ RESOLVED_CLI_PATH=""
54
+ if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
55
+ REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
56
+ RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
57
+ elif [ -f "$proj/dist/cli/index.js" ]; then
58
+ REA_ARGV=(node "$proj/dist/cli/index.js")
59
+ RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
60
+ fi
61
+
62
+ # 3b. Relevance pre-gate. Only used when the CLI is missing.
63
+ if [ "${#REA_ARGV[@]}" -eq 0 ]; then
64
+ CLI_MISSING_CMD=""
65
+ if command -v jq >/dev/null 2>&1; then
66
+ CLI_MISSING_CMD=$(printf '%s' "$INPUT" | jq -r '
67
+ (.tool_input.command // "") | tostring
68
+ ' 2>/dev/null || true)
69
+ else
70
+ CLI_MISSING_CMD="$INPUT"
71
+ fi
72
+ if [ -z "$CLI_MISSING_CMD" ]; then
73
+ exit 0
74
+ fi
75
+ CLI_MISSING_RELEVANT=0
76
+ case "$CLI_MISSING_CMD" in
77
+ *".claude/"*) CLI_MISSING_RELEVANT=1 ;;
78
+ *".husky/"*) CLI_MISSING_RELEVANT=1 ;;
79
+ *".rea/policy.yaml"*) CLI_MISSING_RELEVANT=1 ;;
80
+ *".rea/HALT"*) CLI_MISSING_RELEVANT=1 ;;
81
+ *".rea/last-review"*) CLI_MISSING_RELEVANT=1 ;;
82
+ *".claude\\"*|*".husky\\"*|*".rea\\"*) CLI_MISSING_RELEVANT=1 ;;
83
+ esac
84
+ # Codex round-1 P2 fix: scan policy.protected_writes entries too so a
85
+ # consumer-defined protected path isn't silently allowed when the CLI
86
+ # is missing. Read the policy via the same awk parser the consumer-
87
+ # facing relevance pre-gates use for blocked_paths.
88
+ if [ "$CLI_MISSING_RELEVANT" -eq 0 ]; then
89
+ POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
90
+ if [ -f "$POLICY_FILE" ]; then
91
+ while IFS= read -r entry; do
92
+ [ -z "$entry" ] && continue
93
+ base="$entry"
94
+ case "$base" in
95
+ */) base="${base%/}" ;;
96
+ esac
97
+ [ -z "$base" ] && continue
98
+ case "$CLI_MISSING_CMD" in
99
+ *"$base"*) CLI_MISSING_RELEVANT=1; break ;;
100
+ esac
101
+ done < <(awk '
102
+ /^protected_writes:/ { in_block=1; next }
103
+ in_block && /^[[:space:]]*-/ {
104
+ sub(/^[[:space:]]*-[[:space:]]*/, "")
105
+ gsub(/^["'\'']/, "")
106
+ gsub(/["'\'']$/, "")
107
+ print
108
+ next
109
+ }
110
+ in_block && /^[^[:space:]-]/ { in_block=0 }
111
+ ' "$POLICY_FILE" 2>/dev/null)
112
+ fi
113
+ fi
114
+ if [ "$CLI_MISSING_RELEVANT" -eq 0 ]; then
115
+ exit 0
116
+ fi
117
+ printf 'rea: protected-paths-bash-gate cannot run — the rea CLI is not built.\n' >&2
118
+ printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
119
+ printf 'This shim fails closed because the pre-0.35.0 bash body enforced protected-path refusal without a CLI.\n' >&2
120
+ exit 2
121
+ fi
122
+
123
+ # 4. Realpath sandbox check.
124
+ if ! command -v node >/dev/null 2>&1; then
125
+ printf 'rea: protected-paths-bash-gate cannot run — `node` is not on PATH.\n' >&2
126
+ printf 'Install Node 22+ (engines.node) to restore protected-path refusal.\n' >&2
127
+ exit 2
128
+ fi
129
+
130
+ sandbox_check=$(node -e '
131
+ const fs = require("fs");
132
+ const path = require("path");
133
+ const cli = process.argv[1];
134
+ const projDir = process.argv[2];
135
+ let real, realProj;
136
+ try { real = fs.realpathSync(cli); } catch (e) {
137
+ process.stdout.write("bad:realpath"); process.exit(1);
138
+ }
139
+ try { realProj = fs.realpathSync(projDir); } catch (e) {
140
+ process.stdout.write("bad:realpath-proj"); process.exit(1);
141
+ }
142
+ const sep = path.sep;
143
+ const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
144
+ if (!(real === realProj || real.startsWith(projWithSep))) {
145
+ process.stdout.write("bad:cli-escapes-project"); process.exit(1);
146
+ }
147
+ // Codex round-1 P1 fix: enforce dist/cli/index.js shape (see
148
+ // settings-protection.sh).
149
+ const expectedEnd = path.join("dist", "cli", "index.js");
150
+ if (!real.endsWith(path.sep + expectedEnd) && real !== "/" + expectedEnd) {
151
+ process.stdout.write("bad:cli-shape"); process.exit(1);
152
+ }
153
+ let cur = path.dirname(path.dirname(path.dirname(real)));
154
+ let found = false;
155
+ for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
156
+ const pj = path.join(cur, "package.json");
157
+ if (fs.existsSync(pj)) {
158
+ try {
159
+ const data = JSON.parse(fs.readFileSync(pj, "utf8"));
160
+ if (data && data.name === "@bookedsolid/rea") { found = true; break; }
161
+ } catch (e) { /* keep walking */ }
162
+ }
163
+ cur = path.dirname(cur);
164
+ }
165
+ if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
166
+ process.stdout.write("ok");
167
+ ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
168
+
169
+ if [ "$sandbox_check" != "ok" ]; then
170
+ printf 'rea: protected-paths-bash-gate FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
171
+ exit 2
172
+ fi
173
+
174
+ # 5. Version-probe.
175
+ probe_out=$("${REA_ARGV[@]}" hook protected-paths-bash-gate --help 2>&1)
176
+ probe_status=$?
177
+ if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'protected-paths-bash-gate'; then
178
+ printf 'rea: this shim requires the `rea hook protected-paths-bash-gate` subcommand (introduced in 0.35.0).\n' >&2
179
+ printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
180
+ printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
181
+ exit 2
182
+ fi
183
+
184
+ # 6. Forward stdin (already captured up-front).
185
+ printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook protected-paths-bash-gate
186
+ exit $?