@bookedsolid/rea 0.32.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 +49 -0
- package/dist/hooks/_lib/payload.d.ts +38 -0
- package/dist/hooks/_lib/payload.js +79 -0
- package/dist/hooks/_lib/segments.d.ts +127 -0
- package/dist/hooks/_lib/segments.js +628 -16
- package/dist/hooks/architecture-review-gate/index.d.ts +58 -0
- package/dist/hooks/architecture-review-gate/index.js +250 -0
- package/dist/hooks/changeset-security-gate/index.d.ts +71 -0
- package/dist/hooks/changeset-security-gate/index.js +330 -0
- package/dist/hooks/dangerous-bash-interceptor/index.d.ts +103 -0
- package/dist/hooks/dangerous-bash-interceptor/index.js +669 -0
- package/dist/hooks/dependency-audit-gate/index.d.ts +91 -0
- package/dist/hooks/dependency-audit-gate/index.js +294 -0
- package/dist/hooks/env-file-protection/index.d.ts +55 -0
- package/dist/hooks/env-file-protection/index.js +159 -0
- package/dist/hooks/local-review-gate/index.d.ts +145 -0
- package/dist/hooks/local-review-gate/index.js +374 -0
- package/dist/hooks/secret-scanner/index.d.ts +143 -0
- package/dist/hooks/secret-scanner/index.js +404 -0
- package/hooks/architecture-review-gate.sh +92 -77
- package/hooks/changeset-security-gate.sh +114 -149
- package/hooks/dangerous-bash-interceptor.sh +168 -386
- package/hooks/dependency-audit-gate.sh +115 -156
- package/hooks/env-file-protection.sh +130 -97
- package/hooks/local-review-gate.sh +523 -410
- package/hooks/secret-scanner.sh +210 -200
- package/package.json +1 -1
- package/templates/architecture-review-gate.dogfood-staged.sh +116 -0
- package/templates/changeset-security-gate.dogfood-staged.sh +137 -0
- package/templates/dangerous-bash-interceptor.dogfood-staged.sh +196 -0
- package/templates/dependency-audit-gate.dogfood-staged.sh +138 -0
- package/templates/env-file-protection.dogfood-staged.sh +157 -0
- package/templates/local-review-gate.dogfood-staged.sh +573 -0
- package/templates/secret-scanner.dogfood-staged.sh +240 -0
|
@@ -1,179 +1,138 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# PreToolUse hook: dependency-audit-gate.sh
|
|
3
|
-
#
|
|
4
|
-
# Detects package install commands (npm install, pnpm add, yarn add) and
|
|
5
|
-
# verifies the package exists on the registry before allowing the install.
|
|
3
|
+
# 0.33.0+ — Node-binary shim for `rea hook dependency-audit-gate`.
|
|
6
4
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
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`.
|
|
9
|
+
#
|
|
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.
|
|
13
|
+
#
|
|
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.
|
|
10
25
|
|
|
11
26
|
set -uo pipefail
|
|
12
27
|
|
|
13
|
-
#
|
|
14
|
-
INPUT=$(cat)
|
|
15
|
-
|
|
16
|
-
# ── 2. Dependency check ──────────────────────────────────────────────────────
|
|
17
|
-
if ! command -v jq >/dev/null 2>&1; then
|
|
18
|
-
printf 'REA ERROR: jq is required but not installed.\n' >&2
|
|
19
|
-
printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
|
|
20
|
-
exit 2
|
|
21
|
-
fi
|
|
22
|
-
|
|
23
|
-
# ── 3. HALT check ────────────────────────────────────────────────────────────
|
|
24
|
-
# 0.16.0: HALT check sourced from shared _lib/halt-check.sh.
|
|
28
|
+
# 1. HALT check.
|
|
25
29
|
# shellcheck source=_lib/halt-check.sh
|
|
26
30
|
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
27
31
|
check_halt
|
|
28
32
|
REA_ROOT=$(rea_root)
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
32
|
-
|
|
33
|
-
if [[ -z "$CMD" ]]; then
|
|
34
|
-
exit 0
|
|
35
|
-
fi
|
|
36
|
-
|
|
37
|
-
# ── 5. Detect package install commands ────────────────────────────────────────
|
|
38
|
-
# Match: npm install <pkg>, npm i <pkg>, pnpm add <pkg>, yarn add <pkg>
|
|
39
|
-
# Skip: npm install (no args), npm ci, npm install --save-dev (without new pkg)
|
|
34
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# 0.17.0 helix-017 #3: unwrap nested-shell wrappers (`bash -c 'PAYLOAD'`,
|
|
62
|
-
# `sh -lc "PAYLOAD"`, etc.) before splitting so the inner install
|
|
63
|
-
# command becomes a segment that anchors against the install-pattern
|
|
64
|
-
# check below. Pre-fix `bash -lc 'npm install pkg'` produced a single
|
|
65
|
-
# segment whose first token was `bash` — install-detection skipped.
|
|
66
|
-
# 0.17.0 helix-019 #3: delegate splitting to the shared
|
|
67
|
-
# `_rea_split_segments` so this gate inherits the full separator set
|
|
68
|
-
# (including bare `&` background-process operator added in 0.16.1)
|
|
69
|
-
# and the quote-mask that prevents over-fire from in-quote separators.
|
|
70
|
-
# Pre-fix the local segmenter splat on `|||&&|;|` only, missing bare
|
|
71
|
-
# `&` — `echo warmup & pnpm add lodash` stayed merged into one segment
|
|
72
|
-
# and the install-pattern leading-token check skipped it entirely.
|
|
73
|
-
local segments
|
|
74
|
-
if [ -f "$(dirname "$0")/_lib/cmd-segments.sh" ]; then
|
|
75
|
-
# shellcheck source=_lib/cmd-segments.sh
|
|
76
|
-
source "$(dirname "$0")/_lib/cmd-segments.sh"
|
|
77
|
-
segments=$(_rea_split_segments "$cmd")
|
|
78
|
-
else
|
|
79
|
-
# Fallback (lib unavailable): legacy local splitter preserved.
|
|
80
|
-
segments=$(printf '%s\n' "$cmd" | sed -E 's/(\|\||\&\&|;|\||\&)/\n/g')
|
|
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
|
|
81
55
|
fi
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
# `time` / etc.
|
|
89
|
-
#
|
|
90
|
-
# 0.16.1 helix-016 P2 fix: also strip leading KEY=VALUE env-var
|
|
91
|
-
# assignments. Pre-fix the prefix allow-list only permitted
|
|
92
|
-
# sudo/exec/time, so `CI=1 pnpm add foo` and
|
|
93
|
-
# `NODE_ENV=development npm install bar` bypassed the audit
|
|
94
|
-
# entirely. POSIX shell allows any number of leading KEY=VALUE
|
|
95
|
-
# assignments before the command word; we strip them the same
|
|
96
|
-
# way the shell does.
|
|
97
|
-
local stripped_segment
|
|
98
|
-
stripped_segment=$(printf '%s' "$segment" | sed -E 's/^([[:space:]]*[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]+[[:space:]]+)+//')
|
|
99
|
-
|
|
100
|
-
if printf '%s' "$stripped_segment" | grep -qiE '^(sudo[[:space:]]+|exec[[:space:]]+|time[[:space:]]+)*(npm[[:space:]]+(install|i|add)|pnpm[[:space:]]+(add|install|i)|yarn[[:space:]]+add)[[:space:]]+'; then
|
|
101
|
-
# Strip the leading prefix wrappers + install command, leaving args.
|
|
102
|
-
local after_cmd
|
|
103
|
-
after_cmd=$(printf '%s' "$stripped_segment" | sed -E 's/^(sudo[[:space:]]+|exec[[:space:]]+|time[[:space:]]+)*(npm[[:space:]]+(install|i|add)|pnpm[[:space:]]+(add|install|i)|yarn[[:space:]]+add)[[:space:]]+//')
|
|
104
|
-
|
|
105
|
-
for token in $after_cmd; do
|
|
106
|
-
if [[ "$token" == -* ]]; then continue; fi
|
|
107
|
-
if [[ "$token" == ./* || "$token" == /* || "$token" == ../* ]]; then continue; fi
|
|
108
|
-
if [[ -z "$token" ]]; then continue; fi
|
|
109
|
-
# 0.16.1: tighten token classification (helix-016 sibling concern).
|
|
110
|
-
# A "package name" is something that doesn't contain shell
|
|
111
|
-
# metacharacters — `2>&1`, `$VAR`, etc. are never valid npm
|
|
112
|
-
# package names. Skip any token containing `=`, `>`, `<`, `&`,
|
|
113
|
-
# `|`, `;`, `$`, backtick, or quotes.
|
|
114
|
-
if [[ "$token" == *=* || "$token" == *">"* || "$token" == *"<"* ||
|
|
115
|
-
"$token" == *"&"* || "$token" == *"|"* || "$token" == *";"* ||
|
|
116
|
-
"$token" == *'$'* || "$token" == *'`'* ||
|
|
117
|
-
"$token" == *'"'* || "$token" == *"'"* ]]; then continue; fi
|
|
118
|
-
# `npm view` can't validate `@workspace:*` / `link:` / `file:`
|
|
119
|
-
# prefixes (workspace protocols). Skip them — they're never npm
|
|
120
|
-
# registry packages.
|
|
121
|
-
if [[ "$token" == workspace:* || "$token" == link:* || "$token" == file:* || "$token" == git+* ]]; then continue; fi
|
|
122
|
-
local pkg_name
|
|
123
|
-
pkg_name=$(printf '%s' "$token" | sed -E 's/@[^@/]+$//')
|
|
124
|
-
if [[ -z "$pkg_name" ]]; then
|
|
125
|
-
pkg_name="$token"
|
|
126
|
-
fi
|
|
127
|
-
printf '%s\n' "$pkg_name"
|
|
128
|
-
done
|
|
129
|
-
fi
|
|
130
|
-
done <<< "$segments"
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
PACKAGES=$(extract_packages "$CMD")
|
|
134
|
-
|
|
135
|
-
if [[ -z "$PACKAGES" ]]; then
|
|
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
|
|
59
|
+
fi
|
|
60
|
+
fi
|
|
61
|
+
if [ "$RELEVANT" -eq 0 ]; then
|
|
136
62
|
exit 0
|
|
137
63
|
fi
|
|
138
64
|
|
|
139
|
-
#
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
142
75
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
146
81
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
151
87
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
165
125
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
printf ' Rule: All packages must exist on the npm registry before installation.\n'
|
|
174
|
-
printf ' Check: Is the package name spelled correctly? Does it exist on npmjs.com?\n'
|
|
175
|
-
} >&2
|
|
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
|
|
176
133
|
exit 2
|
|
177
134
|
fi
|
|
178
135
|
|
|
179
|
-
|
|
136
|
+
# 6. Forward stdin.
|
|
137
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook dependency-audit-gate
|
|
138
|
+
exit $?
|
|
@@ -1,124 +1,157 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# PreToolUse hook: env-file-protection.sh
|
|
3
|
-
#
|
|
4
|
-
# Blocks commands that read .env* / .envrc files via shell text utilities.
|
|
3
|
+
# 0.33.0+ — Node-binary shim for `rea hook env-file-protection`.
|
|
5
4
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
5
|
+
# Pre-0.33.0 the gate's full body lived here as bash (124 LOC, the
|
|
6
|
+
# segment splitter + `source`/`cp` anchor patterns + utility-vs-.env
|
|
7
|
+
# co-occurrence check). The migration to the parser-backed Node binary
|
|
8
|
+
# moves all of that into `src/hooks/env-file-protection/index.ts`. This
|
|
9
|
+
# shim is the Claude Code dispatcher's view of the hook — it forwards
|
|
10
|
+
# stdin to the CLI and exits with whatever the CLI returns.
|
|
9
11
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
12
|
+
# Behavioral contract is preserved byte-for-byte: exit 0 on
|
|
13
|
+
# pass-through / no-match, exit 2 on HALT / .env access detected /
|
|
14
|
+
# malformed payload (fail-closed).
|
|
13
15
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
16
|
+
# # CLI-resolution trust boundary
|
|
17
|
+
#
|
|
18
|
+
# Mirrors the 0.32.0 final shim shape (round-8 of the codex iteration
|
|
19
|
+
# on the three Phase 1 pilots). The resolved CLI MUST live INSIDE
|
|
20
|
+
# realpath(CLAUDE_PROJECT_DIR) AND have an ancestor `package.json`
|
|
21
|
+
# whose `name` is `@bookedsolid/rea`. Defends against symlink-out and
|
|
22
|
+
# tarball-replacement attacks on the resolved CLI.
|
|
23
|
+
#
|
|
24
|
+
# # Fail-closed posture
|
|
25
|
+
#
|
|
26
|
+
# env-file-protection is a BLOCKING-tier gate — the pre-0.33.0 bash
|
|
27
|
+
# body refused on .env access without a compiled CLI. The early-exit
|
|
28
|
+
# branches (CLI missing, node missing, sandbox failed, version skew)
|
|
29
|
+
# fail closed AFTER the relevance pre-gate passes. Irrelevant Bash
|
|
30
|
+
# calls exit 0 regardless of CLI state.
|
|
17
31
|
|
|
18
32
|
set -uo pipefail
|
|
19
33
|
|
|
20
|
-
#
|
|
21
|
-
# grep that false-positives on commit messages mentioning `.env` (e.g.
|
|
22
|
-
# `git commit -m "stop reading .env via cat"`).
|
|
23
|
-
# shellcheck source=_lib/cmd-segments.sh
|
|
24
|
-
source "$(dirname "$0")/_lib/cmd-segments.sh"
|
|
25
|
-
|
|
26
|
-
INPUT=$(cat)
|
|
27
|
-
|
|
28
|
-
# ── Dependency check ──────────────────────────────────────────────────────────
|
|
29
|
-
if ! command -v jq >/dev/null 2>&1; then
|
|
30
|
-
printf 'REA ERROR: jq is required but not installed.\n' >&2
|
|
31
|
-
printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
|
|
32
|
-
exit 2
|
|
33
|
-
fi
|
|
34
|
-
|
|
35
|
-
# ── HALT check ────────────────────────────────────────────────────────────────
|
|
36
|
-
# 0.16.0: HALT check sourced from shared _lib/halt-check.sh.
|
|
34
|
+
# 1. HALT check.
|
|
37
35
|
# shellcheck source=_lib/halt-check.sh
|
|
38
36
|
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
39
37
|
check_halt
|
|
40
38
|
REA_ROOT=$(rea_root)
|
|
41
39
|
|
|
42
|
-
|
|
40
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
43
41
|
|
|
44
|
-
|
|
42
|
+
# 2. Relevance pre-gate. Capture stdin + check for `.env` substring
|
|
43
|
+
# BEFORE any CLI/sandbox/probe work so unrelated Bash calls
|
|
44
|
+
# (`ls`, `pnpm test`, `git status`, …) exit 0 even when the CLI
|
|
45
|
+
# is missing/stale/sandboxed-out.
|
|
46
|
+
#
|
|
47
|
+
# 2026-05-15 codex round-2 P2 fix: the substring scan MUST run
|
|
48
|
+
# against `tool_input.command` ONLY, not the raw JSON payload —
|
|
49
|
+
# otherwise a benign `git commit -m "stop reading .env"` (where
|
|
50
|
+
# `.env` appears inside the commit message ARG, NOT as a file
|
|
51
|
+
# target) would hit the fail-closed branch on a fresh checkout
|
|
52
|
+
# where the CLI is unbuilt. Pre-fix the raw scan saw the substring
|
|
53
|
+
# inside the payload's "command" string-quoted body and refused.
|
|
54
|
+
#
|
|
55
|
+
# Strategy: extract `tool_input.command` via `jq` (already required
|
|
56
|
+
# by 5 other hooks; trust assumption is consistent). When `jq` is
|
|
57
|
+
# not installed, fall back to scanning the raw payload — the cost
|
|
58
|
+
# is the same over-trigger the bash original had, NOT a new
|
|
59
|
+
# regression. When `jq` IS installed (the common case), the
|
|
60
|
+
# pre-gate is field-scoped.
|
|
61
|
+
INPUT=$(cat)
|
|
62
|
+
RELEVANT=0
|
|
63
|
+
PROBE=""
|
|
64
|
+
if command -v jq >/dev/null 2>&1; then
|
|
65
|
+
PROBE=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || true)
|
|
66
|
+
if printf '%s' "$PROBE" | grep -qE '\.env'; then
|
|
67
|
+
RELEVANT=1
|
|
68
|
+
fi
|
|
69
|
+
else
|
|
70
|
+
# jq-less fallback — match the pre-0.33.0 over-trigger posture.
|
|
71
|
+
if printf '%s' "$INPUT" | grep -qE '\.env'; then
|
|
72
|
+
RELEVANT=1
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
75
|
+
if [ "$RELEVANT" -eq 0 ]; then
|
|
45
76
|
exit 0
|
|
46
77
|
fi
|
|
47
78
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
79
|
+
# 3. Resolve the rea CLI through the fixed 2-tier sandboxed order.
|
|
80
|
+
REA_ARGV=()
|
|
81
|
+
RESOLVED_CLI_PATH=""
|
|
82
|
+
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
83
|
+
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
84
|
+
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
85
|
+
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
86
|
+
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
87
|
+
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
88
|
+
fi
|
|
57
89
|
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
PATTERN_UTILITY='(cat|head|tail|less|more|grep|sed|awk|bat|strings|printf|xargs|tee|jq|python3?[[:space:]]+-c|ruby[[:space:]]+-e)[[:space:]]'
|
|
68
|
-
# Also catch: source/., cp (reads then writes elsewhere).
|
|
69
|
-
#
|
|
70
|
-
# 0.16.3 discord-ops Round 9 #4 fix: anchored on segment-start. Pre-fix
|
|
71
|
-
# `any_segment_matches` matched anywhere in the segment, so
|
|
72
|
-
# `git commit -m "fix: don't source .env files"` fired even though no
|
|
73
|
-
# real source-of-.env was happening — the trigger words appeared inside
|
|
74
|
-
# the quoted commit-message body. The patterns are command prefixes
|
|
75
|
-
# (`source PATH`, `. PATH`, `cp X PATH`), so segment-start anchoring is
|
|
76
|
-
# the correct shape.
|
|
77
|
-
PATTERN_SOURCE='(source|\.)[[:space:]]+[^;|&]*\.env'
|
|
78
|
-
PATTERN_CP_ENV='cp[[:space:]]+[^;|&]*\.env'
|
|
79
|
-
# .env* files or .envrc (direnv)
|
|
80
|
-
PATTERN_ENV_FILE='(\.env[a-zA-Z0-9._-]*|\.envrc)([[:space:]]|"|'"'"'|$)'
|
|
90
|
+
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
91
|
+
# Blocking-tier: fail closed. The pre-0.33.0 bash body enforced
|
|
92
|
+
# .env protection without a CLI. Refuse and tell the operator how
|
|
93
|
+
# to restore protection.
|
|
94
|
+
printf 'rea: env-file-protection cannot run — the rea CLI is not built.\n' >&2
|
|
95
|
+
printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
|
|
96
|
+
printf 'This shim fails closed because the pre-0.33.0 bash body enforced .env protection without a CLI.\n' >&2
|
|
97
|
+
exit 2
|
|
98
|
+
fi
|
|
81
99
|
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
# .env name in segment 2). Detection is fundamentally a same-segment
|
|
88
|
-
# co-occurrence property.
|
|
89
|
-
MATCHES_BOTH_SAME_SEGMENT=0
|
|
90
|
-
if any_segment_matches_both "$CMD" "$PATTERN_UTILITY" "$PATTERN_ENV_FILE"; then
|
|
91
|
-
MATCHES_BOTH_SAME_SEGMENT=1
|
|
100
|
+
# 4. Realpath sandbox check.
|
|
101
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
102
|
+
printf 'rea: env-file-protection cannot run — `node` is not on PATH.\n' >&2
|
|
103
|
+
printf 'Install Node 22+ (engines.node) to restore .env protection.\n' >&2
|
|
104
|
+
exit 2
|
|
92
105
|
fi
|
|
93
106
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
sandbox_check=$(node -e '
|
|
108
|
+
const fs = require("fs");
|
|
109
|
+
const path = require("path");
|
|
110
|
+
const cli = process.argv[1];
|
|
111
|
+
const projDir = process.argv[2];
|
|
112
|
+
let real, realProj;
|
|
113
|
+
try { real = fs.realpathSync(cli); } catch (e) {
|
|
114
|
+
process.stdout.write("bad:realpath"); process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
117
|
+
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
const sep = path.sep;
|
|
120
|
+
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
121
|
+
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
122
|
+
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
125
|
+
let found = false;
|
|
126
|
+
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
127
|
+
const pj = path.join(cur, "package.json");
|
|
128
|
+
if (fs.existsSync(pj)) {
|
|
129
|
+
try {
|
|
130
|
+
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
131
|
+
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
132
|
+
} catch (e) { /* keep walking */ }
|
|
133
|
+
}
|
|
134
|
+
cur = path.dirname(cur);
|
|
135
|
+
}
|
|
136
|
+
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
137
|
+
process.stdout.write("ok");
|
|
138
|
+
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
139
|
+
|
|
140
|
+
if [ "$sandbox_check" != "ok" ]; then
|
|
141
|
+
printf 'rea: env-file-protection FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
|
|
107
142
|
exit 2
|
|
108
143
|
fi
|
|
109
144
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
printf ' Rule: Load credentials in code only, never via shell.\n'
|
|
118
|
-
printf ' Use: process.env.VAR_NAME, os.environ["VAR_NAME"], etc.\n'
|
|
119
|
-
printf ' .env files must not be read via shell utilities in agent sessions.\n'
|
|
120
|
-
} >&2
|
|
145
|
+
# 5. Version-probe.
|
|
146
|
+
probe_out=$("${REA_ARGV[@]}" hook env-file-protection --help 2>&1)
|
|
147
|
+
probe_status=$?
|
|
148
|
+
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'env-file-protection'; then
|
|
149
|
+
printf 'rea: this shim requires the `rea hook env-file-protection` subcommand (introduced in 0.33.0).\n' >&2
|
|
150
|
+
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
151
|
+
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
|
|
121
152
|
exit 2
|
|
122
153
|
fi
|
|
123
154
|
|
|
124
|
-
|
|
155
|
+
# 6. Forward stdin (already captured up-front for the relevance gate).
|
|
156
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook env-file-protection
|
|
157
|
+
exit $?
|