@bookedsolid/rea 0.36.0 → 0.38.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 (37) hide show
  1. package/hooks/_lib/policy-reader.sh +948 -0
  2. package/hooks/_lib/shim-runtime.sh +405 -0
  3. package/hooks/architecture-review-gate.sh +11 -103
  4. package/hooks/attribution-advisory.sh +43 -155
  5. package/hooks/blocked-paths-bash-gate.sh +35 -149
  6. package/hooks/blocked-paths-enforcer.sh +35 -140
  7. package/hooks/changeset-security-gate.sh +26 -119
  8. package/hooks/dangerous-bash-interceptor.sh +46 -170
  9. package/hooks/delegation-advisory.sh +26 -144
  10. package/hooks/delegation-capture.sh +33 -139
  11. package/hooks/dependency-audit-gate.sh +29 -121
  12. package/hooks/env-file-protection.sh +30 -141
  13. package/hooks/local-review-gate.sh +191 -396
  14. package/hooks/pr-issue-link-gate.sh +16 -118
  15. package/hooks/protected-paths-bash-gate.sh +57 -160
  16. package/hooks/secret-scanner.sh +90 -213
  17. package/hooks/security-disclosure-gate.sh +32 -155
  18. package/hooks/settings-protection.sh +56 -179
  19. package/package.json +1 -1
  20. package/templates/_lib_policy-reader.dogfood-staged.sh +948 -0
  21. package/templates/_lib_shim-runtime.dogfood-staged.sh +405 -0
  22. package/templates/architecture-review-gate.dogfood-staged.sh +11 -103
  23. package/templates/attribution-advisory.dogfood-staged.sh +43 -155
  24. package/templates/blocked-paths-bash-gate.dogfood-staged.sh +35 -149
  25. package/templates/blocked-paths-enforcer.dogfood-staged.sh +35 -140
  26. package/templates/changeset-security-gate.dogfood-staged.sh +26 -119
  27. package/templates/dangerous-bash-interceptor.dogfood-staged.sh +46 -170
  28. package/templates/delegation-advisory.dogfood-staged.sh +44 -0
  29. package/templates/delegation-capture.dogfood-staged.sh +52 -0
  30. package/templates/dependency-audit-gate.dogfood-staged.sh +29 -121
  31. package/templates/env-file-protection.dogfood-staged.sh +30 -141
  32. package/templates/local-review-gate.dogfood-staged.sh +191 -396
  33. package/templates/pr-issue-link-gate.dogfood-staged.sh +16 -118
  34. package/templates/protected-paths-bash-gate.dogfood-staged.sh +57 -160
  35. package/templates/secret-scanner.dogfood-staged.sh +90 -213
  36. package/templates/security-disclosure-gate.dogfood-staged.sh +32 -155
  37. package/templates/settings-protection.dogfood-staged.sh +56 -179
@@ -1,134 +1,32 @@
1
1
  #!/bin/bash
2
2
  # PreToolUse hook: pr-issue-link-gate.sh
3
3
  # 0.32.0+ — Node-binary shim for `rea hook pr-issue-link-gate`.
4
+ # 0.38.0+ — migrated to `_lib/shim-runtime.sh` (shared runtime).
4
5
  #
5
- # Pre-0.32.0 the gate's full body lived here as bash; the migration to
6
- # the parser-backed Node binary moves the matching + advisory logic
7
- # into `src/hooks/pr-issue-link-gate/index.ts`. This shim is the
8
- # Claude Code dispatcher's view of the hook — it forwards stdin to the
9
- # CLI and exits with whatever the CLI returns.
10
- #
11
- # Behavioral contract is preserved byte-for-byte: ALWAYS exit 0 except
12
- # under HALT (exit 2) or a malformed payload (exit 2, fail-closed).
6
+ # Advisory-tier: nudges operators to link an issue when running
7
+ # `gh pr create`. ALWAYS exit 0 except under HALT. The pre-port bash
8
+ # body lived here; the matching + advisory logic is now in
9
+ # `src/hooks/pr-issue-link-gate/index.ts`.
13
10
  #
14
11
  # # CLI-resolution trust boundary
15
12
  #
16
- # Codex round 1 P1 (2026-05-15): mirrors the realpath sandbox check
17
- # from `delegation-advisory.sh` §3 and `protected-paths-bash-gate.sh`
18
- # §6. The resolved CLI MUST live INSIDE realpath(CLAUDE_PROJECT_DIR)
19
- # AND have an ancestor `package.json` whose `name` is
20
- # `@bookedsolid/rea`. Pre-fix the shim executed
21
- # `node_modules/@bookedsolid/rea/dist/cli/index.js` directly without
22
- # realpathing the target, which would let an attacker who controlled
23
- # `node_modules/@bookedsolid/rea` (symlink-out, postinstall script,
24
- # tarball-replacement) ship forged review code that intercepts every
25
- # Bash dispatch.
26
- #
27
- # Sandboxed resolution order (PATH is INTENTIONALLY OMITTED):
28
- # 1. node_modules/@bookedsolid/rea/dist/cli/index.js (consumer-side)
29
- # 2. dist/cli/index.js under CLAUDE_PROJECT_DIR (dogfood)
30
- #
31
- # When NO rea CLI is reachable through the sandboxed order, this hook
32
- # falls through to allow (exit 0) — the advisory is a nudge, not a
33
- # security claim. The bash-tier path gates fail-closed because they
34
- # protect write surfaces; this gate only emits prose.
35
- #
36
- # # Version skew
37
- #
38
- # Codex round 1 P1 (2026-05-15): a fresh `rea init` against a stale
39
- # `node_modules/@bookedsolid/rea` would deliver this 0.32.0 shim while
40
- # the installed CLI lacks the `hook pr-issue-link-gate` subcommand —
41
- # every Bash dispatch would then fail with `unknown command` (exit 1).
42
- # Probe the subcommand's `--help` output before propagating the exit
43
- # code; on probe failure, advise the operator to `pnpm install` and
44
- # fall through silently so the workspace stays usable.
13
+ # The shared runtime enforces the 2-tier sandboxed CLI resolution
14
+ # (node_modules dist/, PATH intentionally omitted) + the realpath
15
+ # sandbox check. See `hooks/_lib/shim-runtime.sh` for the canonical
16
+ # trust boundary documentation.
45
17
 
46
18
  set -uo pipefail
47
19
 
48
- # 1. HALT check. Even though the CLI re-checks for defense-in-depth,
49
- # short-circuit here so we never spawn `node` while frozen.
50
20
  # shellcheck source=_lib/halt-check.sh
51
21
  source "$(dirname "$0")/_lib/halt-check.sh"
52
22
  check_halt
53
23
  REA_ROOT=$(rea_root)
54
24
 
55
- proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
56
-
57
- # 2. Resolve the rea CLI through the fixed 2-tier sandboxed order.
58
- REA_ARGV=()
59
- RESOLVED_CLI_PATH=""
60
- if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
61
- REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
62
- RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
63
- elif [ -f "$proj/dist/cli/index.js" ]; then
64
- REA_ARGV=(node "$proj/dist/cli/index.js")
65
- RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
66
- fi
67
-
68
- if [ "${#REA_ARGV[@]}" -eq 0 ]; then
69
- exit 0
70
- fi
71
-
72
- # 3. Realpath sandbox check — mirrors delegation-advisory.sh §3.
73
- if ! command -v node >/dev/null 2>&1; then
74
- exit 0
75
- fi
76
-
77
- sandbox_check=$(node -e '
78
- const fs = require("fs");
79
- const path = require("path");
80
- const cli = process.argv[1];
81
- const projDir = process.argv[2];
82
- let real, realProj;
83
- try { real = fs.realpathSync(cli); } catch (e) {
84
- process.stdout.write("bad:realpath"); process.exit(1);
85
- }
86
- try { realProj = fs.realpathSync(projDir); } catch (e) {
87
- process.stdout.write("bad:realpath-proj"); process.exit(1);
88
- }
89
- const sep = path.sep;
90
- const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
91
- if (!(real === realProj || real.startsWith(projWithSep))) {
92
- process.stdout.write("bad:cli-escapes-project"); process.exit(1);
93
- }
94
- let cur = path.dirname(path.dirname(path.dirname(real)));
95
- let found = false;
96
- for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
97
- const pj = path.join(cur, "package.json");
98
- if (fs.existsSync(pj)) {
99
- try {
100
- const data = JSON.parse(fs.readFileSync(pj, "utf8"));
101
- if (data && data.name === "@bookedsolid/rea") { found = true; break; }
102
- } catch (e) { /* keep walking */ }
103
- }
104
- cur = path.dirname(cur);
105
- }
106
- if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
107
- process.stdout.write("ok");
108
- ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
109
-
110
- if [ "$sandbox_check" != "ok" ]; then
111
- printf 'rea: pr-issue-link-gate skipped (sandbox check: %s)\n' "$sandbox_check" >&2
112
- exit 0
113
- fi
114
-
115
- # 4. Version-probe: confirm the resolved CLI implements the
116
- # `hook pr-issue-link-gate` subcommand. A stale node_modules from
117
- # a fresh `rea init` against an older installed version would
118
- # otherwise turn every Bash dispatch into a hard failure.
119
- probe_out=$("${REA_ARGV[@]}" hook pr-issue-link-gate --help 2>&1)
120
- probe_status=$?
121
- if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'pr-issue-link-gate'; then
122
- printf 'rea: this shim requires the `rea hook pr-issue-link-gate` subcommand (introduced in 0.32.0).\n' >&2
123
- printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
124
- printf 'Run `pnpm install` (or `npm install`) to sync the CLI to the version this shim expects.\n' >&2
125
- exit 0
126
- fi
25
+ SHIM_NAME="pr-issue-link-gate"
26
+ SHIM_INTRODUCED_IN="0.32.0"
27
+ SHIM_FAIL_OPEN=1
28
+ SHIM_REFUSAL_NOUN="the pr-issue-link advisory"
127
29
 
128
- # 5. Forward stdin to the CLI synchronously. The advisory text must
129
- # reach the operator's stderr before this hook returns; the CLI's
130
- # own exit code is the hook's exit code (0 normally, 2 under HALT
131
- # or malformed payload).
132
- INPUT=$(cat)
133
- printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook pr-issue-link-gate
134
- exit $?
30
+ # shellcheck source=_lib/shim-runtime.sh
31
+ source "$(dirname "$0")/_lib/shim-runtime.sh"
32
+ shim_run
@@ -1,186 +1,83 @@
1
1
  #!/bin/bash
2
2
  # PreToolUse hook: protected-paths-bash-gate.sh
3
3
  # 0.35.0+ — Node-binary shim for `rea hook protected-paths-bash-gate`.
4
+ # 0.38.0+ — migrated to `_lib/shim-runtime.sh` (shared runtime).
4
5
  #
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
6
+ # Blocking-tier Bash gate. Full bash body preserved at
8
7
  # `__tests__/hooks/parity/baselines/protected-paths-bash-gate.sh.pre-0.35.0`.
8
+ # Migration lives in `src/hooks/protected-paths-bash-gate/index.ts`.
9
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.
10
+ # SHIM_ENFORCE_CLI_SHAPE=1: codex round-1 P1 from 0.35.0 enforce that
11
+ # the resolved CLI's realpath ends in dist/cli/index.js so an attacker
12
+ # who repoints node_modules/@bookedsolid/rea at an arbitrary in-project
13
+ # JS file cannot execute it as the trusted gate CLI.
14
14
  #
15
- # Behavioral contract is preserved byte-for-byte: exit 0 on allow,
16
- # exit 2 on HALT / verdict block / malformed payload / sandbox fail.
15
+ # # Relevance pre-gate (CLI-missing only)
17
16
  #
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.
17
+ # Substring scan over the extracted command for protected-path markers
18
+ # AND any policy.protected_writes entry. When the CLI is reachable, the
19
+ # Node body does the precise evaluation; the shim's relevance scan is
20
+ # only consulted on fresh/unbuilt installs to preserve the pre-port
21
+ # bash body's allow-on-no-match posture.
37
22
 
38
23
  set -uo pipefail
39
24
 
40
- # 1. HALT check.
41
25
  # shellcheck source=_lib/halt-check.sh
42
26
  source "$(dirname "$0")/_lib/halt-check.sh"
43
27
  check_halt
44
28
  REA_ROOT=$(rea_root)
45
29
 
46
- proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
47
-
48
- # 2. Capture stdin once.
49
- INPUT=$(cat)
30
+ SHIM_NAME="protected-paths-bash-gate"
31
+ SHIM_INTRODUCED_IN="0.35.0"
32
+ SHIM_FAIL_OPEN=0
33
+ SHIM_ENFORCE_CLI_SHAPE=1
34
+ SHIM_REFUSAL_NOUN="protected-path refusal"
50
35
 
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=""
36
+ shim_cli_missing_relevant() {
37
+ local cli_missing_cmd=""
65
38
  if command -v jq >/dev/null 2>&1; then
66
- CLI_MISSING_CMD=$(printf '%s' "$INPUT" | jq -r '
39
+ cli_missing_cmd=$(printf '%s' "$INPUT" | jq -r '
67
40
  (.tool_input.command // "") | tostring
68
41
  ' 2>/dev/null || true)
69
42
  else
70
- CLI_MISSING_CMD="$INPUT"
43
+ cli_missing_cmd="$INPUT"
71
44
  fi
72
- if [ -z "$CLI_MISSING_CMD" ]; then
73
- exit 0
45
+ if [ -z "$cli_missing_cmd" ]; then
46
+ return 1
74
47
  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 ;;
48
+ case "$cli_missing_cmd" in
49
+ *".claude/"*) return 0 ;;
50
+ *".husky/"*) return 0 ;;
51
+ *".rea/policy.yaml"*) return 0 ;;
52
+ *".rea/HALT"*) return 0 ;;
53
+ *".rea/last-review"*) return 0 ;;
54
+ *".claude\\"*|*".husky\\"*|*".rea\\"*) return 0 ;;
83
55
  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
56
+ # 0.37.0: route protected_writes reads through the unified
57
+ # policy-reader (Tier 1 CLI Tier 2 python3 → Tier 3 awk
58
+ # block-form). Pre-0.37.0 the inline awk parser missed flow-form
59
+ # arrays (`protected_writes: [path/a, path/b]`) on CLI-missing
60
+ # installs.
61
+ local policy_file="${REA_ROOT}/.rea/policy.yaml"
62
+ if [ -f "$policy_file" ]; then
63
+ # shellcheck source=_lib/policy-reader.sh
64
+ source "$(dirname "$0")/_lib/policy-reader.sh"
65
+ local entry base
66
+ while IFS= read -r entry; do
67
+ [ -z "$entry" ] && continue
68
+ base="$entry"
69
+ case "$base" in
70
+ */) base="${base%/}" ;;
71
+ esac
72
+ [ -z "$base" ] && continue
73
+ case "$cli_missing_cmd" in
74
+ *"$base"*) return 0 ;;
75
+ esac
76
+ done < <(policy_reader_get_list protected_writes 2>/dev/null)
116
77
  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
78
+ return 1
79
+ }
183
80
 
184
- # 6. Forward stdin (already captured up-front).
185
- printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook protected-paths-bash-gate
186
- exit $?
81
+ # shellcheck source=_lib/shim-runtime.sh
82
+ source "$(dirname "$0")/_lib/shim-runtime.sh"
83
+ shim_run