@bookedsolid/rea 0.31.0 → 0.33.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/.husky/prepare-commit-msg +80 -6
- package/MIGRATING.md +24 -15
- package/dist/cli/hook.js +60 -22
- package/dist/hooks/_lib/halt-check.d.ts +78 -0
- package/dist/hooks/_lib/halt-check.js +106 -0
- package/dist/hooks/_lib/payload.d.ts +124 -0
- package/dist/hooks/_lib/payload.js +245 -0
- package/dist/hooks/_lib/segments.d.ts +125 -0
- package/dist/hooks/_lib/segments.js +766 -0
- package/dist/hooks/architecture-review-gate/index.d.ts +58 -0
- package/dist/hooks/architecture-review-gate/index.js +250 -0
- package/dist/hooks/attribution-advisory/index.d.ts +72 -0
- package/dist/hooks/attribution-advisory/index.js +233 -0
- package/dist/hooks/bash-scanner/protected-scan.js +14 -2
- package/dist/hooks/changeset-security-gate/index.d.ts +71 -0
- package/dist/hooks/changeset-security-gate/index.js +330 -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/pr-issue-link-gate/index.d.ts +91 -0
- package/dist/hooks/pr-issue-link-gate/index.js +127 -0
- package/dist/hooks/security-disclosure-gate/index.d.ts +91 -0
- package/dist/hooks/security-disclosure-gate/index.js +502 -0
- package/hooks/_lib/protected-paths.sh +10 -3
- package/hooks/architecture-review-gate.sh +92 -77
- package/hooks/attribution-advisory.sh +139 -131
- package/hooks/changeset-security-gate.sh +114 -149
- package/hooks/dependency-audit-gate.sh +115 -156
- package/hooks/env-file-protection.sh +130 -97
- package/hooks/pr-issue-link-gate.sh +114 -45
- package/hooks/security-disclosure-gate.sh +148 -316
- package/hooks/settings-protection.sh +13 -9
- package/package.json +1 -1
- package/templates/architecture-review-gate.dogfood-staged.sh +116 -0
- package/templates/attribution-advisory.dogfood-staged.sh +170 -0
- package/templates/changeset-security-gate.dogfood-staged.sh +137 -0
- package/templates/dependency-audit-gate.dogfood-staged.sh +138 -0
- package/templates/env-file-protection.dogfood-staged.sh +157 -0
- package/templates/pr-issue-link-gate.dogfood-staged.sh +134 -0
- package/templates/prepare-commit-msg.husky.sh +80 -6
- package/templates/security-disclosure-gate.dogfood-staged.sh +171 -0
- package/templates/settings-protection.dogfood.patch +58 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: env-file-protection.sh
|
|
3
|
+
# 0.33.0+ — Node-binary shim for `rea hook env-file-protection`.
|
|
4
|
+
#
|
|
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.
|
|
11
|
+
#
|
|
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).
|
|
15
|
+
#
|
|
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.
|
|
31
|
+
|
|
32
|
+
set -uo pipefail
|
|
33
|
+
|
|
34
|
+
# 1. HALT check.
|
|
35
|
+
# shellcheck source=_lib/halt-check.sh
|
|
36
|
+
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
37
|
+
check_halt
|
|
38
|
+
REA_ROOT=$(rea_root)
|
|
39
|
+
|
|
40
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
41
|
+
|
|
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
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
|
|
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
|
|
89
|
+
|
|
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
|
|
99
|
+
|
|
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
|
|
105
|
+
fi
|
|
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
|
|
142
|
+
exit 2
|
|
143
|
+
fi
|
|
144
|
+
|
|
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
|
|
152
|
+
exit 2
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
# 6. Forward stdin (already captured up-front for the relevance gate).
|
|
156
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook env-file-protection
|
|
157
|
+
exit $?
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: pr-issue-link-gate.sh
|
|
3
|
+
# 0.32.0+ — Node-binary shim for `rea hook pr-issue-link-gate`.
|
|
4
|
+
#
|
|
5
|
+
# Pre-0.32.0 the gate's full body lived here as bash; the migration to
|
|
6
|
+
# the parser-backed Node binary moves the matching + advisory logic
|
|
7
|
+
# into `src/hooks/pr-issue-link-gate/index.ts`. This shim is the
|
|
8
|
+
# Claude Code dispatcher's view of the hook — it forwards stdin to the
|
|
9
|
+
# CLI and exits with whatever the CLI returns.
|
|
10
|
+
#
|
|
11
|
+
# Behavioral contract is preserved byte-for-byte: ALWAYS exit 0 except
|
|
12
|
+
# under HALT (exit 2) or a malformed payload (exit 2, fail-closed).
|
|
13
|
+
#
|
|
14
|
+
# # CLI-resolution trust boundary
|
|
15
|
+
#
|
|
16
|
+
# Codex round 1 P1 (2026-05-15): mirrors the realpath sandbox check
|
|
17
|
+
# from `delegation-advisory.sh` §3 and `protected-paths-bash-gate.sh`
|
|
18
|
+
# §6. The resolved CLI MUST live INSIDE realpath(CLAUDE_PROJECT_DIR)
|
|
19
|
+
# AND have an ancestor `package.json` whose `name` is
|
|
20
|
+
# `@bookedsolid/rea`. Pre-fix the shim executed
|
|
21
|
+
# `node_modules/@bookedsolid/rea/dist/cli/index.js` directly without
|
|
22
|
+
# realpathing the target, which would let an attacker who controlled
|
|
23
|
+
# `node_modules/@bookedsolid/rea` (symlink-out, postinstall script,
|
|
24
|
+
# tarball-replacement) ship forged review code that intercepts every
|
|
25
|
+
# Bash dispatch.
|
|
26
|
+
#
|
|
27
|
+
# Sandboxed resolution order (PATH is INTENTIONALLY OMITTED):
|
|
28
|
+
# 1. node_modules/@bookedsolid/rea/dist/cli/index.js (consumer-side)
|
|
29
|
+
# 2. dist/cli/index.js under CLAUDE_PROJECT_DIR (dogfood)
|
|
30
|
+
#
|
|
31
|
+
# When NO rea CLI is reachable through the sandboxed order, this hook
|
|
32
|
+
# falls through to allow (exit 0) — the advisory is a nudge, not a
|
|
33
|
+
# security claim. The bash-tier path gates fail-closed because they
|
|
34
|
+
# protect write surfaces; this gate only emits prose.
|
|
35
|
+
#
|
|
36
|
+
# # Version skew
|
|
37
|
+
#
|
|
38
|
+
# Codex round 1 P1 (2026-05-15): a fresh `rea init` against a stale
|
|
39
|
+
# `node_modules/@bookedsolid/rea` would deliver this 0.32.0 shim while
|
|
40
|
+
# the installed CLI lacks the `hook pr-issue-link-gate` subcommand —
|
|
41
|
+
# every Bash dispatch would then fail with `unknown command` (exit 1).
|
|
42
|
+
# Probe the subcommand's `--help` output before propagating the exit
|
|
43
|
+
# code; on probe failure, advise the operator to `pnpm install` and
|
|
44
|
+
# fall through silently so the workspace stays usable.
|
|
45
|
+
|
|
46
|
+
set -uo pipefail
|
|
47
|
+
|
|
48
|
+
# 1. HALT check. Even though the CLI re-checks for defense-in-depth,
|
|
49
|
+
# short-circuit here so we never spawn `node` while frozen.
|
|
50
|
+
# shellcheck source=_lib/halt-check.sh
|
|
51
|
+
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
52
|
+
check_halt
|
|
53
|
+
REA_ROOT=$(rea_root)
|
|
54
|
+
|
|
55
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
56
|
+
|
|
57
|
+
# 2. Resolve the rea CLI through the fixed 2-tier sandboxed order.
|
|
58
|
+
REA_ARGV=()
|
|
59
|
+
RESOLVED_CLI_PATH=""
|
|
60
|
+
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
61
|
+
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
62
|
+
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
63
|
+
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
64
|
+
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
65
|
+
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# 3. Realpath sandbox check — mirrors delegation-advisory.sh §3.
|
|
73
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
74
|
+
exit 0
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
sandbox_check=$(node -e '
|
|
78
|
+
const fs = require("fs");
|
|
79
|
+
const path = require("path");
|
|
80
|
+
const cli = process.argv[1];
|
|
81
|
+
const projDir = process.argv[2];
|
|
82
|
+
let real, realProj;
|
|
83
|
+
try { real = fs.realpathSync(cli); } catch (e) {
|
|
84
|
+
process.stdout.write("bad:realpath"); process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
87
|
+
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
const sep = path.sep;
|
|
90
|
+
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
91
|
+
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
92
|
+
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
95
|
+
let found = false;
|
|
96
|
+
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
97
|
+
const pj = path.join(cur, "package.json");
|
|
98
|
+
if (fs.existsSync(pj)) {
|
|
99
|
+
try {
|
|
100
|
+
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
101
|
+
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
102
|
+
} catch (e) { /* keep walking */ }
|
|
103
|
+
}
|
|
104
|
+
cur = path.dirname(cur);
|
|
105
|
+
}
|
|
106
|
+
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
107
|
+
process.stdout.write("ok");
|
|
108
|
+
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
109
|
+
|
|
110
|
+
if [ "$sandbox_check" != "ok" ]; then
|
|
111
|
+
printf 'rea: pr-issue-link-gate skipped (sandbox check: %s)\n' "$sandbox_check" >&2
|
|
112
|
+
exit 0
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# 4. Version-probe: confirm the resolved CLI implements the
|
|
116
|
+
# `hook pr-issue-link-gate` subcommand. A stale node_modules from
|
|
117
|
+
# a fresh `rea init` against an older installed version would
|
|
118
|
+
# otherwise turn every Bash dispatch into a hard failure.
|
|
119
|
+
probe_out=$("${REA_ARGV[@]}" hook pr-issue-link-gate --help 2>&1)
|
|
120
|
+
probe_status=$?
|
|
121
|
+
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'pr-issue-link-gate'; then
|
|
122
|
+
printf 'rea: this shim requires the `rea hook pr-issue-link-gate` subcommand (introduced in 0.32.0).\n' >&2
|
|
123
|
+
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
124
|
+
printf 'Run `pnpm install` (or `npm install`) to sync the CLI to the version this shim expects.\n' >&2
|
|
125
|
+
exit 0
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# 5. Forward stdin to the CLI synchronously. The advisory text must
|
|
129
|
+
# reach the operator's stderr before this hook returns; the CLI's
|
|
130
|
+
# own exit code is the hook's exit code (0 normally, 2 under HALT
|
|
131
|
+
# or malformed payload).
|
|
132
|
+
INPUT=$(cat)
|
|
133
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook pr-issue-link-gate
|
|
134
|
+
exit $?
|
|
@@ -36,28 +36,65 @@ set -u
|
|
|
36
36
|
COMMIT_MSG_FILE="${1:-}"
|
|
37
37
|
COMMIT_SOURCE="${2:-}"
|
|
38
38
|
|
|
39
|
+
REA_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
40
|
+
|
|
41
|
+
# Forward declaration — the extension-chain runner is defined further
|
|
42
|
+
# down (after $REA_ROOT is set so the dir lookup is anchored). We call
|
|
43
|
+
# it from every "augmenter skipped" exit point so consumer fragments
|
|
44
|
+
# under .husky/prepare-commit-msg.d/* run regardless of whether rea's
|
|
45
|
+
# own augmenter ran. The function fires fragments in lex order,
|
|
46
|
+
# logs-and-continues on non-zero exits, and is a no-op if the dir is
|
|
47
|
+
# absent or empty.
|
|
48
|
+
#
|
|
49
|
+
# 0.32.0 Phase 3: the pre-0.32.0 layout exited early at every
|
|
50
|
+
# precondition gate, which made the extension surface unreachable
|
|
51
|
+
# when (a) attribution was disabled, (b) HALT was active, or (c)
|
|
52
|
+
# REA_SKIP_ATTRIBUTION was set. The new layout runs the chain at the
|
|
53
|
+
# end of every exit path EXCEPT when the message file itself is
|
|
54
|
+
# missing/unparseable (no point running fragments against a path that
|
|
55
|
+
# doesn't exist).
|
|
56
|
+
run_extension_chain() {
|
|
57
|
+
ext_dir="${REA_ROOT}/.husky/prepare-commit-msg.d"
|
|
58
|
+
if [ -d "$ext_dir" ]; then
|
|
59
|
+
for frag in "$ext_dir"/*; do
|
|
60
|
+
[ -e "$frag" ] || continue
|
|
61
|
+
[ -f "$frag" ] || continue
|
|
62
|
+
[ -x "$frag" ] || continue
|
|
63
|
+
if ! "$frag" "$COMMIT_MSG_FILE" "$COMMIT_SOURCE"; then
|
|
64
|
+
printf 'rea: prepare-commit-msg.d fragment exited non-zero: %s (continuing)\n' \
|
|
65
|
+
"$(basename "$frag")" >&2
|
|
66
|
+
fi
|
|
67
|
+
done
|
|
68
|
+
fi
|
|
69
|
+
}
|
|
70
|
+
|
|
39
71
|
# Skip conditions: any missing precondition exits 0 silently. The hook
|
|
40
72
|
# is purely additive; refusing here would break commits with no upside.
|
|
41
73
|
|
|
42
|
-
# Missing message file → nothing to augment
|
|
74
|
+
# Missing message file → nothing to augment AND nothing for fragments
|
|
75
|
+
# to act on either. Exit immediately without running the chain.
|
|
43
76
|
if [ -z "$COMMIT_MSG_FILE" ] || [ ! -f "$COMMIT_MSG_FILE" ]; then
|
|
44
77
|
exit 0
|
|
45
78
|
fi
|
|
46
79
|
|
|
47
|
-
# Per-invocation override
|
|
80
|
+
# Per-invocation override — skip the augmenter, but still run consumer
|
|
81
|
+
# fragments. The flag is named REA_SKIP_ATTRIBUTION, not REA_SKIP_HOOK,
|
|
82
|
+
# precisely so the rest of the chain runs.
|
|
48
83
|
if [ -n "${REA_SKIP_ATTRIBUTION:-}" ]; then
|
|
84
|
+
run_extension_chain
|
|
49
85
|
exit 0
|
|
50
86
|
fi
|
|
51
87
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
#
|
|
88
|
+
# HALT kill switch — refuse to mutate anything while frozen. The
|
|
89
|
+
# extension chain is also skipped under HALT: a frozen system means
|
|
90
|
+
# "no agent-side actions" and consumer fragments are agent-side too.
|
|
55
91
|
if [ -f "${REA_ROOT}/.rea/HALT" ]; then
|
|
56
92
|
exit 0
|
|
57
93
|
fi
|
|
58
94
|
|
|
59
95
|
POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
|
|
60
96
|
if [ ! -f "$POLICY_FILE" ]; then
|
|
97
|
+
run_extension_chain
|
|
61
98
|
exit 0
|
|
62
99
|
fi
|
|
63
100
|
|
|
@@ -172,6 +209,7 @@ print(enabled); print(name); print(email); print(skip_merge)
|
|
|
172
209
|
PY
|
|
173
210
|
)
|
|
174
211
|
if [ -z "$CO_AUTHOR_PARSE" ]; then
|
|
212
|
+
run_extension_chain
|
|
175
213
|
exit 0
|
|
176
214
|
fi
|
|
177
215
|
ENABLED=$(printf '%s\n' "$CO_AUTHOR_PARSE" | sed -n '1p')
|
|
@@ -179,11 +217,15 @@ PY
|
|
|
179
217
|
CO_EMAIL=$(printf '%s\n' "$CO_AUTHOR_PARSE" | sed -n '3p')
|
|
180
218
|
SKIP_MERGE=$(printf '%s\n' "$CO_AUTHOR_PARSE" | sed -n '4p')
|
|
181
219
|
else
|
|
182
|
-
# Neither rea CLI nor python3 reachable — silent no-op
|
|
220
|
+
# Neither rea CLI nor python3 reachable — silent no-op for the
|
|
221
|
+
# augmenter, but still run consumer fragments. The chain doesn't
|
|
222
|
+
# need policy values; it just runs `.husky/prepare-commit-msg.d/*`.
|
|
223
|
+
run_extension_chain
|
|
183
224
|
exit 0
|
|
184
225
|
fi
|
|
185
226
|
|
|
186
227
|
if [ "$ENABLED" != "true" ]; then
|
|
228
|
+
run_extension_chain
|
|
187
229
|
exit 0
|
|
188
230
|
fi
|
|
189
231
|
|
|
@@ -204,11 +246,13 @@ if [ -z "$CO_NAME" ] || [ -z "$CO_EMAIL" ]; then
|
|
|
204
246
|
"$([ -z "$CO_NAME" ] && [ -z "$CO_EMAIL" ] && printf '+')" \
|
|
205
247
|
"$([ -z "$CO_EMAIL" ] && printf email)" >&2
|
|
206
248
|
printf 'rea: edit .rea/policy.yaml — set name + email, OR set enabled: false.\n' >&2
|
|
249
|
+
run_extension_chain
|
|
207
250
|
exit 0
|
|
208
251
|
fi
|
|
209
252
|
|
|
210
253
|
# skip_merge: true → skip when commit source is 'merge'.
|
|
211
254
|
if [ "$SKIP_MERGE" = "true" ] && [ "$COMMIT_SOURCE" = "merge" ]; then
|
|
255
|
+
run_extension_chain
|
|
212
256
|
exit 0
|
|
213
257
|
fi
|
|
214
258
|
|
|
@@ -226,6 +270,7 @@ LOWER_EMAIL=$(printf '%s' "$CO_EMAIL" | tr '[:upper:]' '[:lower:]')
|
|
|
226
270
|
ESCAPED_EMAIL=$(printf '%s' "$LOWER_EMAIL" | sed 's/[.[\*^$(){}+?|]/\\&/g')
|
|
227
271
|
if grep -iE "^co-authored-by:[[:space:]]*[^<]*<${ESCAPED_EMAIL}>[[:space:]]*$" \
|
|
228
272
|
"$COMMIT_MSG_FILE" >/dev/null 2>&1; then
|
|
273
|
+
run_extension_chain
|
|
229
274
|
exit 0
|
|
230
275
|
fi
|
|
231
276
|
|
|
@@ -311,4 +356,33 @@ awk '
|
|
|
311
356
|
} > "${COMMIT_MSG_FILE}.rea-tmp" && mv "${COMMIT_MSG_FILE}.rea-tmp" "$COMMIT_MSG_FILE"
|
|
312
357
|
|
|
313
358
|
rm -f "$TMP_BODY_TRIMMED"
|
|
359
|
+
|
|
360
|
+
# ── Extension-hook chaining ───────────────────────────────────────────────────
|
|
361
|
+
# 0.32.0 — `.husky/prepare-commit-msg.d/*` extension surface mirrors
|
|
362
|
+
# the `.husky/commit-msg.d/*` and `.husky/pre-push.d/*` patterns from
|
|
363
|
+
# 0.13.0. Source every executable file under
|
|
364
|
+
# `.husky/prepare-commit-msg.d/` in lexical order. Missing directory
|
|
365
|
+
# is a no-op (backward compatible). Each fragment receives the same
|
|
366
|
+
# `$1` (commit message file path) and `$2` (commit source) that git
|
|
367
|
+
# delivered to this hook so consumers can layer on their own
|
|
368
|
+
# augmenters (lint-staged --on-prepare, branch-name-injection,
|
|
369
|
+
# ticket-reference-prepend, …) without losing rea coverage.
|
|
370
|
+
#
|
|
371
|
+
# Fragments run AFTER rea's attribution augmenter so the
|
|
372
|
+
# `Co-Authored-By` trailer is already in the file before any consumer
|
|
373
|
+
# fragment reads it; that lets a fragment reorder trailers, dedupe,
|
|
374
|
+
# or run its own template substitution against the augmented body.
|
|
375
|
+
#
|
|
376
|
+
# A non-zero exit from a fragment does NOT fail the commit — this
|
|
377
|
+
# hook is purely additive (its bash counterpart `commit-msg` is the
|
|
378
|
+
# blocking gate). We log the failure to stderr and continue so a
|
|
379
|
+
# broken consumer fragment can't take down `git commit`.
|
|
380
|
+
#
|
|
381
|
+
# The actual chain body lives in `run_extension_chain` (defined near
|
|
382
|
+
# the top of the file). The reason for the early definition: several
|
|
383
|
+
# augmenter-skip exit paths (enabled: false, missing identity, idempo-
|
|
384
|
+
# tency hit, skip_merge match) need to run the chain too, so consumer
|
|
385
|
+
# fragments fire regardless of whether rea's own augmenter activated.
|
|
386
|
+
run_extension_chain
|
|
387
|
+
|
|
314
388
|
exit 0
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: security-disclosure-gate.sh
|
|
3
|
+
# 0.32.0+ — Node-binary shim for `rea hook security-disclosure-gate`.
|
|
4
|
+
#
|
|
5
|
+
# Pre-0.32.0 the gate's full body lived here as bash (339 LOC including
|
|
6
|
+
# the awk body-file resolver, security-patterns array, and mode-aware
|
|
7
|
+
# routing). The migration to the parser-backed Node binary moves all of
|
|
8
|
+
# that into `src/hooks/security-disclosure-gate/index.ts`. This shim is
|
|
9
|
+
# the Claude Code dispatcher's view of the hook — it forwards stdin
|
|
10
|
+
# AND the REA_DISCLOSURE_MODE env var to the CLI and exits with
|
|
11
|
+
# whatever the CLI returns.
|
|
12
|
+
#
|
|
13
|
+
# Behavioral contract is preserved byte-for-byte: exit 0 on
|
|
14
|
+
# pass-through / no-match, exit 2 on HALT / pattern match / traversal
|
|
15
|
+
# refusal / malformed payload (fail-closed).
|
|
16
|
+
#
|
|
17
|
+
# # CLI-resolution trust boundary
|
|
18
|
+
#
|
|
19
|
+
# Codex round 1 P1 (2026-05-15): realpath sandbox check matches
|
|
20
|
+
# delegation-advisory.sh §3. The resolved CLI MUST live INSIDE
|
|
21
|
+
# realpath(CLAUDE_PROJECT_DIR) AND have an ancestor `package.json`
|
|
22
|
+
# whose `name` is `@bookedsolid/rea`. Defends against symlink-out
|
|
23
|
+
# and tarball-replacement attacks that could otherwise forge the
|
|
24
|
+
# pattern matcher and either suppress real findings or leak a
|
|
25
|
+
# vulnerability through the disclosure gate.
|
|
26
|
+
#
|
|
27
|
+
# Sandboxed resolution order (PATH is INTENTIONALLY OMITTED):
|
|
28
|
+
# 1. node_modules/@bookedsolid/rea/dist/cli/index.js (consumer-side)
|
|
29
|
+
# 2. dist/cli/index.js under CLAUDE_PROJECT_DIR (dogfood)
|
|
30
|
+
#
|
|
31
|
+
# When NO rea CLI is reachable, the hook falls through to allow —
|
|
32
|
+
# same posture as the bash-resident version, which `source`d
|
|
33
|
+
# _lib/common.sh first and exited cleanly if the lib was missing.
|
|
34
|
+
|
|
35
|
+
set -uo pipefail
|
|
36
|
+
|
|
37
|
+
# 1. HALT check.
|
|
38
|
+
# shellcheck source=_lib/halt-check.sh
|
|
39
|
+
source "$(dirname "$0")/_lib/halt-check.sh"
|
|
40
|
+
check_halt
|
|
41
|
+
REA_ROOT=$(rea_root)
|
|
42
|
+
|
|
43
|
+
proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
|
|
44
|
+
|
|
45
|
+
# 2. Relevance pre-gate (0.32.0 round-5 P1, round-6 fix). PreToolUse
|
|
46
|
+
# Bash matchers fire on EVERY shell command, but this hook only
|
|
47
|
+
# enforces against `gh issue create` payloads carrying disclosure
|
|
48
|
+
# keywords. Capture stdin + check relevance FIRST so unrelated
|
|
49
|
+
# commands exit 0 even when the CLI is missing/stale.
|
|
50
|
+
#
|
|
51
|
+
# Match `gh issue create` ANYWHERE in the command string (allow
|
|
52
|
+
# shell prefixes — `sudo`, env assignments). Round-6 P1.
|
|
53
|
+
INPUT=$(cat)
|
|
54
|
+
# Substring scan (NOT JSON-aware). Round-7 P1: any JSON-aware regex
|
|
55
|
+
# anchored on `"command":"...` gets tripped by escaped quotes in
|
|
56
|
+
# quoted env prefixes (`MODE="internal" gh issue create …`). Plain
|
|
57
|
+
# substring match has no such edge — and false-positives just defer
|
|
58
|
+
# to the Node body which handles correctly.
|
|
59
|
+
RELEVANT=0
|
|
60
|
+
if printf '%s' "$INPUT" | grep -qE 'gh[[:space:]]+issue[[:space:]]+create'; then
|
|
61
|
+
RELEVANT=1
|
|
62
|
+
fi
|
|
63
|
+
if [ "$RELEVANT" -eq 0 ]; then
|
|
64
|
+
exit 0
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# 2b. Mode short-circuit (round-6 P2). The pre-0.32.0 bash body
|
|
68
|
+
# no-op'd ONLY when `REA_DISCLOSURE_MODE=disabled` — `advisory`
|
|
69
|
+
# mode and the `issues` mode (default) BOTH enforced. Without
|
|
70
|
+
# this check, an unbuilt/stale install would refuse every relevant
|
|
71
|
+
# `gh issue create` even when the operator has deliberately set
|
|
72
|
+
# mode=disabled.
|
|
73
|
+
MODE="${REA_DISCLOSURE_MODE:-advisory}"
|
|
74
|
+
if [ "$MODE" = "disabled" ]; then
|
|
75
|
+
exit 0
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# 3. Resolve the rea CLI.
|
|
79
|
+
REA_ARGV=()
|
|
80
|
+
RESOLVED_CLI_PATH=""
|
|
81
|
+
if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
|
|
82
|
+
REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
|
|
83
|
+
RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
|
|
84
|
+
elif [ -f "$proj/dist/cli/index.js" ]; then
|
|
85
|
+
REA_ARGV=(node "$proj/dist/cli/index.js")
|
|
86
|
+
RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
if [ "${#REA_ARGV[@]}" -eq 0 ]; then
|
|
90
|
+
# 0.32.0 round-4 P1: this is a blocking-tier gate — the pre-0.32.0
|
|
91
|
+
# bash body enforced the disclosure policy WITHOUT a compiled CLI.
|
|
92
|
+
# Falling through to exit 0 here would silently disable security-
|
|
93
|
+
# keyword blocking on `gh issue create` until the operator runs
|
|
94
|
+
# `pnpm install` / `pnpm build`. Fail closed: refuse the operation
|
|
95
|
+
# and tell the operator how to restore protection.
|
|
96
|
+
printf 'rea: security-disclosure-gate cannot run — the rea CLI is not built.\n' >&2
|
|
97
|
+
printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
|
|
98
|
+
printf 'This shim fails closed because the pre-0.32.0 bash body enforced disclosure policy without a CLI.\n' >&2
|
|
99
|
+
exit 2
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# 3. Realpath sandbox check.
|
|
103
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
104
|
+
printf 'rea: security-disclosure-gate cannot run — `node` is not on PATH.\n' >&2
|
|
105
|
+
printf 'Install Node 22+ (engines.node) to restore disclosure-policy enforcement.\n' >&2
|
|
106
|
+
exit 2
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
sandbox_check=$(node -e '
|
|
110
|
+
const fs = require("fs");
|
|
111
|
+
const path = require("path");
|
|
112
|
+
const cli = process.argv[1];
|
|
113
|
+
const projDir = process.argv[2];
|
|
114
|
+
let real, realProj;
|
|
115
|
+
try { real = fs.realpathSync(cli); } catch (e) {
|
|
116
|
+
process.stdout.write("bad:realpath"); process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
try { realProj = fs.realpathSync(projDir); } catch (e) {
|
|
119
|
+
process.stdout.write("bad:realpath-proj"); process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
const sep = path.sep;
|
|
122
|
+
const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
|
|
123
|
+
if (!(real === realProj || real.startsWith(projWithSep))) {
|
|
124
|
+
process.stdout.write("bad:cli-escapes-project"); process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
let cur = path.dirname(path.dirname(path.dirname(real)));
|
|
127
|
+
let found = false;
|
|
128
|
+
for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
|
|
129
|
+
const pj = path.join(cur, "package.json");
|
|
130
|
+
if (fs.existsSync(pj)) {
|
|
131
|
+
try {
|
|
132
|
+
const data = JSON.parse(fs.readFileSync(pj, "utf8"));
|
|
133
|
+
if (data && data.name === "@bookedsolid/rea") { found = true; break; }
|
|
134
|
+
} catch (e) { /* keep walking */ }
|
|
135
|
+
}
|
|
136
|
+
cur = path.dirname(cur);
|
|
137
|
+
}
|
|
138
|
+
if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
|
|
139
|
+
process.stdout.write("ok");
|
|
140
|
+
' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
|
|
141
|
+
|
|
142
|
+
if [ "$sandbox_check" != "ok" ]; then
|
|
143
|
+
# 0.32.0 round-4 P1: fail closed (blocking-tier — see exit-0 → exit-2
|
|
144
|
+
# rationale at the top). A failed sandbox check means the CLI we
|
|
145
|
+
# would run cannot be authenticated as the rea binary; refusing is
|
|
146
|
+
# both the safest posture AND preserves the pre-0.32.0 bash-body
|
|
147
|
+
# contract that this hook always enforces policy.
|
|
148
|
+
printf 'rea: security-disclosure-gate FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
|
|
149
|
+
exit 2
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
# 4. Version-probe: confirm the resolved CLI implements
|
|
153
|
+
# `hook security-disclosure-gate`. Codex round 1 P1.
|
|
154
|
+
probe_out=$("${REA_ARGV[@]}" hook security-disclosure-gate --help 2>&1)
|
|
155
|
+
probe_status=$?
|
|
156
|
+
if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'security-disclosure-gate'; then
|
|
157
|
+
# 0.32.0 round-4 P1: a stale/older CLI without the new subcommand is
|
|
158
|
+
# NOT a "harmless availability fallback" for this hook — the bash
|
|
159
|
+
# body it replaces always enforced. Fail closed and tell the
|
|
160
|
+
# operator exactly how to fix.
|
|
161
|
+
printf 'rea: this shim requires the `rea hook security-disclosure-gate` subcommand (introduced in 0.32.0).\n' >&2
|
|
162
|
+
printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
|
|
163
|
+
printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
|
|
164
|
+
exit 2
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# 5. Forward stdin (already captured up-front for the relevance gate).
|
|
168
|
+
# REA_DISCLOSURE_MODE is in env already; the Node binary reads it
|
|
169
|
+
# directly.
|
|
170
|
+
printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook security-disclosure-gate
|
|
171
|
+
exit $?
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
--- /Volumes/Development/booked/rea/.claude/hooks/settings-protection.sh 2026-05-12 21:19:51
|
|
2
|
+
+++ /Volumes/Development/booked/rea/hooks/settings-protection.sh 2026-05-14 23:50:17
|
|
3
|
+
@@ -172,10 +172,12 @@
|
|
4
|
+
LOWER_NORM=$(printf '%s' "$NORMALIZED" | tr '[:upper:]' '[:lower:]')
|
|
5
|
+
|
|
6
|
+
# ── 5b. Extension-surface allow-list ──────────────────────────────────────────
|
|
7
|
+
-# `.husky/commit-msg.d/*` and `.husky/pre-push.d/*` are the documented
|
|
8
|
+
-# consumer extension surface (Fix H / 0.13.0). Consumers — and the agents
|
|
9
|
+
-# that govern those consumers — are expected to write here freely so they
|
|
10
|
+
-# can layer commitlint, lint-staged, branch-policy, act-CI, etc. without
|
|
11
|
+
+# `.husky/commit-msg.d/*`, `.husky/pre-push.d/*`, and (0.32.0+)
|
|
12
|
+
+# `.husky/prepare-commit-msg.d/*` are the documented consumer
|
|
13
|
+
+# extension surface (Fix H / 0.13.0; Phase 3 / 0.32.0 for the
|
|
14
|
+
+# prepare-commit-msg lane). Consumers — and the agents that govern
|
|
15
|
+
+# those consumers — are expected to write here freely so they can
|
|
16
|
+
+# layer commitlint, lint-staged, branch-policy, act-CI, etc. without
|
|
17
|
+
# losing rea coverage on `rea upgrade`.
|
|
18
|
+
#
|
|
19
|
+
# The §6 PROTECTED_PATTERNS list below has `.husky/` as a prefix block,
|
|
20
|
+
@@ -216,14 +218,14 @@
|
|
21
|
+
# subshell pattern (no Python or readlink -f dependency required).
|
|
22
|
+
# Closes the path-string→symlink bypass completely.
|
|
23
|
+
case "$LOWER_NORM" in
|
|
24
|
+
- .husky/commit-msg.d/*|.husky/pre-push.d/*)
|
|
25
|
+
+ .husky/commit-msg.d/*|.husky/pre-push.d/*|.husky/prepare-commit-msg.d/*)
|
|
26
|
+
if [ -L "$FILE_PATH" ]; then
|
|
27
|
+
{
|
|
28
|
+
printf 'SETTINGS PROTECTION: symlink in extension surface refused\n'
|
|
29
|
+
printf '\n'
|
|
30
|
+
printf ' File: %s\n' "$SAFE_FILE_PATH"
|
|
31
|
+
- printf ' Rule: .husky/commit-msg.d/* and .husky/pre-push.d/* must be\n'
|
|
32
|
+
- printf ' regular files (a symlink could resolve to a protected\n'
|
|
33
|
+
+ printf ' Rule: .husky/{commit-msg,pre-push,prepare-commit-msg}.d/* must\n'
|
|
34
|
+
+ printf ' be regular files (a symlink could resolve to a protected\n'
|
|
35
|
+
printf ' package-managed body and bypass §6 protection).\n'
|
|
36
|
+
} >&2
|
|
37
|
+
exit 2
|
|
38
|
+
@@ -245,8 +247,10 @@
|
|
39
|
+
# to `.husky/pre-push.d.bak/...` and slipped through.
|
|
40
|
+
# The trailing `/` on each pattern (and the explicit
|
|
41
|
+
# exact-match arm) requires a real directory boundary.
|
|
42
|
+
+ # 0.32.0 Phase 3: `.husky/prepare-commit-msg.d/` joins the
|
|
43
|
+
+ # allow-list (mirrors commit-msg.d/pre-push.d patterns).
|
|
44
|
+
case "$resolved_parent" in
|
|
45
|
+
- */.husky/commit-msg.d|*/.husky/commit-msg.d/*|*/.husky/pre-push.d|*/.husky/pre-push.d/*) : ;;
|
|
46
|
+
+ */.husky/commit-msg.d|*/.husky/commit-msg.d/*|*/.husky/pre-push.d|*/.husky/pre-push.d/*|*/.husky/prepare-commit-msg.d|*/.husky/prepare-commit-msg.d/*) : ;;
|
|
47
|
+
*)
|
|
48
|
+
{
|
|
49
|
+
printf 'SETTINGS PROTECTION: extension path resolves outside surface\n'
|
|
50
|
+
@@ -254,7 +258,7 @@
|
|
51
|
+
printf ' Logical: %s\n' "$SAFE_FILE_PATH"
|
|
52
|
+
printf ' Resolved: %s\n' "$resolved_parent"
|
|
53
|
+
printf ' Rule: an intermediate directory of the extension path is a\n'
|
|
54
|
+
- printf ' symlink whose target leaves .husky/{commit-msg,pre-push}.d/.\n'
|
|
55
|
+
+ printf ' symlink whose target leaves .husky/{commit-msg,pre-push,prepare-commit-msg}.d/.\n'
|
|
56
|
+
printf ' Refused to prevent symlinked-parent bypass of the\n'
|
|
57
|
+
printf ' package-managed body protection.\n'
|
|
58
|
+
} >&2
|