@bookedsolid/rea 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hooks/_lib/policy-reader.sh +948 -0
- package/hooks/_lib/shim-runtime.sh +405 -0
- package/hooks/architecture-review-gate.sh +11 -103
- package/hooks/attribution-advisory.sh +43 -155
- package/hooks/blocked-paths-bash-gate.sh +35 -149
- package/hooks/blocked-paths-enforcer.sh +35 -140
- package/hooks/changeset-security-gate.sh +26 -119
- package/hooks/dangerous-bash-interceptor.sh +46 -170
- package/hooks/delegation-advisory.sh +26 -144
- package/hooks/delegation-capture.sh +33 -139
- package/hooks/dependency-audit-gate.sh +29 -121
- package/hooks/env-file-protection.sh +30 -141
- package/hooks/local-review-gate.sh +191 -396
- package/hooks/pr-issue-link-gate.sh +16 -118
- package/hooks/protected-paths-bash-gate.sh +57 -160
- package/hooks/secret-scanner.sh +90 -213
- package/hooks/security-disclosure-gate.sh +32 -155
- package/hooks/settings-protection.sh +56 -179
- package/package.json +1 -1
- package/templates/_lib_policy-reader.dogfood-staged.sh +948 -0
- package/templates/_lib_shim-runtime.dogfood-staged.sh +405 -0
- package/templates/architecture-review-gate.dogfood-staged.sh +11 -103
- package/templates/attribution-advisory.dogfood-staged.sh +43 -155
- package/templates/blocked-paths-bash-gate.dogfood-staged.sh +35 -149
- package/templates/blocked-paths-enforcer.dogfood-staged.sh +35 -140
- package/templates/changeset-security-gate.dogfood-staged.sh +26 -119
- package/templates/dangerous-bash-interceptor.dogfood-staged.sh +46 -170
- package/templates/delegation-advisory.dogfood-staged.sh +44 -0
- package/templates/delegation-capture.dogfood-staged.sh +52 -0
- package/templates/dependency-audit-gate.dogfood-staged.sh +29 -121
- package/templates/env-file-protection.dogfood-staged.sh +30 -141
- package/templates/local-review-gate.dogfood-staged.sh +191 -396
- package/templates/pr-issue-link-gate.dogfood-staged.sh +16 -118
- package/templates/protected-paths-bash-gate.dogfood-staged.sh +57 -160
- package/templates/secret-scanner.dogfood-staged.sh +90 -213
- package/templates/security-disclosure-gate.dogfood-staged.sh +32 -155
- package/templates/settings-protection.dogfood-staged.sh +56 -179
|
@@ -1,57 +1,30 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# PreToolUse hook: local-review-gate.sh
|
|
3
3
|
# 0.34.0+ — Node-binary shim for `rea hook local-review-gate`.
|
|
4
|
+
# 0.38.0+ — uses helpers from `_lib/shim-runtime.sh` (shared
|
|
5
|
+
# CLI-resolution, sandbox, and banners). Cannot use the
|
|
6
|
+
# full `shim_run` orchestrator because the hot-path policy
|
|
7
|
+
# reads need to happen AFTER an early sandbox check (round-5
|
|
8
|
+
# P1) and the relevance scan is policy-driven on
|
|
9
|
+
# `review.local_review.refuse_at` (round-1 P2).
|
|
4
10
|
#
|
|
5
|
-
# Pre-0.34.0 the gate's full body lived here as bash (460 LOC
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# to the Node binary moves the per-segment trigger detection +
|
|
9
|
-
# preflight call into `src/hooks/local-review-gate/index.ts`. This
|
|
10
|
-
# shim is the Claude Code dispatcher's view of the hook — it
|
|
11
|
-
# forwards stdin to the CLI and exits with whatever the CLI returns.
|
|
11
|
+
# Pre-0.34.0 the gate's full body lived here as bash (460 LOC). The
|
|
12
|
+
# migration moves per-segment trigger detection + preflight call into
|
|
13
|
+
# `src/hooks/local-review-gate/index.ts`. This shim:
|
|
12
14
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# Round-1 P1 fix: read the mode + bypass env-var INLINE in the shim
|
|
27
|
-
# BEFORE any CLI resolution. These two short-circuits exit 0 cleanly
|
|
28
|
-
# without spawning node. The full enforcement (multi-trigger sweep,
|
|
29
|
-
# inline-bypass evaluation, preflight call) still lives in the CLI.
|
|
30
|
-
#
|
|
31
|
-
# # CLI-resolution trust boundary
|
|
32
|
-
#
|
|
33
|
-
# Mirrors the 0.32.0 final shim shape. The resolved CLI MUST live
|
|
34
|
-
# INSIDE realpath(CLAUDE_PROJECT_DIR) AND have an ancestor
|
|
35
|
-
# `package.json` whose `name` is `@bookedsolid/rea`.
|
|
36
|
-
#
|
|
37
|
-
# # Fail-closed posture
|
|
38
|
-
#
|
|
39
|
-
# local-review-gate is BLOCKING-tier — the pre-0.34.0 bash body
|
|
40
|
-
# refused `git push` (and optionally `git commit`) without a recent
|
|
41
|
-
# audit entry. The early-exit branches (CLI missing, node missing,
|
|
42
|
-
# sandbox failed, version skew) fail closed AFTER the relevance
|
|
43
|
-
# pre-gate passes AND AFTER the mode/bypass short-circuits.
|
|
44
|
-
#
|
|
45
|
-
# # Relevance pre-gate
|
|
46
|
-
#
|
|
47
|
-
# Round-1 P2 fix: the substring scan must NOT mark commands as
|
|
48
|
-
# relevant when `git push`/`git commit` only appears inside a quoted
|
|
49
|
-
# argument body (`echo "remember git push later"`,
|
|
50
|
-
# `git commit -m "doc: explain git push --force"`). Pre-fix the
|
|
51
|
-
# substring scan saw these as relevant → entered fail-closed branch
|
|
52
|
-
# when CLI was missing. Fix: anchor the substring scan on segment
|
|
53
|
-
# heads via a stripped-prefix check, matching the CLI's segment-aware
|
|
54
|
-
# detector.
|
|
15
|
+
# 1. HALT check
|
|
16
|
+
# 2. Read stdin
|
|
17
|
+
# 2b. Early default-bypass-env-var short-circuit (round-7 P2)
|
|
18
|
+
# 3. Resolve CLI + EARLY sandbox check (round-5 P1: prevent
|
|
19
|
+
# unsandboxed CLI from running during policy lookup)
|
|
20
|
+
# 3b. Subtree-cached policy reads via `_lib/policy-reader.sh`
|
|
21
|
+
# 4. Mode-off short-circuit
|
|
22
|
+
# 5. Refuse_at + relevance scan
|
|
23
|
+
# 6. Policy-driven bypass env-var short-circuit
|
|
24
|
+
# 7. CLI-missing handling
|
|
25
|
+
# 8. Sandbox check (idempotent re-run; emit banner on failure)
|
|
26
|
+
# 9. Version probe
|
|
27
|
+
# 10. Forward
|
|
55
28
|
|
|
56
29
|
set -uo pipefail
|
|
57
30
|
|
|
@@ -63,260 +36,173 @@ REA_ROOT=$(rea_root)
|
|
|
63
36
|
|
|
64
37
|
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
65
38
|
|
|
66
|
-
#
|
|
67
|
-
|
|
39
|
+
# SHIM_* metadata for shared banner helpers.
|
|
40
|
+
SHIM_NAME="local-review-gate"
|
|
41
|
+
SHIM_INTRODUCED_IN="0.34.0"
|
|
42
|
+
SHIM_FAIL_OPEN=0
|
|
43
|
+
SHIM_ENFORCE_CLI_SHAPE=0
|
|
44
|
+
SHIM_REFUSAL_NOUN="local-first review enforcement"
|
|
45
|
+
SHIM_NODE_MISSING_NOUN="local-first review enforcement"
|
|
46
|
+
SHIM_SKIP_VERSION_PROBE=0
|
|
47
|
+
# shellcheck source=_lib/shim-runtime.sh
|
|
48
|
+
source "$(dirname "$0")/_lib/shim-runtime.sh"
|
|
49
|
+
_shim_apply_defaults
|
|
50
|
+
|
|
51
|
+
# 2. Read stdin once.
|
|
68
52
|
INPUT=$(cat)
|
|
69
53
|
|
|
70
|
-
# 2b. Early bypass-env-var short-circuit
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
# check
|
|
74
|
-
#
|
|
75
|
-
# unbuilt installs OR when the CLI fails the sandbox check, those
|
|
76
|
-
# policy reads can no-op silently — but the audited bypass should
|
|
77
|
-
# STILL short-circuit so operators can push through the gate.
|
|
78
|
-
#
|
|
79
|
-
# We can only check the DEFAULT var name (REA_SKIP_LOCAL_REVIEW)
|
|
80
|
-
# this early because the policy-renamed `bypass_env_var` requires
|
|
81
|
-
# a policy read. The policy-aware re-check at section 6 still runs
|
|
82
|
-
# for renamed vars when the CLI is reachable. Operators who rename
|
|
83
|
-
# the var AND have a broken CLI fall back to the section-6 awk
|
|
84
|
-
# parser (block-form only) — same posture as pre-fix; this early
|
|
85
|
-
# gate only adds coverage for the default-var case.
|
|
54
|
+
# 2b. Early default-bypass-env-var short-circuit. We can only check the
|
|
55
|
+
# DEFAULT var name (REA_SKIP_LOCAL_REVIEW) this early because the
|
|
56
|
+
# policy-renamed var requires a policy read. The policy-aware
|
|
57
|
+
# re-check at section 6 still runs for renamed vars when the CLI
|
|
58
|
+
# is reachable.
|
|
86
59
|
EARLY_BYPASS_VALUE="${REA_SKIP_LOCAL_REVIEW:-}"
|
|
87
60
|
if [ -n "$EARLY_BYPASS_VALUE" ]; then
|
|
88
61
|
exit 0
|
|
89
62
|
fi
|
|
90
63
|
|
|
91
|
-
# 3. Resolve
|
|
92
|
-
|
|
93
|
-
# mappings, and (b) by the forward step at the bottom. Stored as
|
|
94
|
-
# REA_ARGV so the same array drives both calls.
|
|
95
|
-
POLICY_FILE="$proj/.rea/policy.yaml"
|
|
96
|
-
REA_ARGV=()
|
|
97
|
-
RESOLVED_CLI_PATH=""
|
|
98
|
-
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
99
|
-
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
100
|
-
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
101
|
-
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
102
|
-
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
103
|
-
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
104
|
-
fi
|
|
64
|
+
# 3. Resolve CLI early (used by policy reader Tier 1 + final forward).
|
|
65
|
+
shim_resolve_cli
|
|
105
66
|
|
|
106
|
-
# Round-5 P1 fix: sandbox-check the
|
|
67
|
+
# Round-5 P1 fix: sandbox-check the CLI BEFORE any policy-get
|
|
107
68
|
# invocation. Pre-fix `_lrg_read_policy()` could spawn the resolved CLI
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
# We now validate the CLI's realpath sits inside CLAUDE_PROJECT_DIR
|
|
113
|
-
# AND has an ancestor `package.json` with name `@bookedsolid/rea`
|
|
114
|
-
# BEFORE the policy reader is allowed to spawn it. On failure we
|
|
115
|
-
# zero out REA_ARGV so the policy reader falls through to the awk
|
|
116
|
-
# block-form parser (which never spawns anything), and the eventual
|
|
117
|
-
# CLI-forward step at section 7 will refuse with the sandbox banner.
|
|
69
|
+
# for mode-off / refuse_at reads BEFORE the sandbox guard fired — a
|
|
70
|
+
# symlinked or swapped dist/cli/index.js would execute during policy
|
|
71
|
+
# lookup, defeating the realpath / package.json trust boundary.
|
|
72
|
+
SANDBOX_EARLY_FAILURE=""
|
|
118
73
|
if [ "${#REA_ARGV[@]}" -gt 0 ] && command -v node >/dev/null 2>&1; then
|
|
119
|
-
sandbox_check_early=$(
|
|
120
|
-
const fs = require("fs");
|
|
121
|
-
const path = require("path");
|
|
122
|
-
const cli = process.argv[1];
|
|
123
|
-
const projDir = process.argv[2];
|
|
124
|
-
let real, realProj;
|
|
125
|
-
try { real = fs.realpathSync(cli); } catch (e) {
|
|
126
|
-
process.stdout.write("bad:realpath"); process.exit(1);
|
|
127
|
-
}
|
|
128
|
-
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
129
|
-
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
130
|
-
}
|
|
131
|
-
const sep = path.sep;
|
|
132
|
-
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
133
|
-
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
134
|
-
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
135
|
-
}
|
|
136
|
-
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
137
|
-
let found = false;
|
|
138
|
-
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
139
|
-
const pj = path.join(cur, "package.json");
|
|
140
|
-
if (fs.existsSync(pj)) {
|
|
141
|
-
try {
|
|
142
|
-
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
143
|
-
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
144
|
-
} catch (e) { /* keep walking */ }
|
|
145
|
-
}
|
|
146
|
-
cur = path.dirname(cur);
|
|
147
|
-
}
|
|
148
|
-
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
149
|
-
process.stdout.write("ok");
|
|
150
|
-
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
74
|
+
sandbox_check_early=$(shim_sandbox_check "$RESOLVED_CLI_PATH" "$proj" "$SHIM_ENFORCE_CLI_SHAPE")
|
|
151
75
|
if [ "$sandbox_check_early" != "ok" ]; then
|
|
152
|
-
# Sandbox failed. Stash the failure reason and clear REA_ARGV so
|
|
153
|
-
# the policy reader falls through to awk. The section-7 forward
|
|
154
|
-
# step will re-run the sandbox check and emit the canonical
|
|
155
|
-
# refusal banner to stderr.
|
|
156
76
|
SANDBOX_EARLY_FAILURE="$sandbox_check_early"
|
|
157
77
|
REA_ARGV=()
|
|
158
78
|
fi
|
|
159
79
|
fi
|
|
160
80
|
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
# 0.34.0 round-2 P2 fix: pre-fix the shim only ran the block-form awk
|
|
169
|
-
# parser, so inline-form mappings like
|
|
170
|
-
# `local_review: { mode: off, refuse_at: commit }` silently no-op'd on
|
|
171
|
-
# stale-CLI installs (the canonical loader DOES handle them — only the
|
|
172
|
-
# shim was block-only). Hybrid policy reader mirrors the pattern used
|
|
173
|
-
# by prepare-commit-msg's augmenter.
|
|
81
|
+
# 0.37.0: route policy reads through the unified policy-reader. The
|
|
82
|
+
# pre-0.37.0 helper was a hand-rolled dual-tier (CLI subtree JSON +
|
|
83
|
+
# per-leaf awk block-form parser). The new helper consolidates CLI +
|
|
84
|
+
# python3 + awk into a single 4-tier ladder so inline-form mappings
|
|
85
|
+
# like `local_review: { mode: off, refuse_at: commit }` work on
|
|
86
|
+
# installs where the CLI is unreachable AND python3 + PyYAML are
|
|
87
|
+
# available.
|
|
174
88
|
#
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
#
|
|
178
|
-
#
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
89
|
+
# Codex round 4 P2 (2026-05-16): local-review-gate fires on EVERY Bash
|
|
90
|
+
# PreToolUse and reads three leaves from `review.local_review`. The
|
|
91
|
+
# unified reader's CLI tier spawns a fresh `rea hook policy-get` per
|
|
92
|
+
# leaf, so the hot path went from 1 CLI startup (pre-0.37.0 subtree
|
|
93
|
+
# call) to 4. We restore the subtree-cache shape: fetch
|
|
94
|
+
# `review.local_review` as JSON once, then extract leaves locally.
|
|
95
|
+
# Falls back to per-leaf reads when the subtree call returns null /
|
|
96
|
+
# empty (e.g. Tier 3 awk can't serve subtree).
|
|
97
|
+
# shellcheck source=_lib/policy-reader.sh
|
|
98
|
+
source "$(dirname "$0")/_lib/policy-reader.sh"
|
|
99
|
+
|
|
100
|
+
_LRG_LR_SUBTREE_JSON=""
|
|
101
|
+
|
|
102
|
+
_lrg_load_local_review_subtree() {
|
|
103
|
+
if [ -n "$_LRG_LR_SUBTREE_JSON" ]; then
|
|
104
|
+
return 0
|
|
105
|
+
fi
|
|
106
|
+
local sub
|
|
107
|
+
sub=$(policy_reader_get_subtree_json review.local_review 2>/dev/null)
|
|
108
|
+
if [ -z "$sub" ]; then
|
|
109
|
+
_LRG_LR_SUBTREE_JSON="null"
|
|
110
|
+
else
|
|
111
|
+
_LRG_LR_SUBTREE_JSON="$sub"
|
|
112
|
+
fi
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_lrg_subtree_leaf() {
|
|
116
|
+
local leaf="$1"
|
|
117
|
+
if [ -z "$_LRG_LR_SUBTREE_JSON" ] || [ "$_LRG_LR_SUBTREE_JSON" = "null" ]; then
|
|
118
|
+
return 1
|
|
200
119
|
fi
|
|
201
|
-
|
|
202
|
-
if [ "$_lrg_subtree_json" != "<none>" ]; then
|
|
120
|
+
if command -v jq >/dev/null 2>&1; then
|
|
203
121
|
local out
|
|
204
|
-
out=$(printf '%s' "$
|
|
205
|
-
|
|
206
|
-
|
|
122
|
+
out=$(printf '%s' "$_LRG_LR_SUBTREE_JSON" | jq -r --arg k "$leaf" '
|
|
123
|
+
.[$k] as $v
|
|
124
|
+
| if $v == null then empty
|
|
125
|
+
elif ($v|type) == "string" or ($v|type) == "number" or ($v|type) == "boolean"
|
|
126
|
+
then $v | tostring
|
|
127
|
+
else empty
|
|
128
|
+
end
|
|
129
|
+
' 2>/dev/null)
|
|
207
130
|
if [ -n "$out" ]; then
|
|
208
131
|
printf '%s' "$out"
|
|
209
132
|
return 0
|
|
210
133
|
fi
|
|
211
|
-
|
|
212
|
-
# NOT also try the awk parser; the canonical loader is the source
|
|
213
|
-
# of truth here.
|
|
214
|
-
return 0
|
|
134
|
+
return 1
|
|
215
135
|
fi
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
136
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
137
|
+
local out
|
|
138
|
+
out=$(env -u PYTHONPATH -u PYTHONHOME -u PYTHONSTARTUP \
|
|
139
|
+
PYTHONSAFEPATH=1 python3 -c '
|
|
140
|
+
import sys
|
|
141
|
+
import os
|
|
142
|
+
_cwd = os.getcwd()
|
|
143
|
+
_cwd_real = os.path.realpath(_cwd)
|
|
144
|
+
sys.path[:] = [p for p in sys.path if p not in ("", ".", _cwd, _cwd_real)]
|
|
145
|
+
import json
|
|
146
|
+
try:
|
|
147
|
+
doc = json.loads(sys.argv[1])
|
|
148
|
+
except Exception:
|
|
149
|
+
sys.exit(0)
|
|
150
|
+
leaf = sys.argv[2]
|
|
151
|
+
if isinstance(doc, dict) and leaf in doc:
|
|
152
|
+
v = doc[leaf]
|
|
153
|
+
if isinstance(v, bool):
|
|
154
|
+
sys.stdout.write("true" if v else "false")
|
|
155
|
+
elif isinstance(v, (int, float, str)):
|
|
156
|
+
sys.stdout.write(str(v))
|
|
157
|
+
' "$_LRG_LR_SUBTREE_JSON" "$leaf" 2>/dev/null)
|
|
158
|
+
if [ -n "$out" ]; then
|
|
159
|
+
printf '%s' "$out"
|
|
160
|
+
return 0
|
|
161
|
+
fi
|
|
223
162
|
fi
|
|
163
|
+
return 1
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_lrg_read_policy() {
|
|
167
|
+
local key="$1"
|
|
224
168
|
case "$key" in
|
|
225
|
-
review.local_review
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
in_lr && /^[[:space:]]{0,2}[a-zA-Z]/ && !/^[[:space:]]+local_review/ { in_lr=0 }
|
|
234
|
-
' "$POLICY_FILE" 2>/dev/null | tr -d '"' | tr -d "'" | head -1
|
|
235
|
-
;;
|
|
236
|
-
review.local_review.refuse_at)
|
|
237
|
-
awk '
|
|
238
|
-
/^review[[:space:]]*:[[:space:]]*$/ { in_review=1; next }
|
|
239
|
-
/^[^[:space:]]/ { in_review=0; in_lr=0; next }
|
|
240
|
-
in_review && /^[[:space:]]+local_review[[:space:]]*:[[:space:]]*$/ { in_lr=1; next }
|
|
241
|
-
in_lr && /^[[:space:]]{2,}refuse_at[[:space:]]*:/ { print $2; exit }
|
|
242
|
-
in_lr && /^[[:space:]]{0,2}[a-zA-Z]/ && !/^[[:space:]]+local_review/ && !/^[[:space:]]+(mode|refuse_at|bypass_env_var|max_age_seconds)/ { in_lr=0 }
|
|
243
|
-
' "$POLICY_FILE" 2>/dev/null | tr -d '"' | tr -d "'" | head -1
|
|
244
|
-
;;
|
|
245
|
-
review.local_review.bypass_env_var)
|
|
246
|
-
awk '
|
|
247
|
-
/^review[[:space:]]*:[[:space:]]*$/ { in_review=1; next }
|
|
248
|
-
/^[^[:space:]]/ { in_review=0; in_lr=0; next }
|
|
249
|
-
in_review && /^[[:space:]]+local_review[[:space:]]*:[[:space:]]*$/ { in_lr=1; next }
|
|
250
|
-
in_lr && /^[[:space:]]{2,}bypass_env_var[[:space:]]*:/ { print $2; exit }
|
|
251
|
-
in_lr && /^[[:space:]]{0,2}[a-zA-Z]/ && !/^[[:space:]]+local_review/ && !/^[[:space:]]+(mode|refuse_at|bypass_env_var|max_age_seconds)/ { in_lr=0 }
|
|
252
|
-
' "$POLICY_FILE" 2>/dev/null | tr -d '"' | tr -d "'" | head -1
|
|
169
|
+
review.local_review.*)
|
|
170
|
+
_lrg_load_local_review_subtree
|
|
171
|
+
local leaf="${key##*.}"
|
|
172
|
+
local v
|
|
173
|
+
if v=$(_lrg_subtree_leaf "$leaf"); then
|
|
174
|
+
printf '%s' "$v"
|
|
175
|
+
return 0
|
|
176
|
+
fi
|
|
253
177
|
;;
|
|
254
178
|
esac
|
|
179
|
+
policy_reader_get "$key" 2>/dev/null
|
|
255
180
|
}
|
|
256
181
|
|
|
257
|
-
# 4. Mode-off short-circuit.
|
|
258
|
-
# `policy_get_local_review_mode` check at the top — `off` → silent
|
|
259
|
-
# no-op BEFORE any other work.
|
|
182
|
+
# 4. Mode-off short-circuit.
|
|
260
183
|
LOCAL_REVIEW_MODE=$(_lrg_read_policy review.local_review.mode)
|
|
261
184
|
if [ "$LOCAL_REVIEW_MODE" = "off" ]; then
|
|
262
185
|
exit 0
|
|
263
186
|
fi
|
|
264
187
|
|
|
265
|
-
# 5. Read
|
|
266
|
-
# default `refuse_at: push`, a `git commit` segment is NOT refused
|
|
267
|
-
# by the CLI — so when the CLI is missing, the shim should let
|
|
268
|
-
# `git commit -m "..."` pass without hitting fail-closed. Mirrors
|
|
269
|
-
# the bash hook's posture: a non-refused git op does not enter
|
|
270
|
-
# the preflight-refuse branch.
|
|
188
|
+
# 5. Read refuse_at to scope the relevance pre-gate.
|
|
271
189
|
REFUSE_AT="push"
|
|
272
190
|
POLICY_REFUSE=$(_lrg_read_policy review.local_review.refuse_at)
|
|
273
191
|
case "$POLICY_REFUSE" in push|commit|both) REFUSE_AT="$POLICY_REFUSE" ;; esac
|
|
274
|
-
# Build trigger-head alternation based on refuse_at.
|
|
275
192
|
case "$REFUSE_AT" in
|
|
276
193
|
push) TRIGGER_RE='git[[:space:]]+push' ;;
|
|
277
194
|
commit) TRIGGER_RE='git[[:space:]]+commit' ;;
|
|
278
195
|
both) TRIGGER_RE='git[[:space:]]+(push|commit)' ;;
|
|
279
196
|
esac
|
|
280
197
|
|
|
281
|
-
#
|
|
282
|
-
#
|
|
283
|
-
#
|
|
284
|
-
#
|
|
285
|
-
#
|
|
286
|
-
# The check is approximate (it uses a coarse quote masker that the CLI
|
|
287
|
-
# does properly via mvdan-sh) because if it errs on the side of
|
|
288
|
-
# relevant→true, the CLI's real segment walker will sort it out. We
|
|
289
|
-
# only want to short-circuit confidently-non-relevant cases (where
|
|
290
|
-
# there's NO trigger head in any segment) so unbuilt installs don't
|
|
291
|
-
# fail closed on benign Bash calls.
|
|
292
|
-
#
|
|
293
|
-
# 0.34.0 round-2 P1 fix: the env-prefix-strip MUST accept quoted
|
|
294
|
-
# values. Pre-fix the strip pattern was
|
|
295
|
-
# `[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]+[[:space:]]+`, which silently
|
|
296
|
-
# missed shapes like `GIT_SSH_COMMAND="ssh -i ~/.ssh/id" git push`
|
|
297
|
-
# because the `[^[:space:]]+` value group stops at the first space
|
|
298
|
-
# inside the quotes. We mirror the segments.ts `matchEnvAssignLength`
|
|
299
|
-
# helper — accept value shapes `"..."`, `'...'`, `\S*` (zero-or-more
|
|
300
|
-
# so bare `FOO= cmd` resolves too). The strip runs ITERATIVELY so
|
|
301
|
-
# stacked env prefixes (`A="x" B='y' C=z git push`) all get peeled.
|
|
198
|
+
# 0.34.0 round-4 P2 fix: capture jq exit code separately rather than
|
|
199
|
+
# swallowing with `|| true`. Malformed payload pre-fix → empty PROBE →
|
|
200
|
+
# RELEVANT=0 → silent bypass. Post-fix: jq parse failure forces
|
|
201
|
+
# RELEVANT=1 so the CLI body decides (Zod fails closed on schema
|
|
202
|
+
# violations).
|
|
302
203
|
RELEVANT=0
|
|
303
204
|
PROBE=""
|
|
304
205
|
JQ_PARSE_FAILED=0
|
|
305
|
-
# 0.34.0 round-4 P2 fix: capture jq's exit code SEPARATELY rather than
|
|
306
|
-
# swallowing it with `|| true`. Malformed PreToolUse payload (invalid
|
|
307
|
-
# JSON, schema mismatch) pre-fix → empty PROBE → RELEVANT=0 fast path
|
|
308
|
-
# → silent bypass. Post-fix we distinguish:
|
|
309
|
-
# - jq exit 0 + non-empty stdout → use as PROBE (the normal path)
|
|
310
|
-
# - jq exit 0 + empty stdout → non-Bash payload / empty cmd, RELEVANT=0
|
|
311
|
-
# - jq exit != 0 (parse failure) → JQ_PARSE_FAILED=1, force RELEVANT=1
|
|
312
|
-
# so we skip the awk pre-gate and
|
|
313
|
-
# forward straight to the CLI body
|
|
314
|
-
# which fails closed on malformed
|
|
315
|
-
# payloads via Zod. Substring-only
|
|
316
|
-
# fallback was insufficient because
|
|
317
|
-
# raw JSON often won't contain
|
|
318
|
-
# `git push` literally and would
|
|
319
|
-
# still short-circuit to exit 0.
|
|
320
206
|
if command -v jq >/dev/null 2>&1; then
|
|
321
207
|
PROBE=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null)
|
|
322
208
|
jq_status=$?
|
|
@@ -324,32 +210,28 @@ if command -v jq >/dev/null 2>&1; then
|
|
|
324
210
|
JQ_PARSE_FAILED=1
|
|
325
211
|
fi
|
|
326
212
|
else
|
|
327
|
-
# 0.34.0 round-6 P1 fix: pre-fix the shim set `PROBE="$INPUT"` (
|
|
328
|
-
#
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
# `{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}`
|
|
332
|
-
# → the `^git push` anchor never matched → RELEVANT=0 → silent
|
|
333
|
-
# bypass on every jq-less machine. Fix: treat jq-missing the same
|
|
334
|
-
# as a parse failure — force RELEVANT=1 and let the CLI body decide.
|
|
335
|
-
# The CLI uses native Node JSON parsing so jq is not required for
|
|
336
|
-
# the actual enforcement.
|
|
213
|
+
# 0.34.0 round-6 P1 fix: pre-fix the shim set `PROBE="$INPUT"` (raw
|
|
214
|
+
# JSON payload) when jq was missing, then ran the awk relevance scan
|
|
215
|
+
# over JSON instead of a bare command. Fix: treat jq-missing the
|
|
216
|
+
# same as a parse failure — force RELEVANT=1 and let the CLI decide.
|
|
337
217
|
JQ_PARSE_FAILED=1
|
|
338
218
|
fi
|
|
339
|
-
# Split on shell separators then look for a segment whose head is
|
|
340
|
-
#
|
|
341
|
-
#
|
|
342
|
-
#
|
|
343
|
-
#
|
|
219
|
+
# Split on shell separators then look for a segment whose head is the
|
|
220
|
+
# configured trigger. The awk here masks chars inside "..." and '...'
|
|
221
|
+
# spans before splitting — same posture as the CLI splitSegments but
|
|
222
|
+
# coarser (no nested-shell unwrap; the CLI handles that). For
|
|
223
|
+
# relevance-pre-gate purposes the masker is sufficient.
|
|
224
|
+
#
|
|
225
|
+
# IMPORTANT: the env-prefix strip runs on the UNMASKED `seg` so the
|
|
226
|
+
# value's original quote characters are still present. Strip patterns
|
|
227
|
+
# accept quoted ("...", '...') AND unquoted (\S*) values so quoted env
|
|
228
|
+
# prefixes don't hide the trigger.
|
|
344
229
|
#
|
|
345
|
-
#
|
|
346
|
-
#
|
|
347
|
-
#
|
|
348
|
-
#
|
|
349
|
-
#
|
|
350
|
-
# Round-4 P2: if jq couldn't parse the payload, skip the awk pre-gate
|
|
351
|
-
# entirely and force RELEVANT=1 so the CLI body decides. The CLI's Zod
|
|
352
|
-
# parser fails closed on schema violations.
|
|
230
|
+
# 0.34.0 round-2 P1: env-prefix strip MUST accept quoted values.
|
|
231
|
+
# 0.34.0 round-5 P1: iteratively strip stacked env prefixes AND
|
|
232
|
+
# keyword prefixes (sudo / time / etc).
|
|
233
|
+
# 0.34.0 round-6 P2: only force relevance on shell-wrappers when a
|
|
234
|
+
# -c-class flag is present (so `bash scripts/setup.sh` doesn't trip).
|
|
353
235
|
if [ "$JQ_PARSE_FAILED" -eq 1 ]; then
|
|
354
236
|
RELEVANT=1
|
|
355
237
|
elif [ -n "$PROBE" ]; then
|
|
@@ -386,19 +268,14 @@ elif [ -n "$PROBE" ]; then
|
|
|
386
268
|
' | tr ';|&' '\n\n\n' | awk -v trigger="^${TRIGGER_RE}([[:space:]]|$)" '
|
|
387
269
|
{
|
|
388
270
|
seg = $0
|
|
389
|
-
# Strip leading whitespace and common prefixes (sudo, exec,
|
|
390
|
-
# time, VAR=value). Coarse — the CLI does this properly.
|
|
391
271
|
sub(/^[[:space:]]+/, "", seg)
|
|
392
272
|
# Iteratively strip env-var assignment prefix VAR=<value> +
|
|
393
|
-
# one-or-more spaces. <value> may be a double-quoted string,
|
|
394
|
-
#
|
|
395
|
-
#
|
|
396
|
-
#
|
|
397
|
-
#
|
|
398
|
-
#
|
|
399
|
-
# "awk: syntax error" at runtime, swallowed by `|| true`.
|
|
400
|
-
# Try quoted shapes first; bare last. Run until no more prefixes
|
|
401
|
-
# match (POSIX-legal stacked-env-prefix support).
|
|
273
|
+
# one-or-more spaces. <value> may be a double-quoted string, a
|
|
274
|
+
# single-quoted string, or a bare token (zero-or-more non-space
|
|
275
|
+
# chars). Quote characters in this comment are intentionally
|
|
276
|
+
# avoided — see round-4 P1 fix: a literal single-quote inside an
|
|
277
|
+
# awk comment inside a single-quoted shell heredoc terminates
|
|
278
|
+
# the bash string and causes "awk: syntax error" at runtime.
|
|
402
279
|
changed = 1
|
|
403
280
|
while (changed) {
|
|
404
281
|
changed = 0
|
|
@@ -412,11 +289,6 @@ elif [ -n "$PROBE" ]; then
|
|
|
412
289
|
seg = substr(seg, RLENGTH + 1); changed = 1; continue
|
|
413
290
|
}
|
|
414
291
|
}
|
|
415
|
-
# Iteratively strip keyword prefixes. Round-5 P1 fix: the pre-
|
|
416
|
-
# fix `sub` only stripped ONE keyword, so `time sudo git push`
|
|
417
|
-
# left `sudo git push` and missed the trigger. Loop until no
|
|
418
|
-
# more keyword prefixes match. Coarse — the CLI does this
|
|
419
|
-
# properly with full builtin-tokenization.
|
|
420
292
|
kchanged = 1
|
|
421
293
|
while (kchanged) {
|
|
422
294
|
kchanged = 0
|
|
@@ -424,35 +296,15 @@ elif [ -n "$PROBE" ]; then
|
|
|
424
296
|
kchanged = 1
|
|
425
297
|
}
|
|
426
298
|
}
|
|
427
|
-
# Round-5 P1
|
|
428
|
-
#
|
|
429
|
-
#
|
|
430
|
-
#
|
|
431
|
-
#
|
|
432
|
-
# bypass. The CLI does full nested-shell unwrapping via
|
|
433
|
-
# mvdan-sh; the shim should not try to compete.
|
|
434
|
-
#
|
|
435
|
-
# Round-6 P2 fix: the round-5 pattern matched ANY segment
|
|
436
|
-
# whose head started with a shell name, including benign
|
|
437
|
-
# bash-script-execution like `bash scripts/setup.sh`. That
|
|
438
|
-
# hit the fail-closed branch on unbuilt installs with "rea
|
|
439
|
-
# CLI is not built", even though the pre-0.34 hook only
|
|
440
|
-
# gated actual git push / git commit commands. Fix: require
|
|
441
|
-
# a -c-class flag (combined form -c, -lc, -lic, -cl, -cli,
|
|
442
|
-
# -li, -il, -ic — the bash WRAP pattern set) OR a separated
|
|
443
|
-
# --c flag, before forcing relevance.
|
|
444
|
-
# IMPORTANT: comments here avoid bare single-quote characters
|
|
445
|
-
# to prevent terminating the surrounding bash single-quoted
|
|
446
|
-
# string at runtime — see round-4 P1 lesson (awk: syntax
|
|
447
|
-
# error swallowed by `|| true`).
|
|
299
|
+
# Round-5 P1 + round-6 P2: if the head is a shell wrapper WITH a
|
|
300
|
+
# -c-class flag, FORCE relevance and let the CLI walk the payload.
|
|
301
|
+
# Comments here avoid bare single-quote characters to prevent
|
|
302
|
+
# terminating the surrounding bash single-quoted string at
|
|
303
|
+
# runtime — see round-4 P1 lesson.
|
|
448
304
|
if (match(seg, /^(bash|sh|zsh|dash|ksh|mksh|oksh|posh|yash|csh|tcsh|fish)[[:space:]]+(-([a-z]*c[a-z]*)|--c)([[:space:]]|$)/)) {
|
|
449
305
|
print "1"
|
|
450
306
|
exit
|
|
451
307
|
}
|
|
452
|
-
# Pre-flag variants: bash -l -c PAYLOAD, bash --noprofile -c
|
|
453
|
-
# PAYLOAD. Match shell then one-or-more flags then a -c-class
|
|
454
|
-
# flag. Comments deliberately have no inline quotes (round-4
|
|
455
|
-
# P1 lesson).
|
|
456
308
|
if (match(seg, /^(bash|sh|zsh|dash|ksh|mksh|oksh|posh|yash|csh|tcsh|fish)([[:space:]]+(-[a-z]+|--[a-z]+))+[[:space:]]+(-([a-z]*c[a-z]*)|--c)([[:space:]]|$)/)) {
|
|
457
309
|
print "1"
|
|
458
310
|
exit
|
|
@@ -464,110 +316,53 @@ elif [ -n "$PROBE" ]; then
|
|
|
464
316
|
}
|
|
465
317
|
END { print "0" }
|
|
466
318
|
' | head -1)
|
|
467
|
-
# Fallback for environments without awk (vanishingly rare on the
|
|
468
|
-
# platforms rea supports): default to relevant=1 — over-trigger is
|
|
469
|
-
# safer than under-trigger.
|
|
470
319
|
case "$RELEVANT" in 0|1) ;; *) RELEVANT=1 ;; esac
|
|
471
320
|
fi
|
|
472
321
|
if [ "$RELEVANT" -eq 0 ]; then
|
|
473
322
|
exit 0
|
|
474
323
|
fi
|
|
475
324
|
|
|
476
|
-
# 6. Bypass env-var short-circuit.
|
|
477
|
-
#
|
|
478
|
-
# var) BEFORE invoking preflight. We mirror that here so an
|
|
479
|
-
# audited bypass works even when the CLI isn't built.
|
|
480
|
-
#
|
|
481
|
-
# Policy-driven var name: read `policy.review.local_review.bypass_env_var`
|
|
482
|
-
# if present; default to `REA_SKIP_LOCAL_REVIEW`. The CLI does its
|
|
483
|
-
# own per-segment inline-bypass evaluation; the shim only checks
|
|
484
|
-
# the operator-exported (process-env) form.
|
|
325
|
+
# 6. Bypass env-var short-circuit. Policy-driven var name; default
|
|
326
|
+
# REA_SKIP_LOCAL_REVIEW. Only honor POSIX-identifier-shaped names.
|
|
485
327
|
BYPASS_VAR="REA_SKIP_LOCAL_REVIEW"
|
|
486
328
|
POLICY_VAR=$(_lrg_read_policy review.local_review.bypass_env_var)
|
|
487
|
-
# Only honor POSIX-identifier-shaped names. Junk falls back to default.
|
|
488
329
|
if printf '%s' "$POLICY_VAR" | grep -qE '^[A-Za-z_][A-Za-z0-9_]*$'; then
|
|
489
330
|
BYPASS_VAR="$POLICY_VAR"
|
|
490
331
|
fi
|
|
491
|
-
# Read the configured env-var via indirect expansion (bash 3.2 compatible).
|
|
492
332
|
BYPASS_VALUE="${!BYPASS_VAR:-}"
|
|
493
333
|
if [ -n "$BYPASS_VALUE" ]; then
|
|
494
|
-
# Operator-exported bypass — allow. The CLI's per-segment inline
|
|
495
|
-
# bypass and multi-trigger laundering defense run when the CLI is
|
|
496
|
-
# reached; this shim short-circuit only covers the global
|
|
497
|
-
# process-env shape.
|
|
498
334
|
exit 0
|
|
499
335
|
fi
|
|
500
336
|
|
|
501
|
-
# 7. CLI
|
|
502
|
-
#
|
|
503
|
-
#
|
|
504
|
-
# the early sandbox check (round-5 P1) cleared them. Distinguish.
|
|
337
|
+
# 7. CLI required. If REA_ARGV is empty either (a) the CLI wasn't
|
|
338
|
+
# built/installed, OR (b) the early sandbox check cleared it.
|
|
339
|
+
# Distinguish.
|
|
505
340
|
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
506
|
-
if [ -n "$
|
|
507
|
-
|
|
341
|
+
if [ -n "$SANDBOX_EARLY_FAILURE" ]; then
|
|
342
|
+
shim_emit_sandbox_failure_banner "$SANDBOX_EARLY_FAILURE"
|
|
508
343
|
exit 2
|
|
509
344
|
fi
|
|
510
|
-
|
|
511
|
-
printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
|
|
512
|
-
printf 'This shim fails closed because the pre-0.34.0 bash body enforced local-first review without a CLI.\n' >&2
|
|
345
|
+
shim_emit_cli_missing_banner
|
|
513
346
|
exit 2
|
|
514
347
|
fi
|
|
515
348
|
|
|
516
|
-
# 8.
|
|
349
|
+
# 8. (Redundant on the success path — the early sandbox already passed
|
|
350
|
+
# and cleared REA_ARGV on failure — but we re-emit the node-missing
|
|
351
|
+
# banner explicitly because node could have disappeared between
|
|
352
|
+
# section 3 and now in pathological setups.)
|
|
517
353
|
if ! command -v node >/dev/null 2>&1; then
|
|
518
|
-
|
|
519
|
-
printf 'Install Node 22+ (engines.node) to restore local-first review enforcement.\n' >&2
|
|
520
|
-
exit 2
|
|
521
|
-
fi
|
|
522
|
-
|
|
523
|
-
sandbox_check=$(node -e '
|
|
524
|
-
const fs = require("fs");
|
|
525
|
-
const path = require("path");
|
|
526
|
-
const cli = process.argv[1];
|
|
527
|
-
const projDir = process.argv[2];
|
|
528
|
-
let real, realProj;
|
|
529
|
-
try { real = fs.realpathSync(cli); } catch (e) {
|
|
530
|
-
process.stdout.write("bad:realpath"); process.exit(1);
|
|
531
|
-
}
|
|
532
|
-
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
533
|
-
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
534
|
-
}
|
|
535
|
-
const sep = path.sep;
|
|
536
|
-
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
537
|
-
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
538
|
-
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
539
|
-
}
|
|
540
|
-
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
541
|
-
let found = false;
|
|
542
|
-
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
543
|
-
const pj = path.join(cur, "package.json");
|
|
544
|
-
if (fs.existsSync(pj)) {
|
|
545
|
-
try {
|
|
546
|
-
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
547
|
-
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
548
|
-
} catch (e) { /* keep walking */ }
|
|
549
|
-
}
|
|
550
|
-
cur = path.dirname(cur);
|
|
551
|
-
}
|
|
552
|
-
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
553
|
-
process.stdout.write("ok");
|
|
554
|
-
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
555
|
-
|
|
556
|
-
if [ "$sandbox_check" != "ok" ]; then
|
|
557
|
-
printf 'rea: local-review-gate FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
|
|
354
|
+
shim_emit_node_missing_banner
|
|
558
355
|
exit 2
|
|
559
356
|
fi
|
|
560
357
|
|
|
561
|
-
# 9. Version
|
|
562
|
-
probe_out=$("${REA_ARGV[@]}" hook
|
|
358
|
+
# 9. Version probe.
|
|
359
|
+
probe_out=$("${REA_ARGV[@]}" hook "$SHIM_NAME" --help 2>&1)
|
|
563
360
|
probe_status=$?
|
|
564
|
-
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e
|
|
565
|
-
|
|
566
|
-
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
567
|
-
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
|
|
361
|
+
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e "$SHIM_NAME"; then
|
|
362
|
+
shim_emit_version_skew_banner_blocking
|
|
568
363
|
exit 2
|
|
569
364
|
fi
|
|
570
365
|
|
|
571
|
-
# 10. Forward stdin
|
|
572
|
-
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook
|
|
366
|
+
# 10. Forward stdin.
|
|
367
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook "$SHIM_NAME"
|
|
573
368
|
exit $?
|