@bookedsolid/rea 0.37.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 (35) hide show
  1. package/hooks/_lib/shim-runtime.sh +405 -0
  2. package/hooks/architecture-review-gate.sh +11 -103
  3. package/hooks/attribution-advisory.sh +38 -209
  4. package/hooks/blocked-paths-bash-gate.sh +32 -146
  5. package/hooks/blocked-paths-enforcer.sh +32 -137
  6. package/hooks/changeset-security-gate.sh +26 -119
  7. package/hooks/dangerous-bash-interceptor.sh +46 -170
  8. package/hooks/delegation-advisory.sh +26 -144
  9. package/hooks/delegation-capture.sh +33 -139
  10. package/hooks/dependency-audit-gate.sh +29 -121
  11. package/hooks/env-file-protection.sh +30 -141
  12. package/hooks/local-review-gate.sh +117 -352
  13. package/hooks/pr-issue-link-gate.sh +16 -118
  14. package/hooks/protected-paths-bash-gate.sh +53 -152
  15. package/hooks/secret-scanner.sh +90 -213
  16. package/hooks/security-disclosure-gate.sh +32 -155
  17. package/hooks/settings-protection.sh +56 -176
  18. package/package.json +1 -1
  19. package/templates/_lib_shim-runtime.dogfood-staged.sh +405 -0
  20. package/templates/architecture-review-gate.dogfood-staged.sh +11 -103
  21. package/templates/attribution-advisory.dogfood-staged.sh +38 -209
  22. package/templates/blocked-paths-bash-gate.dogfood-staged.sh +32 -146
  23. package/templates/blocked-paths-enforcer.dogfood-staged.sh +32 -137
  24. package/templates/changeset-security-gate.dogfood-staged.sh +26 -119
  25. package/templates/dangerous-bash-interceptor.dogfood-staged.sh +46 -170
  26. package/templates/delegation-advisory.dogfood-staged.sh +44 -0
  27. package/templates/delegation-capture.dogfood-staged.sh +52 -0
  28. package/templates/dependency-audit-gate.dogfood-staged.sh +29 -121
  29. package/templates/env-file-protection.dogfood-staged.sh +30 -141
  30. package/templates/local-review-gate.dogfood-staged.sh +117 -352
  31. package/templates/pr-issue-link-gate.dogfood-staged.sh +16 -118
  32. package/templates/protected-paths-bash-gate.dogfood-staged.sh +53 -152
  33. package/templates/secret-scanner.dogfood-staged.sh +90 -213
  34. package/templates/security-disclosure-gate.dogfood-staged.sh +32 -155
  35. package/templates/settings-protection.dogfood-staged.sh +56 -176
@@ -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,182 +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
56
  # 0.37.0: route protected_writes reads through the unified
85
57
  # policy-reader (Tier 1 CLI → Tier 2 python3 → Tier 3 awk
86
58
  # block-form). Pre-0.37.0 the inline awk parser missed flow-form
87
59
  # arrays (`protected_writes: [path/a, path/b]`) on CLI-missing
88
- # installs, silently allowing writes to consumer-defined protected
89
- # paths. The 4-tier ladder closes the bypass via Tier 2 whenever
90
- # python3 + PyYAML are reachable; Tier 3 preserves the pre-0.37.0
91
- # block-only posture as a no-dep fallback.
92
- if [ "$CLI_MISSING_RELEVANT" -eq 0 ]; then
93
- POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
94
- if [ -f "$POLICY_FILE" ]; then
95
- # shellcheck source=_lib/policy-reader.sh
96
- source "$(dirname "$0")/_lib/policy-reader.sh"
97
- while IFS= read -r entry; do
98
- [ -z "$entry" ] && continue
99
- base="$entry"
100
- case "$base" in
101
- */) base="${base%/}" ;;
102
- esac
103
- [ -z "$base" ] && continue
104
- case "$CLI_MISSING_CMD" in
105
- *"$base"*) CLI_MISSING_RELEVANT=1; break ;;
106
- esac
107
- done < <(policy_reader_get_list protected_writes 2>/dev/null)
108
- fi
109
- fi
110
- if [ "$CLI_MISSING_RELEVANT" -eq 0 ]; then
111
- exit 0
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)
112
77
  fi
113
- printf 'rea: protected-paths-bash-gate cannot run — the rea CLI is not built.\n' >&2
114
- printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
115
- printf 'This shim fails closed because the pre-0.35.0 bash body enforced protected-path refusal without a CLI.\n' >&2
116
- exit 2
117
- fi
118
-
119
- # 4. Realpath sandbox check.
120
- if ! command -v node >/dev/null 2>&1; then
121
- printf 'rea: protected-paths-bash-gate cannot run — `node` is not on PATH.\n' >&2
122
- printf 'Install Node 22+ (engines.node) to restore protected-path refusal.\n' >&2
123
- exit 2
124
- fi
125
-
126
- sandbox_check=$(node -e '
127
- const fs = require("fs");
128
- const path = require("path");
129
- const cli = process.argv[1];
130
- const projDir = process.argv[2];
131
- let real, realProj;
132
- try { real = fs.realpathSync(cli); } catch (e) {
133
- process.stdout.write("bad:realpath"); process.exit(1);
134
- }
135
- try { realProj = fs.realpathSync(projDir); } catch (e) {
136
- process.stdout.write("bad:realpath-proj"); process.exit(1);
137
- }
138
- const sep = path.sep;
139
- const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
140
- if (!(real === realProj || real.startsWith(projWithSep))) {
141
- process.stdout.write("bad:cli-escapes-project"); process.exit(1);
142
- }
143
- // Codex round-1 P1 fix: enforce dist/cli/index.js shape (see
144
- // settings-protection.sh).
145
- const expectedEnd = path.join("dist", "cli", "index.js");
146
- if (!real.endsWith(path.sep + expectedEnd) && real !== "/" + expectedEnd) {
147
- process.stdout.write("bad:cli-shape"); process.exit(1);
148
- }
149
- let cur = path.dirname(path.dirname(path.dirname(real)));
150
- let found = false;
151
- for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
152
- const pj = path.join(cur, "package.json");
153
- if (fs.existsSync(pj)) {
154
- try {
155
- const data = JSON.parse(fs.readFileSync(pj, "utf8"));
156
- if (data && data.name === "@bookedsolid/rea") { found = true; break; }
157
- } catch (e) { /* keep walking */ }
158
- }
159
- cur = path.dirname(cur);
160
- }
161
- if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
162
- process.stdout.write("ok");
163
- ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
164
-
165
- if [ "$sandbox_check" != "ok" ]; then
166
- printf 'rea: protected-paths-bash-gate FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
167
- exit 2
168
- fi
169
-
170
- # 5. Version-probe.
171
- probe_out=$("${REA_ARGV[@]}" hook protected-paths-bash-gate --help 2>&1)
172
- probe_status=$?
173
- if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'protected-paths-bash-gate'; then
174
- printf 'rea: this shim requires the `rea hook protected-paths-bash-gate` subcommand (introduced in 0.35.0).\n' >&2
175
- printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
176
- printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
177
- exit 2
178
- fi
78
+ return 1
79
+ }
179
80
 
180
- # 6. Forward stdin (already captured up-front).
181
- printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook protected-paths-bash-gate
182
- exit $?
81
+ # shellcheck source=_lib/shim-runtime.sh
82
+ source "$(dirname "$0")/_lib/shim-runtime.sh"
83
+ shim_run