@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.
Files changed (34) hide show
  1. package/dist/cli/hook.js +49 -0
  2. package/dist/hooks/_lib/payload.d.ts +38 -0
  3. package/dist/hooks/_lib/payload.js +79 -0
  4. package/dist/hooks/_lib/segments.d.ts +127 -0
  5. package/dist/hooks/_lib/segments.js +628 -16
  6. package/dist/hooks/architecture-review-gate/index.d.ts +58 -0
  7. package/dist/hooks/architecture-review-gate/index.js +250 -0
  8. package/dist/hooks/changeset-security-gate/index.d.ts +71 -0
  9. package/dist/hooks/changeset-security-gate/index.js +330 -0
  10. package/dist/hooks/dangerous-bash-interceptor/index.d.ts +103 -0
  11. package/dist/hooks/dangerous-bash-interceptor/index.js +669 -0
  12. package/dist/hooks/dependency-audit-gate/index.d.ts +91 -0
  13. package/dist/hooks/dependency-audit-gate/index.js +294 -0
  14. package/dist/hooks/env-file-protection/index.d.ts +55 -0
  15. package/dist/hooks/env-file-protection/index.js +159 -0
  16. package/dist/hooks/local-review-gate/index.d.ts +145 -0
  17. package/dist/hooks/local-review-gate/index.js +374 -0
  18. package/dist/hooks/secret-scanner/index.d.ts +143 -0
  19. package/dist/hooks/secret-scanner/index.js +404 -0
  20. package/hooks/architecture-review-gate.sh +92 -77
  21. package/hooks/changeset-security-gate.sh +114 -149
  22. package/hooks/dangerous-bash-interceptor.sh +168 -386
  23. package/hooks/dependency-audit-gate.sh +115 -156
  24. package/hooks/env-file-protection.sh +130 -97
  25. package/hooks/local-review-gate.sh +523 -410
  26. package/hooks/secret-scanner.sh +210 -200
  27. package/package.json +1 -1
  28. package/templates/architecture-review-gate.dogfood-staged.sh +116 -0
  29. package/templates/changeset-security-gate.dogfood-staged.sh +137 -0
  30. package/templates/dangerous-bash-interceptor.dogfood-staged.sh +196 -0
  31. package/templates/dependency-audit-gate.dogfood-staged.sh +138 -0
  32. package/templates/env-file-protection.dogfood-staged.sh +157 -0
  33. package/templates/local-review-gate.dogfood-staged.sh +573 -0
  34. package/templates/secret-scanner.dogfood-staged.sh +240 -0
@@ -1,172 +1,137 @@
1
- #!/usr/bin/env bash
2
- # changeset-security-gate.sh — PreToolUse: Write|Edit
1
+ #!/bin/bash
2
+ # PreToolUse hook: changeset-security-gate.sh
3
+ # 0.33.0+ — Node-binary shim for `rea hook changeset-security-gate`.
3
4
  #
4
- # Guards .changeset/*.md files against two failure modes:
5
+ # Pre-0.33.0 the gate's full body lived here as bash (172 LOC, frontmatter
6
+ # validation + GHSA/CVE scan + MultiEdit-aware tool handling). The
7
+ # migration to the parser-backed Node binary moves all of that into
8
+ # `src/hooks/changeset-security-gate/index.ts`.
5
9
  #
6
- # 1. SECURITY DISCLOSURE LEAK GHSA IDs or CVE numbers written to a changeset
7
- # file before the advisory is published. Changeset files are committed to git
8
- # and appear verbatim in CHANGELOG.md referencing a GHSA ID pre-publish
9
- # creates public pre-disclosure in git history.
10
+ # Behavioral contract is preserved byte-for-byte: exit 0 on
11
+ # pass-through / non-changeset / valid frontmatter, exit 2 on HALT /
12
+ # disclosure leak / malformed frontmatter / malformed payload.
10
13
  #
11
- # 2. MISSING OR MALFORMED FRONTMATTER — changeset files without proper frontmatter
12
- # are silently ignored by the changesets tool, wasting the release entry.
14
+ # # CLI-resolution trust boundary
13
15
  #
14
- # Triggered by: PreToolUse Write and Edit tools
15
-
16
- set -euo pipefail
16
+ # Realpath sandbox check + version probe. Same shape as the 0.32.0
17
+ # pilots.
18
+ #
19
+ # # Fail-closed posture
20
+ #
21
+ # changeset-security-gate is BLOCKING-tier — the pre-0.33.0 bash body
22
+ # refused on GHSA/CVE patterns and on malformed frontmatter. Early-exit
23
+ # branches fail closed AFTER the relevance pre-gate passes.
17
24
 
18
- # shellcheck source=_lib/common.sh
19
- source "$(dirname "$0")/_lib/common.sh"
25
+ set -uo pipefail
20
26
 
27
+ # 1. HALT check.
28
+ # shellcheck source=_lib/halt-check.sh
29
+ source "$(dirname "$0")/_lib/halt-check.sh"
21
30
  check_halt
31
+ REA_ROOT=$(rea_root)
22
32
 
23
- INPUT="$(cat)"
24
- TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
25
-
26
- # 0.15.0 fix: MultiEdit was not in the allowed tool_name set, so the gate
27
- # silently exited 0 on every MultiEdit call against `.changeset/*.md` —
28
- # letting GHSA / CVE pre-disclosure through and skipping frontmatter
29
- # validation. 0.16.0: NotebookEdit added too (changesets are .md files
30
- # but a malicious agent could in principle route a .md write through
31
- # NotebookEdit's new_source path; cheap to allow, free to test).
32
- if [[ "$TOOL_NAME" != "Write" && "$TOOL_NAME" != "Edit" && "$TOOL_NAME" != "MultiEdit" && "$TOOL_NAME" != "NotebookEdit" ]]; then
33
- exit 0
34
- fi
35
-
36
- require_jq
37
-
38
- # 0.16.0: payload extraction migrated to `_lib/payload-read.sh`. Shared
39
- # helpers handle every write-tier tool with the same defensive
40
- # coercion. Adding the next write-tier tool is a one-line edit there.
41
- # shellcheck source=_lib/payload-read.sh
42
- source "$(dirname "$0")/_lib/payload-read.sh"
33
+ proj="${CLAUDE_PROJECT_DIR:-$REA_ROOT}"
43
34
 
44
- FILE_PATH=$(extract_file_path "$INPUT")
45
-
46
- # Only care about .changeset/*.md files — exclude README.md (changeset tool metadata)
47
- if ! echo "$FILE_PATH" | grep -qE '\.changeset/[^/]+\.md$' || echo "$FILE_PATH" | grep -qE '\.changeset/README\.md$'; then
48
- exit 0
49
- fi
50
-
51
- CONTENT=$(extract_write_content "$INPUT")
52
-
53
- # ─── 1. SECURITY DISCLOSURE CHECK ───────────────────────────────────────────
35
+ # 2. Relevance pre-gate. This is a PreToolUse Write/Edit/MultiEdit/
36
+ # NotebookEdit matcher, so the payload always has a `tool_input.
37
+ # file_path` (or `notebook_path`).
54
38
  #
55
- # These patterns in a changeset mean security details are about to be committed
56
- # to git history BEFORE the advisory is published — creating pre-disclosure.
57
- # GHSA IDs and CVE numbers must NEVER appear in changeset files.
58
-
59
- DISCLOSURE_PATTERNS=(
60
- 'GHSA-[0-9A-Za-z]{4}-[0-9A-Za-z]{4}-[0-9A-Za-z]{4}'
61
- 'CVE-[0-9]{4}-[0-9]+'
62
- )
63
-
64
- MATCHED_PATTERN=""
65
- for PATTERN in "${DISCLOSURE_PATTERNS[@]}"; do
66
- if echo "$CONTENT" | grep -qE "$PATTERN"; then
67
- MATCHED_PATTERN="$PATTERN"
68
- break
39
+ # 2026-05-15 codex round-2 P2 fix: scan `tool_input.file_path` /
40
+ # `tool_input.notebook_path` ONLY, NOT the raw JSON payload. Pre-fix
41
+ # a Write to `README.md` whose body merely mentions `.changeset/`
42
+ # (e.g. "See .changeset/example.md") tripped the fail-closed branch
43
+ # when the CLI was unbuilt — the substring lived in the
44
+ # tool_input.content blob, not in the target path. The Node body
45
+ # correctly filters by file_path; the shim's pre-gate must match
46
+ # that posture.
47
+ INPUT=$(cat)
48
+ RELEVANT=0
49
+ PROBE=""
50
+ if command -v jq >/dev/null 2>&1; then
51
+ PROBE=$(printf '%s' "$INPUT" | jq -r '(.tool_input.file_path // .tool_input.notebook_path // "")' 2>/dev/null || true)
52
+ if printf '%s' "$PROBE" | grep -qE '\.changeset/'; then
53
+ RELEVANT=1
54
+ fi
55
+ else
56
+ if printf '%s' "$INPUT" | grep -qE '\.changeset/'; then
57
+ RELEVANT=1
69
58
  fi
70
- done
71
-
72
- if [[ -n "$MATCHED_PATTERN" ]]; then
73
- json_output "block" \
74
- "CHANGESET SECURITY GATE: This changeset contains a security advisory identifier (matched: '${MATCHED_PATTERN}').
75
-
76
- Do NOT reference GHSA IDs or CVE numbers in changeset files before the advisory is published.
77
- Changeset files are committed to git — this creates pre-disclosure in public history and CHANGELOG.
78
-
79
- CORRECT approach for security fix changesets:
80
- Use vague language only — no identifiers, no vulnerability details.
81
-
82
- WRONG: 'fix(hooks): patch GHSA-3w3m-7gg4-f82g — symlink-guard now covers Edit tool'
83
- RIGHT: 'security: extend symlink protection to cover all write-capable tools'
84
-
85
- WRONG: 'security: fix CVE-2026-1234 prompt injection via tool descriptions'
86
- RIGHT: 'security: harden middleware chain against indirect instruction attacks'
87
-
88
- After the release ships:
89
- 1. Publish the GitHub Security Advisory (Security tab → Advisories → Publish)
90
- 2. The GHSA becomes the detailed public disclosure document
91
- 3. Optionally update CHANGELOG.md post-publish to add the GHSA reference"
92
59
  fi
93
-
94
- # ─── 2. FRONTMATTER VALIDATION ───────────────────────────────────────────────
95
- #
96
- # A changeset without valid frontmatter is silently ignored by the changesets
97
- # tool — the package bump and CHANGELOG entry never appear in the release.
98
- #
99
- # 0.15.0 fix: skip frontmatter validation for MultiEdit. MultiEdit's
100
- # `tool_input.edits[].new_string` payload is a list of partial string
101
- # replacements, not the full file body — running the frontmatter
102
- # validator against the concatenation of new_strings would reject every
103
- # legitimate MultiEdit on an existing changeset (none of the edit
104
- # fragments individually contains a frontmatter block, even though the
105
- # resulting file does). The disclosure scan above still runs on
106
- # MultiEdit content because GHSA/CVE patterns match per-fragment without
107
- # any structural assumption.
108
- if [[ "$TOOL_NAME" == "MultiEdit" ]]; then
60
+ if [ "$RELEVANT" -eq 0 ]; then
109
61
  exit 0
110
62
  fi
111
63
 
112
- # Must start with ---
113
- if ! echo "$CONTENT" | head -1 | grep -qE '^---'; then
114
- json_output "block" \
115
- "CHANGESET FORMAT GATE: Missing frontmatter block.
116
-
117
- Every changeset must start with a frontmatter block specifying which package to bump:
118
-
119
- ---
120
- '@bookedsolid/rea': patch
121
- ---
122
-
123
- Brief description of what changed and why (close #N if applicable).
124
-
125
- Bump types: patch (bug fix/security), minor (new feature), major (breaking change)"
64
+ # 3. Resolve the rea CLI.
65
+ REA_ARGV=()
66
+ RESOLVED_CLI_PATH=""
67
+ if [ -f "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js" ]; then
68
+ REA_ARGV=(node "$proj/node_modules/@bookedsolid/rea/dist/cli/index.js")
69
+ RESOLVED_CLI_PATH="$proj/node_modules/@bookedsolid/rea/dist/cli/index.js"
70
+ elif [ -f "$proj/dist/cli/index.js" ]; then
71
+ REA_ARGV=(node "$proj/dist/cli/index.js")
72
+ RESOLVED_CLI_PATH="$proj/dist/cli/index.js"
126
73
  fi
127
74
 
128
- # Must have at least one package bump entry and a closing ---.
129
- # 0.15.0 fix: accept single-quoted, double-quoted, AND unquoted package
130
- # names (all three are valid YAML for the same string). Pre-fix the
131
- # regex required single quotes, so a tool or human authoring the
132
- # changeset with `"@scope/name": patch` was rejected as malformed even
133
- # though the Changesets tool itself accepts every form.
134
- #
135
- # Codex round-1 P2-1 fix: explicit-alternation form (no backref) so
136
- # the unquoted variant matches on BSD grep too. The earlier
137
- # `^([\"']?)[^\"']+\1: ...` shape relied on backref-with-empty-capture
138
- # semantics that BSD's grep rejects when the capture group's `?` made
139
- # it absent — quoted forms matched on macOS but unquoted did not.
140
- FRONTMATTER=$(echo "$CONTENT" | awk '/^---/{count++; if(count==2){exit} next} count==1{print}')
141
- if ! echo "$FRONTMATTER" | grep -qE "^(\"[^\"]+\"|'[^']+'|[^\"'[:space:]]+): (patch|minor|major)"; then
142
- json_output "block" \
143
- "CHANGESET FORMAT GATE: Frontmatter does not contain a valid package bump entry.
144
-
145
- The frontmatter must include at least one package/bump pair:
146
-
147
- ---
148
- '@bookedsolid/rea': patch
149
- ---
150
-
151
- Valid bump types: patch | minor | major"
75
+ if [ "${#REA_ARGV[@]}" -eq 0 ]; then
76
+ printf 'rea: changeset-security-gate cannot run — the rea CLI is not built.\n' >&2
77
+ printf 'Run `pnpm install && pnpm build` (or `npm install` for a consumer install) to restore protection.\n' >&2
78
+ exit 2
152
79
  fi
153
80
 
154
- # Must have a non-empty description after the closing ---
155
- DESCRIPTION=$(echo "$CONTENT" | awk 'BEGIN{count=0} /^---/{count++; next} count>=2{print}' | grep -v '^[[:space:]]*$' | head -1 || true)
156
- if [[ -z "$DESCRIPTION" ]]; then
157
- json_output "block" \
158
- "CHANGESET FORMAT GATE: Missing description after frontmatter.
159
-
160
- Add a meaningful description explaining what changed and why:
161
-
162
- ---
163
- '@bookedsolid/rea': patch
164
- ---
81
+ # 4. Realpath sandbox check.
82
+ if ! command -v node >/dev/null 2>&1; then
83
+ printf 'rea: changeset-security-gate cannot run — `node` is not on PATH.\n' >&2
84
+ exit 2
85
+ fi
165
86
 
166
- fix(gateway): policy-loader now uses async I/O with 500ms TTL cache
87
+ sandbox_check=$(node -e '
88
+ const fs = require("fs");
89
+ const path = require("path");
90
+ const cli = process.argv[1];
91
+ const projDir = process.argv[2];
92
+ let real, realProj;
93
+ try { real = fs.realpathSync(cli); } catch (e) {
94
+ process.stdout.write("bad:realpath"); process.exit(1);
95
+ }
96
+ try { realProj = fs.realpathSync(projDir); } catch (e) {
97
+ process.stdout.write("bad:realpath-proj"); process.exit(1);
98
+ }
99
+ const sep = path.sep;
100
+ const projWithSep = realProj.endsWith(sep) ? realProj : realProj + sep;
101
+ if (!(real === realProj || real.startsWith(projWithSep))) {
102
+ process.stdout.write("bad:cli-escapes-project"); process.exit(1);
103
+ }
104
+ let cur = path.dirname(path.dirname(path.dirname(real)));
105
+ let found = false;
106
+ for (let i = 0; i < 20 && cur && cur !== path.dirname(cur); i += 1) {
107
+ const pj = path.join(cur, "package.json");
108
+ if (fs.existsSync(pj)) {
109
+ try {
110
+ const data = JSON.parse(fs.readFileSync(pj, "utf8"));
111
+ if (data && data.name === "@bookedsolid/rea") { found = true; break; }
112
+ } catch (e) { /* keep walking */ }
113
+ }
114
+ cur = path.dirname(cur);
115
+ }
116
+ if (!found) { process.stdout.write("bad:no-rea-pkg-json"); process.exit(1); }
117
+ process.stdout.write("ok");
118
+ ' -- "$RESOLVED_CLI_PATH" "$proj" 2>/dev/null)
119
+
120
+ if [ "$sandbox_check" != "ok" ]; then
121
+ printf 'rea: changeset-security-gate FAILED sandbox check (%s) — refusing.\n' "$sandbox_check" >&2
122
+ exit 2
123
+ fi
167
124
 
168
- Previously, loadPolicy used fs.readFileSync on every tool invocation, blocking
169
- the event loop under concurrency. Closes #34."
125
+ # 5. Version-probe.
126
+ probe_out=$("${REA_ARGV[@]}" hook changeset-security-gate --help 2>&1)
127
+ probe_status=$?
128
+ if [ "$probe_status" -ne 0 ] || ! printf '%s' "$probe_out" | grep -q -e 'changeset-security-gate'; then
129
+ printf 'rea: this shim requires the `rea hook changeset-security-gate` subcommand (introduced in 0.33.0).\n' >&2
130
+ printf 'The resolved CLI at %s does not implement it.\n' "$RESOLVED_CLI_PATH" >&2
131
+ printf 'Run `pnpm install` (or `npm install`) to sync the CLI; refusing in the meantime to preserve enforcement.\n' >&2
132
+ exit 2
170
133
  fi
171
134
 
172
- exit 0
135
+ # 6. Forward stdin.
136
+ printf '%s' "$INPUT" | "${REA_ARGV[@]}" hook changeset-security-gate
137
+ exit $?