@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,162 +1,44 @@
1
1
  #!/bin/bash
2
2
  # PostToolUse hook: delegation-advisory.sh
3
3
  # 0.31.0+ — delegation-telemetry completion (the *nudge*).
4
+ # 0.38.0+ — migrated to `_lib/shim-runtime.sh` (shared runtime).
4
5
  #
5
- # Fires AFTER every write-class tool call. The settings.json matcher is
6
- # `Bash|Edit|Write|MultiEdit|NotebookEdit`. Reads the Claude Code hook
7
- # payload from stdin, pipes it to `rea hook delegation-advisory`, and
8
- # exits 0.
9
- #
10
- # 0.29.0 shipped the delegation-telemetry *observability* layer
11
- # (`delegation-capture.sh` + `rea audit specialists`). 0.31.0 closes the
12
- # loop with the *nudge*: `rea hook delegation-advisory` maintains a
13
- # per-session write-class counter and, the FIRST time that counter
14
- # crosses `policy.delegation_advisory.threshold` while the session has
15
- # recorded zero real delegation signals, prints a one-time stderr
16
- # advisory ("this session has done a lot of work without delegating to
17
- # a specialist").
18
- #
19
- # # Advisory, never gating
20
- #
21
- # This hook ALWAYS exits 0 (under normal operation). The advisory is a
22
- # nudge — it never blocks a tool call. The ONLY non-zero exit is 2
23
- # under HALT, to keep the kill-switch contract uniform with the rest of
24
- # the hook tree.
6
+ # Fires AFTER every write-class tool call. ALWAYS exits 0 except under
7
+ # HALT. The CLI maintains a per-session write-class counter; first
8
+ # crossing of `policy.delegation_advisory.threshold` with zero recorded
9
+ # delegation signals prints a one-time stderr advisory.
25
10
  #
26
11
  # # Synchronous, NOT detached
27
12
  #
28
- # Unlike `delegation-capture.sh` (which backgrounds `rea hook
29
- # delegation-signal` with `& disown` because the audit write must not
30
- # block tool dispatch), this hook runs the CLI SYNCHRONOUSLY. The
31
- # advisory text must reach the operator's stderr before the hook
32
- # returns — backgrounding it would race the hook's own exit and the
33
- # message could be lost or interleaved with the next tool call's
34
- # output. The CLI is cheap on the hot path: below the threshold it
35
- # only bumps an integer counter file and exits, no audit scan, no
36
- # roster discovery.
13
+ # Unlike delegation-capture.sh, this hook runs the CLI synchronously
14
+ # so the advisory text reaches stderr BEFORE the hook returns. The
15
+ # default `shim_default_forward` already does this no override needed.
37
16
  #
38
- # # CLI-resolution trust boundary
17
+ # # No version probe (codex round-1 P2)
39
18
  #
40
- # Same 2-tier sandboxed resolution `delegation-capture.sh`,
41
- # `protected-paths-bash-gate.sh`, and `blocked-paths-bash-gate.sh` use:
42
- # 1. node_modules/@bookedsolid/rea/dist/cli/index.js (consumer-side
43
- # published artifact)
44
- # 2. dist/cli/index.js under CLAUDE_PROJECT_DIR (the rea repo's own
45
- # dogfood install)
46
- # PATH lookup is INTENTIONALLY OMITTED agent-controlled $PATH would
47
- # let a forged `rea` binary intercept this hook on every write-class
48
- # tool call. A realpath sandbox check ensures the resolved CLI lives
49
- # INSIDE realpath(CLAUDE_PROJECT_DIR) with an ancestor package.json
50
- # declaring `@bookedsolid/rea`.
51
- #
52
- # Exit codes:
53
- # 0 — always (under normal operation). Disabled-by-policy,
54
- # below-threshold, already-fired, just-fired — all exit 0.
55
- # 2 — HALT active.
19
+ # SHIM_SKIP_VERSION_PROBE=1: this hook runs on EVERY write-class
20
+ # PostToolUse (matcher `Bash|Edit|Write|MultiEdit|NotebookEdit`), so
21
+ # the hot path is hot. The pre-port body had NO version probe — it
22
+ # went straight from sandbox check to forward. Adding a probe doubles
23
+ # Node startups on every tool call (`--help` invocation + the real
24
+ # forward), which noticeably regresses interactive latency during
25
+ # long sessions. Skip the probe; a stale CLI without the subcommand
26
+ # will still fail at forward time, which is fine for an advisory-tier
27
+ # nudge (the operator will run `pnpm install` to fix it).
56
28
 
57
29
  set -uo pipefail
58
30
 
59
- # 1. HALT check. Even though this hook is advisory, refusing to run
60
- # while frozen matches the rest of the hook tree and keeps the
61
- # kill-switch contract uniform.
62
31
  # shellcheck source=_lib/halt-check.sh
63
32
  source "$(dirname "$0")/_lib/halt-check.sh"
64
33
  check_halt
65
34
  REA_ROOT=$(rea_root)
66
35
 
67
- proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
68
-
69
- # 2. Resolve the rea CLI through the fixed 2-tier sandboxed order.
70
- # PATH lookup is omitted on purpose (see header). Other install
71
- # shapes silently drop the advisory — matching the bash-gate
72
- # posture; the nudge is a convenience, not a security claim.
73
- REA_ARGV=()
74
- RESOLVED_CLI_PATH=""
75
- if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
76
- REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
77
- RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
78
- elif [ -f "$proj/dist/cli/index.js" ]; then
79
- # rea repo dogfood: the project IS @bookedsolid/rea.
80
- REA_ARGV=(node "$proj/dist/cli/index.js")
81
- RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
82
- fi
83
-
84
- if [ "${#REA_ARGV[@]}" -eq 0 ]; then
85
- # No rea CLI in scope — drop the advisory silently. This is the
86
- # expected state during bootstrap (consumer ran `rea init` but
87
- # hasn't installed the npm package yet) or in non-rea repos. A
88
- # noisy stderr warning here would fire on every write-class tool
89
- # call and drown legitimate output.
90
- exit 0
91
- fi
92
-
93
- # 3. Realpath sandbox check — mirrors delegation-capture.sh §3 and
94
- # protected-paths-bash-gate.sh §6. The resolved CLI MUST live inside
95
- # realpath(CLAUDE_PROJECT_DIR) AND have an ancestor package.json
96
- # declaring `@bookedsolid/rea` as its `name`. Catches symlink-out
97
- # attacks where an attacker writes
98
- # node_modules/@bookedsolid/rea → /tmp/forged-tree.
99
- if ! command -v node >/dev/null 2>&1; then
100
- # Node not on PATH — we can't verify the CLI shape. Fail safe by
101
- # dropping the advisory (it is not a security claim; the rest of
102
- # the Bash gate suite refuses on this path).
103
- exit 0
104
- fi
105
-
106
- sandbox_check=$(node -e '
107
- const fs = require("fs");
108
- const path = require("path");
109
- const cli = process.argv[1];
110
- const projDir = process.argv[2];
111
- let real, realProj;
112
- try { real = fs.realpathSync(cli); } catch (e) {
113
- process.stdout.write("bad:realpath");
114
- process.exit(1);
115
- }
116
- try { realProj = fs.realpathSync(projDir); } catch (e) {
117
- process.stdout.write("bad:realpath-proj");
118
- process.exit(1);
119
- }
120
- const sep = path.sep;
121
- const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
122
- if (!(real === realProj || real.startsWith(projWithSep))) {
123
- process.stdout.write("bad:cli-escapes-project");
124
- process.exit(1);
125
- }
126
- // Walk up looking for package.json with the protected name.
127
- let cur = path.dirname(path.dirname(path.dirname(real))); // pkg root
128
- let found = false;
129
- for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
130
- const pj = path.join(cur, "package.json");
131
- if (fs.existsSync(pj)) {
132
- try {
133
- const data = JSON.parse(fs.readFileSync(pj, "utf8"));
134
- if (data && data.name === "@bookedsolid/rea") { found = true; break; }
135
- } catch (e) { /* keep walking */ }
136
- }
137
- cur = path.dirname(cur);
138
- }
139
- if (!found) {
140
- process.stdout.write("bad:no-rea-pkg-json");
141
- process.exit(1);
142
- }
143
- process.stdout.write("ok");
144
- ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
145
-
146
- if [ "$sandbox_check" != "ok" ]; then
147
- # CLI failed the sandbox check — silent drop. The forensic
148
- # breadcrumb in stderr is intentional but trimmed so this doesn't
149
- # become spammy on every tool call.
150
- printf 'rea: delegation-advisory skipped (sandbox check: %s)\n' "$sandbox_check" >&2
151
- exit 0
152
- fi
36
+ SHIM_NAME="delegation-advisory"
37
+ SHIM_INTRODUCED_IN="0.31.0"
38
+ SHIM_FAIL_OPEN=1
39
+ SHIM_SKIP_VERSION_PROBE=1
40
+ SHIM_REFUSAL_NOUN="the delegation-advisory nudge"
153
41
 
154
- # 4. Read stdin and pipe to the CLI SYNCHRONOUSLY. The advisory must
155
- # print before this hook returns — see the "Synchronous" note in
156
- # the header. We pass CLAUDE_PROJECT_DIR through explicitly so the
157
- # CLI resolves the same REA_ROOT this shim did. The CLI's own exit
158
- # code is the hook's exit code: 0 normally, 2 under HALT (the CLI
159
- # re-checks HALT itself for defense-in-depth).
160
- INPUT=$(cat)
161
- printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook delegation-advisory
162
- exit $?
42
+ # shellcheck source=_lib/shim-runtime.sh
43
+ source "$(dirname "$0")/_lib/shim-runtime.sh"
44
+ shim_run
@@ -1,158 +1,52 @@
1
1
  #!/bin/bash
2
2
  # PreToolUse hook: delegation-capture.sh
3
3
  # 0.29.0+ — delegation-telemetry MVP.
4
+ # 0.38.0+ — migrated to `_lib/shim-runtime.sh` (shared runtime).
4
5
  #
5
- # Fires BEFORE every `Agent` or `Skill` tool call. Reads the Claude
6
- # Code hook payload from stdin, pipes it to
7
- # `rea hook delegation-signal --detach`, and exits 0 immediately.
6
+ # Fires BEFORE every `Agent` or `Skill` tool call. Pipes the payload to
7
+ # `rea hook delegation-signal --detach`, BACKGROUNDED so the hook
8
+ # returns instantly even when the CLI's startup takes a few ms. The
9
+ # signal is OBSERVATIONAL — never gates tool dispatch.
8
10
  #
9
- # The signal is OBSERVATIONAL — never gates tool dispatch. Worst-case
10
- # latency budget is ~50ms even when the audit chain is under
11
- # cross-process contention, because the audit append runs in the
12
- # background (via `&`) and the CLI subcommand itself only validates
13
- # the payload before forking the writer.
11
+ # Matcher: `Agent|Skill` (NOT `Task|Skill`).
14
12
  #
15
- # Matcher: `Agent|Skill` (NOT `Task|Skill` `TaskCreate`/`TaskList`
16
- # are the unrelated todo-list tools and MUST NOT match).
13
+ # # CLI subcommand differs from SHIM_NAME
17
14
  #
18
- # # CLI-resolution trust boundary
15
+ # The forward target is `rea hook delegation-signal`, not `rea hook
16
+ # delegation-capture` — the hook name and the CLI subcommand differ.
17
+ # We use shim_forward to invoke the correct subcommand.
19
18
  #
20
- # Codex round 3 P1 (2026-05-12): pre-fix this hook resolved the rea
21
- # binary via `$REA_ROOT/node_modules/.bin/rea` then PATH-walked
22
- # `command -v rea`. Either path was attacker-influenced in a consumer
23
- # repo with a forged `node_modules/.bin/rea` symlink or a
24
- # PATH-prepended fake `rea` binary — giving attacker-controlled code
25
- # execution on every Agent/Skill dispatch.
19
+ # # No version probe
26
20
  #
27
- # Fix: this hook now uses the same 2-tier sandboxed resolution that
28
- # protected-paths-bash-gate.sh + blocked-paths-bash-gate.sh use:
29
- # 1. node_modules/@bookedsolid/rea/dist/cli/index.js (consumer-side
30
- # published artifact)
31
- # 2. dist/cli/index.js under CLAUDE_PROJECT_DIR (the rea repo's own
32
- # dogfood install)
33
- #
34
- # A realpath sandbox check ensures the resolved CLI lives INSIDE
35
- # realpath(CLAUDE_PROJECT_DIR) — catches symlink-out attacks.
36
- #
37
- # Exit codes:
38
- # 0 — always (under normal operation). Failure to write the audit
39
- # signal must NEVER block Claude Code's tool dispatch. Stderr
40
- # breadcrumbs surface diagnostic info to the operator. HALT
41
- # still exits 2 because the kill-switch contract must hold.
42
- # 2 — HALT active.
21
+ # SHIM_SKIP_VERSION_PROBE=1: the pre-port body had no probe; a stale
22
+ # CLI drops the signal silently rather than emit a probe-skew banner
23
+ # on every Agent/Skill dispatch.
43
24
 
44
25
  set -uo pipefail
45
26
 
46
- # 1. HALT check. Even though this hook is observational, refusing to
47
- # emit signals while frozen matches the rest of the hook tree and
48
- # keeps the kill-switch contract uniform.
49
27
  # shellcheck source=_lib/halt-check.sh
50
28
  source "$(dirname "$0")/_lib/halt-check.sh"
51
29
  check_halt
52
30
  REA_ROOT=$(rea_root)
53
31
 
54
- proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
55
-
56
- # 2. Resolve the rea CLI through the same fixed 2-tier sandboxed order
57
- # the protected-paths / blocked-paths bash gates use. PATH lookup
58
- # is INTENTIONALLY OMITTED — agent-controlled $PATH would let a
59
- # forged `rea` binary on a consumer machine intercept the
60
- # delegation signal on every Agent/Skill dispatch. The trade-off:
61
- # consumers MUST have `@bookedsolid/rea` installed under
62
- # `node_modules` (the common case after `pnpm i`) OR be running
63
- # against the rea repo's own dogfood (where dist/cli/index.js
64
- # holds the canonical CLI). Other install shapes silently drop the
65
- # signal matching the bash-gate posture.
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 repo dogfood: the project IS @bookedsolid/rea.
73
- REA_ARGV=(node "$proj/dist/cli/index.js")
74
- RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
75
- fi
76
-
77
- if [ "${#REA_ARGV[@]}" -eq 0 ]; then
78
- # No rea CLI in scope — drop the signal silently. This is the
79
- # expected state during bootstrap (consumer ran `rea init` but
80
- # hasn't installed the npm package yet) or in non-rea repos. A
81
- # noisy stderr warning here would fire on every Agent/Skill
82
- # dispatch and drown legitimate signals.
83
- exit 0
84
- fi
85
-
86
- # 3. Realpath sandbox check — mirrors protected-paths-bash-gate.sh §6.
87
- # The resolved CLI MUST live inside realpath(CLAUDE_PROJECT_DIR)
88
- # AND have an ancestor package.json declaring `@bookedsolid/rea`
89
- # as its `name`. Catches symlink-out attacks where an attacker
90
- # writes node_modules/@bookedsolid/rea → /tmp/forged-tree.
91
- if ! command -v node >/dev/null 2>&1; then
92
- # Node not on PATH — we can't verify the CLI shape. Fail safe by
93
- # dropping the signal (observability is not a security claim; the
94
- # rest of the Bash gate suite refuses on this path).
32
+ SHIM_NAME="delegation-capture"
33
+ SHIM_INTRODUCED_IN="0.29.0"
34
+ SHIM_FAIL_OPEN=1
35
+ SHIM_SKIP_VERSION_PROBE=1
36
+ SHIM_REFUSAL_NOUN="the delegation telemetry signal"
37
+
38
+ shim_forward() {
39
+ # Pipe to `rea hook delegation-signal --detach`. `--detach` tells the
40
+ # CLI to suppress stderr; the whole pipeline is backgrounded with
41
+ # `& disown` so the shell hook returns instantly.
42
+ {
43
+ printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook delegation-signal --detach \
44
+ >/dev/null 2>&1 &
45
+ disown 2>/dev/null || true
46
+ } 2>/dev/null
95
47
  exit 0
96
- fi
97
-
98
- sandbox_check=$(node -e '
99
- const fs = require("fs");
100
- const path = require("path");
101
- const cli = process.argv[1];
102
- const projDir = process.argv[2];
103
- let real, realProj;
104
- try { real = fs.realpathSync(cli); } catch (e) {
105
- process.stdout.write("bad:realpath");
106
- process.exit(1);
107
- }
108
- try { realProj = fs.realpathSync(projDir); } catch (e) {
109
- process.stdout.write("bad:realpath-proj");
110
- process.exit(1);
111
- }
112
- const sep = path.sep;
113
- const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
114
- if (!(real === realProj || real.startsWith(projWithSep))) {
115
- process.stdout.write("bad:cli-escapes-project");
116
- process.exit(1);
117
- }
118
- // Walk up looking for package.json with the protected name.
119
- let cur = path.dirname(path.dirname(path.dirname(real))); // pkg root
120
- let found = false;
121
- for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
122
- const pj = path.join(cur, "package.json");
123
- if (fs.existsSync(pj)) {
124
- try {
125
- const data = JSON.parse(fs.readFileSync(pj, "utf8"));
126
- if (data && data.name === "@bookedsolid/rea") { found = true; break; }
127
- } catch (e) { /* keep walking */ }
128
- }
129
- cur = path.dirname(cur);
130
- }
131
- if (!found) {
132
- process.stdout.write("bad:no-rea-pkg-json");
133
- process.exit(1);
134
- }
135
- process.stdout.write("ok");
136
- ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
137
-
138
- if [ "$sandbox_check" != "ok" ]; then
139
- # CLI failed the sandbox check — silent drop. The forensic
140
- # breadcrumb in stderr is intentional but trimmed so this doesn't
141
- # become spammy on every dispatch.
142
- printf 'rea: delegation-capture skipped (sandbox check: %s)\n' "$sandbox_check" >&2
143
- exit 0
144
- fi
145
-
146
- # 4. Read stdin and pipe to the CLI. `--detach` tells the CLI to
147
- # suppress stderr output (no parent shell is listening); we ALSO
148
- # background the whole pipeline with `&` and `disown` so the
149
- # shell hook returns instantly even if the CLI's own startup
150
- # takes a few ms.
151
- INPUT=$(cat)
152
- {
153
- printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook delegation-signal --detach \
154
- >/dev/null 2>&1 &
155
- disown 2>/dev/null || true
156
- } 2>/dev/null
48
+ }
157
49
 
158
- exit 0
50
+ # shellcheck source=_lib/shim-runtime.sh
51
+ source "$(dirname "$0")/_lib/shim-runtime.sh"
52
+ shim_run
@@ -1,138 +1,46 @@
1
1
  #!/bin/bash
2
2
  # PreToolUse hook: dependency-audit-gate.sh
3
3
  # 0.33.0+ — Node-binary shim for `rea hook dependency-audit-gate`.
4
+ # 0.38.0+ — migrated to `_lib/shim-runtime.sh` (shared runtime).
4
5
  #
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`.
6
+ # Blocking-tier: refuses install commands when any requested package
7
+ # isn't published on the registry (pre-port behavior). The full
8
+ # segment splitter + per-package `npm view` probe is in
9
+ # `src/hooks/dependency-audit-gate/index.ts`.
9
10
  #
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.
11
+ # # Relevance pre-gate
13
12
  #
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.
13
+ # 2026-05-15 codex round-2 P2 fix: scan `tool_input.command` ONLY,
14
+ # not the raw JSON payload. Pre-fix `git commit -m "docs: run pnpm
15
+ # install foo"` triggered fail-closed on fresh checkout (the regex hit
16
+ # the substring inside the commit-message ARG). The jq-less fallback
17
+ # preserves the pre-0.33.0 over-trigger shape.
25
18
 
26
19
  set -uo pipefail
27
20
 
28
- # 1. HALT check.
29
21
  # shellcheck source=_lib/halt-check.sh
30
22
  source "$(dirname "$0")/_lib/halt-check.sh"
31
23
  check_halt
32
24
  REA_ROOT=$(rea_root)
33
25
 
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
26
+ SHIM_NAME="dependency-audit-gate"
27
+ SHIM_INTRODUCED_IN="0.33.0"
28
+ SHIM_FAIL_OPEN=0
29
+ SHIM_REFUSAL_NOUN="dependency-audit refusal"
30
+
31
+ shim_is_relevant() {
32
+ local probe
33
+ if command -v jq >/dev/null 2>&1; then
34
+ probe=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || true)
35
+ else
36
+ probe="$INPUT"
55
37
  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
38
+ if printf '%s' "$probe" | grep -qE '(npm[[:space:]]+(install|i|add)|pnpm[[:space:]]+(add|install|i)|yarn[[:space:]]+add)[[:space:]]'; then
39
+ return 0
59
40
  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
41
+ return 1
42
+ }
135
43
 
136
- # 6. Forward stdin.
137
- printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook dependency-audit-gate
138
- exit $?
44
+ # shellcheck source=_lib/shim-runtime.sh
45
+ source "$(dirname "$0")/_lib/shim-runtime.sh"
46
+ shim_run