@bookedsolid/rea 0.1.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/LICENSE +21 -0
- package/README.md +339 -0
- package/SECURITY.md +104 -0
- package/THREAT_MODEL.md +245 -0
- package/agents/accessibility-engineer.md +101 -0
- package/agents/backend-engineer.md +126 -0
- package/agents/code-reviewer.md +144 -0
- package/agents/codex-adversarial.md +107 -0
- package/agents/frontend-specialist.md +84 -0
- package/agents/qa-engineer.md +138 -0
- package/agents/rea-orchestrator.md +101 -0
- package/agents/security-engineer.md +108 -0
- package/agents/technical-writer.md +140 -0
- package/agents/typescript-specialist.md +111 -0
- package/commands/codex-review.md +104 -0
- package/commands/freeze.md +81 -0
- package/commands/halt-check.md +120 -0
- package/commands/rea.md +52 -0
- package/commands/review.md +79 -0
- package/dist/cli/check.d.ts +1 -0
- package/dist/cli/check.js +66 -0
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +93 -0
- package/dist/cli/freeze.d.ts +8 -0
- package/dist/cli/freeze.js +61 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +65 -0
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.js +237 -0
- package/dist/cli/serve.d.ts +1 -0
- package/dist/cli/serve.js +19 -0
- package/dist/cli/utils.d.ts +23 -0
- package/dist/cli/utils.js +51 -0
- package/dist/config/tier-map.d.ts +11 -0
- package/dist/config/tier-map.js +108 -0
- package/dist/config/types.d.ts +24 -0
- package/dist/config/types.js +1 -0
- package/dist/gateway/circuit-breaker.d.ts +43 -0
- package/dist/gateway/circuit-breaker.js +86 -0
- package/dist/gateway/middleware/audit-types.d.ts +16 -0
- package/dist/gateway/middleware/audit-types.js +1 -0
- package/dist/gateway/middleware/audit.d.ts +12 -0
- package/dist/gateway/middleware/audit.js +98 -0
- package/dist/gateway/middleware/blocked-paths.d.ts +12 -0
- package/dist/gateway/middleware/blocked-paths.js +117 -0
- package/dist/gateway/middleware/chain.d.ts +28 -0
- package/dist/gateway/middleware/chain.js +40 -0
- package/dist/gateway/middleware/circuit-breaker.d.ts +11 -0
- package/dist/gateway/middleware/circuit-breaker.js +43 -0
- package/dist/gateway/middleware/injection.d.ts +22 -0
- package/dist/gateway/middleware/injection.js +128 -0
- package/dist/gateway/middleware/kill-switch.d.ts +10 -0
- package/dist/gateway/middleware/kill-switch.js +58 -0
- package/dist/gateway/middleware/policy.d.ts +12 -0
- package/dist/gateway/middleware/policy.js +70 -0
- package/dist/gateway/middleware/rate-limit.d.ts +12 -0
- package/dist/gateway/middleware/rate-limit.js +31 -0
- package/dist/gateway/middleware/redact.d.ts +16 -0
- package/dist/gateway/middleware/redact.js +128 -0
- package/dist/gateway/middleware/result-size-cap.d.ts +13 -0
- package/dist/gateway/middleware/result-size-cap.js +48 -0
- package/dist/gateway/middleware/session.d.ts +10 -0
- package/dist/gateway/middleware/session.js +18 -0
- package/dist/gateway/middleware/tier.d.ts +6 -0
- package/dist/gateway/middleware/tier.js +10 -0
- package/dist/gateway/rate-limiter.d.ts +36 -0
- package/dist/gateway/rate-limiter.js +75 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/policy/loader.d.ts +80 -0
- package/dist/policy/loader.js +146 -0
- package/dist/policy/types.d.ts +34 -0
- package/dist/policy/types.js +19 -0
- package/hooks/_lib/common.sh +105 -0
- package/hooks/_lib/halt-check.sh +39 -0
- package/hooks/_lib/policy-read.sh +79 -0
- package/hooks/architecture-review-gate.sh +84 -0
- package/hooks/attribution-advisory.sh +126 -0
- package/hooks/blocked-paths-enforcer.sh +176 -0
- package/hooks/changeset-security-gate.sh +143 -0
- package/hooks/commit-review-gate.sh +166 -0
- package/hooks/dangerous-bash-interceptor.sh +362 -0
- package/hooks/dependency-audit-gate.sh +118 -0
- package/hooks/env-file-protection.sh +110 -0
- package/hooks/pr-issue-link-gate.sh +65 -0
- package/hooks/push-review-gate.sh +120 -0
- package/hooks/secret-scanner.sh +229 -0
- package/hooks/security-disclosure-gate.sh +146 -0
- package/hooks/settings-protection.sh +147 -0
- package/package.json +93 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: attribution-advisory.sh
|
|
3
|
+
# Fires BEFORE every Bash tool call.
|
|
4
|
+
#
|
|
5
|
+
# OPT-IN: Only enforces when .rea/policy.yaml contains:
|
|
6
|
+
# block_ai_attribution: true
|
|
7
|
+
#
|
|
8
|
+
# When disabled (default), this hook does nothing.
|
|
9
|
+
# When enabled, BLOCKS (exit 2) gh pr create/edit and git commit commands
|
|
10
|
+
# that contain structural AI attribution markers.
|
|
11
|
+
#
|
|
12
|
+
# Exit codes:
|
|
13
|
+
# 0 = allow (disabled, no attribution found, or not a relevant command)
|
|
14
|
+
# 2 = block (attribution detected, or HALT is active)
|
|
15
|
+
|
|
16
|
+
set -uo pipefail
|
|
17
|
+
|
|
18
|
+
# ── 1. Read ALL stdin immediately before doing anything else ──────────────────
|
|
19
|
+
INPUT=$(cat)
|
|
20
|
+
|
|
21
|
+
# ── 2. Dependency check ───────────────────────────────────────────────────────
|
|
22
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
23
|
+
printf 'REA ERROR: jq is required but not installed.\n' >&2
|
|
24
|
+
printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
|
|
25
|
+
exit 2
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# ── 3. HALT check ─────────────────────────────────────────────────────────────
|
|
29
|
+
REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
30
|
+
HALT_FILE="${REA_ROOT}/.rea/HALT"
|
|
31
|
+
if [ -f "$HALT_FILE" ]; then
|
|
32
|
+
printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
|
|
33
|
+
"$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
|
|
34
|
+
exit 2
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# ── 4. Check if attribution blocking is enabled ──────────────────────────────
|
|
38
|
+
POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
|
|
39
|
+
if [ ! -f "$POLICY_FILE" ]; then
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
if ! grep -qE '^block_ai_attribution:[[:space:]]*true' "$POLICY_FILE" 2>/dev/null; then
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# ── 5. Parse tool_input.command from the hook payload ─────────────────────────
|
|
47
|
+
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
48
|
+
|
|
49
|
+
if [[ -z "$CMD" ]]; then
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# ── 6. Check if this is a relevant command ────────────────────────────────────
|
|
54
|
+
IS_RELEVANT=0
|
|
55
|
+
|
|
56
|
+
if printf '%s' "$CMD" | grep -qiE 'gh[[:space:]]+pr[[:space:]]+(create|edit)'; then
|
|
57
|
+
IS_RELEVANT=1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+commit'; then
|
|
61
|
+
IS_RELEVANT=1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
if [[ $IS_RELEVANT -eq 0 ]]; then
|
|
65
|
+
exit 0
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# ── 7. Check for structural AI attribution markers ───────────────────────────
|
|
69
|
+
|
|
70
|
+
FOUND=0
|
|
71
|
+
|
|
72
|
+
# Co-Authored-By with noreply@ email
|
|
73
|
+
if printf '%s' "$CMD" | grep -qiE 'Co-Authored-By:.*noreply@'; then
|
|
74
|
+
FOUND=1
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# Co-Authored-By with known AI names
|
|
78
|
+
if printf '%s' "$CMD" | grep -qiE 'Co-Authored-By:.*\b(Claude|Sonnet|Opus|Haiku|Copilot|GPT|ChatGPT|Gemini|Cursor|Codeium|Tabnine|Amazon Q|CodeWhisperer|Devin|Windsurf|Cline|Aider|Anthropic|OpenAI|GitHub Copilot)\b'; then
|
|
79
|
+
FOUND=1
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# "Generated/Built/Powered with/by [AI Tool]" lines
|
|
83
|
+
if printf '%s' "$CMD" | grep -qiE '(Generated|Created|Built|Powered|Authored|Written|Produced)[[:space:]]+(with|by)[[:space:]]+(Claude|Copilot|GPT|ChatGPT|Gemini|Cursor|Codeium|Tabnine|CodeWhisperer|Devin|Windsurf|Cline|Aider|AI|an? AI)\b'; then
|
|
84
|
+
FOUND=1
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# Markdown-linked attribution
|
|
88
|
+
if printf '%s' "$CMD" | grep -qiE '\[Claude Code\]|\[GitHub Copilot\]|\[ChatGPT\]|\[Gemini\]|\[Cursor\]'; then
|
|
89
|
+
FOUND=1
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# Emoji attribution
|
|
93
|
+
if printf '%s' "$CMD" | grep -qE '🤖.*[Gg]enerated'; then
|
|
94
|
+
FOUND=1
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
if [[ $FOUND -eq 1 ]]; then
|
|
98
|
+
{
|
|
99
|
+
printf '\n'
|
|
100
|
+
printf '═══════════════════════════════════════════════════════════════════\n'
|
|
101
|
+
printf ' BLOCKED: AI attribution detected in command\n'
|
|
102
|
+
printf '═══════════════════════════════════════════════════════════════════\n'
|
|
103
|
+
printf '\n'
|
|
104
|
+
printf ' Your command contains structural AI attribution markers.\n'
|
|
105
|
+
printf '\n'
|
|
106
|
+
printf ' What gets BLOCKED (structural attribution):\n'
|
|
107
|
+
printf ' - Co-Authored-By with AI names or noreply@ emails\n'
|
|
108
|
+
printf ' - "Generated with/by [AI Tool]" footer lines\n'
|
|
109
|
+
printf ' - Markdown-linked tool names: [Claude Code](...)\n'
|
|
110
|
+
printf ' - Emoji attribution: 🤖 Generated...\n'
|
|
111
|
+
printf '\n'
|
|
112
|
+
printf ' What is ALLOWED (legitimate references):\n'
|
|
113
|
+
printf ' - "Fix Claude API integration"\n'
|
|
114
|
+
printf ' - "Update OpenAI SDK version"\n'
|
|
115
|
+
printf ' - "Add Copilot config"\n'
|
|
116
|
+
printf '\n'
|
|
117
|
+
printf ' Remove the attribution markers and rewrite the command.\n'
|
|
118
|
+
printf ' To disable: set block_ai_attribution: false in .rea/policy.yaml\n'
|
|
119
|
+
printf '═══════════════════════════════════════════════════════════════════\n'
|
|
120
|
+
printf '\n'
|
|
121
|
+
} >&2
|
|
122
|
+
exit 2
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# No attribution found — allow
|
|
126
|
+
exit 0
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: blocked-paths-enforcer.sh
|
|
3
|
+
# Fires BEFORE every Write or Edit tool call.
|
|
4
|
+
# Reads blocked_paths from .rea/policy.yaml and blocks matching writes.
|
|
5
|
+
#
|
|
6
|
+
# This enforces the policy layer at the hook level — even if an agent ignores
|
|
7
|
+
# the CLAUDE.md rules or skips the orchestrator, the hook will catch it.
|
|
8
|
+
#
|
|
9
|
+
# Exit codes:
|
|
10
|
+
# 0 = allow (path not blocked)
|
|
11
|
+
# 2 = block (path matches a blocked_paths entry)
|
|
12
|
+
|
|
13
|
+
set -uo pipefail
|
|
14
|
+
|
|
15
|
+
# ── 1. Read ALL stdin immediately ─────────────────────────────────────────────
|
|
16
|
+
INPUT=$(cat)
|
|
17
|
+
|
|
18
|
+
# ── 2. Dependency check ──────────────────────────────────────────────────────
|
|
19
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
20
|
+
printf 'REA ERROR: jq is required but not installed.\n' >&2
|
|
21
|
+
printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
|
|
22
|
+
exit 2
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# ── 3. HALT check ────────────────────────────────────────────────────────────
|
|
26
|
+
REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
27
|
+
HALT_FILE="${REA_ROOT}/.rea/HALT"
|
|
28
|
+
if [ -f "$HALT_FILE" ]; then
|
|
29
|
+
printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
|
|
30
|
+
"$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
|
|
31
|
+
exit 2
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# ── 4. Extract file path from payload ─────────────────────────────────────────
|
|
35
|
+
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
36
|
+
|
|
37
|
+
if [[ -z "$FILE_PATH" ]]; then
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# ── 5. Load blocked_paths from policy ─────────────────────────────────────────
|
|
42
|
+
POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
|
|
43
|
+
|
|
44
|
+
if [[ ! -f "$POLICY_FILE" ]]; then
|
|
45
|
+
exit 0
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Parse blocked_paths using grep + sed (avoid yaml parser dependency)
|
|
49
|
+
# Handles both inline array [] and block sequence - "..." formats
|
|
50
|
+
BLOCKED_PATHS=()
|
|
51
|
+
IN_BLOCK=0
|
|
52
|
+
while IFS= read -r line; do
|
|
53
|
+
# Check if we're entering blocked_paths section
|
|
54
|
+
if printf '%s' "$line" | grep -qE '^blocked_paths:'; then
|
|
55
|
+
# Check for inline empty array
|
|
56
|
+
if printf '%s' "$line" | grep -qE 'blocked_paths:[[:space:]]*\[\]'; then
|
|
57
|
+
break
|
|
58
|
+
fi
|
|
59
|
+
# Check for inline array with values
|
|
60
|
+
if printf '%s' "$line" | grep -qE 'blocked_paths:[[:space:]]*\['; then
|
|
61
|
+
# Extract inline array items
|
|
62
|
+
items=$(printf '%s' "$line" | sed 's/.*\[//; s/\].*//; s/,/ /g')
|
|
63
|
+
for item in $items; do
|
|
64
|
+
cleaned=$(printf '%s' "$item" | sed "s/^[[:space:]]*[\"']//; s/[\"'][[:space:]]*$//")
|
|
65
|
+
if [[ -n "$cleaned" ]]; then
|
|
66
|
+
BLOCKED_PATHS+=("$cleaned")
|
|
67
|
+
fi
|
|
68
|
+
done
|
|
69
|
+
break
|
|
70
|
+
fi
|
|
71
|
+
IN_BLOCK=1
|
|
72
|
+
continue
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
if [[ $IN_BLOCK -eq 1 ]]; then
|
|
76
|
+
# Block sequence items start with " - "
|
|
77
|
+
if printf '%s' "$line" | grep -qE '^[[:space:]]+-'; then
|
|
78
|
+
cleaned=$(printf '%s' "$line" | sed 's/^[[:space:]]*-[[:space:]]*//; s/^"//; s/"$//; s/^'"'"'//; s/'"'"'$//')
|
|
79
|
+
if [[ -n "$cleaned" ]]; then
|
|
80
|
+
BLOCKED_PATHS+=("$cleaned")
|
|
81
|
+
fi
|
|
82
|
+
else
|
|
83
|
+
# Non-indented line means we've left the block
|
|
84
|
+
break
|
|
85
|
+
fi
|
|
86
|
+
fi
|
|
87
|
+
done < "$POLICY_FILE"
|
|
88
|
+
|
|
89
|
+
if [[ ${#BLOCKED_PATHS[@]} -eq 0 ]]; then
|
|
90
|
+
exit 0
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# ── 6. Agent-writable allowlist ───────────────────────────────────────────────
|
|
94
|
+
# These paths under .rea/ must always be writable by agents regardless of
|
|
95
|
+
# what blocked_paths says. Blocking the whole .rea/ directory in policy
|
|
96
|
+
# is a common default, but tasks.jsonl is the PM data store — agents must
|
|
97
|
+
# write there. Settings-protection.sh guards the sensitive files explicitly.
|
|
98
|
+
AGENT_WRITABLE=(
|
|
99
|
+
'.rea/tasks.jsonl'
|
|
100
|
+
'.rea/audit/'
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
normalize_path() {
|
|
104
|
+
local p="$1"
|
|
105
|
+
local root="$REA_ROOT"
|
|
106
|
+
if [[ "$p" == "$root"/* ]]; then
|
|
107
|
+
p="${p#$root/}"
|
|
108
|
+
fi
|
|
109
|
+
p=$(printf '%s' "$p" | sed 's/%2[Ff]/\//g; s/%2[Ee]/./g; s/%20/ /g')
|
|
110
|
+
p="${p#./}"
|
|
111
|
+
printf '%s' "$p"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
NORMALIZED=$(normalize_path "$FILE_PATH")
|
|
115
|
+
|
|
116
|
+
for writable in "${AGENT_WRITABLE[@]}"; do
|
|
117
|
+
if [[ "$NORMALIZED" == "$writable" ]] || [[ "$NORMALIZED" == "$writable"* && "$writable" == */ ]]; then
|
|
118
|
+
exit 0
|
|
119
|
+
fi
|
|
120
|
+
done
|
|
121
|
+
|
|
122
|
+
# ── 7. Match against blocked_paths ───────────────────────────────────────────
|
|
123
|
+
LOWER_NORM=$(printf '%s' "$NORMALIZED" | tr '[:upper:]' '[:lower:]')
|
|
124
|
+
|
|
125
|
+
for blocked in "${BLOCKED_PATHS[@]}"; do
|
|
126
|
+
LOWER_BLOCKED=$(printf '%s' "$blocked" | tr '[:upper:]' '[:lower:]')
|
|
127
|
+
|
|
128
|
+
# Directory match (blocked path ends with /)
|
|
129
|
+
if [[ "$LOWER_BLOCKED" == */ ]]; then
|
|
130
|
+
if [[ "$LOWER_NORM" == "$LOWER_BLOCKED"* ]] || [[ "$LOWER_NORM" == "${LOWER_BLOCKED%/}" ]]; then
|
|
131
|
+
{
|
|
132
|
+
printf 'BLOCKED PATH: Write denied by policy\n'
|
|
133
|
+
printf '\n'
|
|
134
|
+
printf ' File: %s\n' "$FILE_PATH"
|
|
135
|
+
printf ' Blocked by: %s\n' "$blocked"
|
|
136
|
+
printf ' Source: .rea/policy.yaml → blocked_paths\n'
|
|
137
|
+
printf '\n'
|
|
138
|
+
printf ' This path is protected by policy. To modify it, a human must\n'
|
|
139
|
+
printf ' either update blocked_paths in policy.yaml or edit the file directly.\n'
|
|
140
|
+
} >&2
|
|
141
|
+
exit 2
|
|
142
|
+
fi
|
|
143
|
+
continue
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# Glob pattern match (contains *)
|
|
147
|
+
if [[ "$blocked" == *'*'* ]]; then
|
|
148
|
+
# Convert glob to regex: . → \., * → .*
|
|
149
|
+
regex=$(printf '%s' "$LOWER_BLOCKED" | sed 's/\./\\./g; s/\*/.*/g')
|
|
150
|
+
if printf '%s' "$LOWER_NORM" | grep -qE "^${regex}$"; then
|
|
151
|
+
{
|
|
152
|
+
printf 'BLOCKED PATH: Write denied by policy\n'
|
|
153
|
+
printf '\n'
|
|
154
|
+
printf ' File: %s\n' "$FILE_PATH"
|
|
155
|
+
printf ' Blocked by: %s (glob pattern)\n' "$blocked"
|
|
156
|
+
printf ' Source: .rea/policy.yaml → blocked_paths\n'
|
|
157
|
+
} >&2
|
|
158
|
+
exit 2
|
|
159
|
+
fi
|
|
160
|
+
continue
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
# Exact match
|
|
164
|
+
if [[ "$LOWER_NORM" == "$LOWER_BLOCKED" ]]; then
|
|
165
|
+
{
|
|
166
|
+
printf 'BLOCKED PATH: Write denied by policy\n'
|
|
167
|
+
printf '\n'
|
|
168
|
+
printf ' File: %s\n' "$FILE_PATH"
|
|
169
|
+
printf ' Blocked by: %s\n' "$blocked"
|
|
170
|
+
printf ' Source: .rea/policy.yaml → blocked_paths\n'
|
|
171
|
+
} >&2
|
|
172
|
+
exit 2
|
|
173
|
+
fi
|
|
174
|
+
done
|
|
175
|
+
|
|
176
|
+
exit 0
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# changeset-security-gate.sh — PreToolUse: Write|Edit
|
|
3
|
+
#
|
|
4
|
+
# Guards .changeset/*.md files against two failure modes:
|
|
5
|
+
#
|
|
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
|
+
#
|
|
11
|
+
# 2. MISSING OR MALFORMED FRONTMATTER — changeset files without proper frontmatter
|
|
12
|
+
# are silently ignored by the changesets tool, wasting the release entry.
|
|
13
|
+
#
|
|
14
|
+
# Triggered by: PreToolUse — Write and Edit tools
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
# shellcheck source=_lib/common.sh
|
|
19
|
+
source "$(dirname "$0")/_lib/common.sh"
|
|
20
|
+
|
|
21
|
+
check_halt
|
|
22
|
+
|
|
23
|
+
INPUT="$(cat)"
|
|
24
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
25
|
+
|
|
26
|
+
# Only handle Write and Edit
|
|
27
|
+
if [[ "$TOOL_NAME" != "Write" && "$TOOL_NAME" != "Edit" ]]; then
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
32
|
+
|
|
33
|
+
# Only care about .changeset/*.md files — exclude README.md (changeset tool metadata)
|
|
34
|
+
if ! echo "$FILE_PATH" | grep -qE '\.changeset/[^/]+\.md$' || echo "$FILE_PATH" | grep -qE '\.changeset/README\.md$'; then
|
|
35
|
+
exit 0
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
require_jq
|
|
39
|
+
|
|
40
|
+
# Extract the content being written
|
|
41
|
+
if [[ "$TOOL_NAME" == "Write" ]]; then
|
|
42
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // ""')
|
|
43
|
+
else
|
|
44
|
+
# For Edit: check the new_string being inserted
|
|
45
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // ""')
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# ─── 1. SECURITY DISCLOSURE CHECK ───────────────────────────────────────────
|
|
49
|
+
#
|
|
50
|
+
# These patterns in a changeset mean security details are about to be committed
|
|
51
|
+
# to git history BEFORE the advisory is published — creating pre-disclosure.
|
|
52
|
+
# GHSA IDs and CVE numbers must NEVER appear in changeset files.
|
|
53
|
+
|
|
54
|
+
DISCLOSURE_PATTERNS=(
|
|
55
|
+
'GHSA-[0-9A-Za-z]{4}-[0-9A-Za-z]{4}-[0-9A-Za-z]{4}'
|
|
56
|
+
'CVE-[0-9]{4}-[0-9]+'
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
MATCHED_PATTERN=""
|
|
60
|
+
for PATTERN in "${DISCLOSURE_PATTERNS[@]}"; do
|
|
61
|
+
if echo "$CONTENT" | grep -qE "$PATTERN"; then
|
|
62
|
+
MATCHED_PATTERN="$PATTERN"
|
|
63
|
+
break
|
|
64
|
+
fi
|
|
65
|
+
done
|
|
66
|
+
|
|
67
|
+
if [[ -n "$MATCHED_PATTERN" ]]; then
|
|
68
|
+
json_output "block" \
|
|
69
|
+
"CHANGESET SECURITY GATE: This changeset contains a security advisory identifier (matched: '${MATCHED_PATTERN}').
|
|
70
|
+
|
|
71
|
+
Do NOT reference GHSA IDs or CVE numbers in changeset files before the advisory is published.
|
|
72
|
+
Changeset files are committed to git — this creates pre-disclosure in public history and CHANGELOG.
|
|
73
|
+
|
|
74
|
+
CORRECT approach for security fix changesets:
|
|
75
|
+
Use vague language only — no identifiers, no vulnerability details.
|
|
76
|
+
|
|
77
|
+
WRONG: 'fix(hooks): patch GHSA-3w3m-7gg4-f82g — symlink-guard now covers Edit tool'
|
|
78
|
+
RIGHT: 'security: extend symlink protection to cover all write-capable tools'
|
|
79
|
+
|
|
80
|
+
WRONG: 'security: fix CVE-2026-1234 prompt injection via tool descriptions'
|
|
81
|
+
RIGHT: 'security: harden middleware chain against indirect instruction attacks'
|
|
82
|
+
|
|
83
|
+
After the release ships:
|
|
84
|
+
1. Publish the GitHub Security Advisory (Security tab → Advisories → Publish)
|
|
85
|
+
2. The GHSA becomes the detailed public disclosure document
|
|
86
|
+
3. Optionally update CHANGELOG.md post-publish to add the GHSA reference"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# ─── 2. FRONTMATTER VALIDATION ───────────────────────────────────────────────
|
|
90
|
+
#
|
|
91
|
+
# A changeset without valid frontmatter is silently ignored by the changesets
|
|
92
|
+
# tool — the package bump and CHANGELOG entry never appear in the release.
|
|
93
|
+
|
|
94
|
+
# Must start with ---
|
|
95
|
+
if ! echo "$CONTENT" | head -1 | grep -qE '^---'; then
|
|
96
|
+
json_output "block" \
|
|
97
|
+
"CHANGESET FORMAT GATE: Missing frontmatter block.
|
|
98
|
+
|
|
99
|
+
Every changeset must start with a frontmatter block specifying which package to bump:
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
'@bookedsolid/rea': patch
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
Brief description of what changed and why (close #N if applicable).
|
|
106
|
+
|
|
107
|
+
Bump types: patch (bug fix/security), minor (new feature), major (breaking change)"
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# Must have at least one package bump entry and a closing ---
|
|
111
|
+
FRONTMATTER=$(echo "$CONTENT" | awk '/^---/{count++; if(count==2){exit} next} count==1{print}')
|
|
112
|
+
if ! echo "$FRONTMATTER" | grep -qE "^'.+': (patch|minor|major)"; then
|
|
113
|
+
json_output "block" \
|
|
114
|
+
"CHANGESET FORMAT GATE: Frontmatter does not contain a valid package bump entry.
|
|
115
|
+
|
|
116
|
+
The frontmatter must include at least one package/bump pair:
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
'@bookedsolid/rea': patch
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
Valid bump types: patch | minor | major"
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# Must have a non-empty description after the closing ---
|
|
126
|
+
DESCRIPTION=$(echo "$CONTENT" | awk 'BEGIN{count=0} /^---/{count++; next} count>=2{print}' | grep -v '^[[:space:]]*$' | head -1 || true)
|
|
127
|
+
if [[ -z "$DESCRIPTION" ]]; then
|
|
128
|
+
json_output "block" \
|
|
129
|
+
"CHANGESET FORMAT GATE: Missing description after frontmatter.
|
|
130
|
+
|
|
131
|
+
Add a meaningful description explaining what changed and why:
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
'@bookedsolid/rea': patch
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
fix(gateway): policy-loader now uses async I/O with 500ms TTL cache
|
|
138
|
+
|
|
139
|
+
Previously, loadPolicy used fs.readFileSync on every tool invocation, blocking
|
|
140
|
+
the event loop under concurrency. Closes #34."
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
exit 0
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: commit-review-gate.sh
|
|
3
|
+
# Fires BEFORE every Bash tool call that matches "git commit".
|
|
4
|
+
# Implements a triage-based review gate:
|
|
5
|
+
# - trivial (<20 changed lines, non-sensitive paths) → pass immediately
|
|
6
|
+
# - standard (20-200 lines) → check review cache, pass if cached
|
|
7
|
+
# - significant (>200 lines or sensitive paths) → block, request agent review
|
|
8
|
+
#
|
|
9
|
+
# Exit codes:
|
|
10
|
+
# 0 = allow (trivial change, or cached review found)
|
|
11
|
+
# 2 = block (needs review — returns additionalContext for agent)
|
|
12
|
+
|
|
13
|
+
set -uo pipefail
|
|
14
|
+
|
|
15
|
+
# ── 1. Read ALL stdin immediately ─────────────────────────────────────────────
|
|
16
|
+
INPUT=$(cat)
|
|
17
|
+
|
|
18
|
+
# ── 2. Dependency check ──────────────────────────────────────────────────────
|
|
19
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
20
|
+
printf 'REA ERROR: jq is required but not installed.\n' >&2
|
|
21
|
+
printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
|
|
22
|
+
exit 2
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# ── 3. HALT check ────────────────────────────────────────────────────────────
|
|
26
|
+
REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
27
|
+
HALT_FILE="${REA_ROOT}/.rea/HALT"
|
|
28
|
+
if [ -f "$HALT_FILE" ]; then
|
|
29
|
+
printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
|
|
30
|
+
"$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
|
|
31
|
+
exit 2
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# ── 4. Parse command ──────────────────────────────────────────────────────────
|
|
35
|
+
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
36
|
+
|
|
37
|
+
if [[ -z "$CMD" ]]; then
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Only trigger on git commit commands
|
|
42
|
+
if ! printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+commit'; then
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Skip --amend (reviewing amendments is a future feature)
|
|
47
|
+
if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+commit.*--amend'; then
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# ── 5. Check if quality gates are enabled ─────────────────────────────────────
|
|
52
|
+
# Fail-open if policy doesn't exist or doesn't have quality_gates
|
|
53
|
+
POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
|
|
54
|
+
if [[ -f "$POLICY_FILE" ]]; then
|
|
55
|
+
if grep -qE '^quality_gates:' "$POLICY_FILE" 2>/dev/null; then
|
|
56
|
+
if grep -qE 'commit_review:[[:space:]]*false' "$POLICY_FILE" 2>/dev/null; then
|
|
57
|
+
exit 0
|
|
58
|
+
fi
|
|
59
|
+
fi
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# ── 6. Compute diff stats ────────────────────────────────────────────────────
|
|
63
|
+
# Get staged diff (what would be committed)
|
|
64
|
+
DIFF_OUTPUT=$(cd "$REA_ROOT" && git diff --cached --stat 2>/dev/null || echo "")
|
|
65
|
+
DIFF_FULL=$(cd "$REA_ROOT" && git diff --cached 2>/dev/null || echo "")
|
|
66
|
+
|
|
67
|
+
if [[ -z "$DIFF_OUTPUT" ]]; then
|
|
68
|
+
# No staged changes — let git commit handle the error
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# Count changed lines (additions + deletions)
|
|
73
|
+
LINE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -cE '^\+[^+]|^-[^-]' 2>/dev/null || echo "0")
|
|
74
|
+
|
|
75
|
+
# Check for sensitive paths
|
|
76
|
+
SENSITIVE=0
|
|
77
|
+
SENSITIVE_FILES=""
|
|
78
|
+
if printf '%s' "$DIFF_FULL" | grep -qE '^\+\+\+ .*(\.rea/|\.claude/|\.env|auth|security|\.github/workflows)'; then
|
|
79
|
+
SENSITIVE=1
|
|
80
|
+
SENSITIVE_FILES=$(printf '%s' "$DIFF_FULL" | grep -oE '^\+\+\+ .*(\.rea/|\.claude/|\.env|auth|security|\.github/workflows)[^ ]*' | sed 's/^\+\+\+ [ab]\// /' | head -5)
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# ── 7. Triage scoring ────────────────────────────────────────────────────────
|
|
84
|
+
TRIVIAL_THRESHOLD=20
|
|
85
|
+
SIGNIFICANT_THRESHOLD=200
|
|
86
|
+
|
|
87
|
+
if [[ $SENSITIVE -eq 1 ]] || [[ $LINE_COUNT -gt $SIGNIFICANT_THRESHOLD ]]; then
|
|
88
|
+
SCORE="significant"
|
|
89
|
+
elif [[ $LINE_COUNT -ge $TRIVIAL_THRESHOLD ]]; then
|
|
90
|
+
SCORE="standard"
|
|
91
|
+
else
|
|
92
|
+
SCORE="trivial"
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# ── 8. Trivial → pass immediately ─────────────────────────────────────────────
|
|
96
|
+
if [[ "$SCORE" == "trivial" ]]; then
|
|
97
|
+
exit 0
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# ── 9. Resolve rea CLI ────────────────────────────────────────────────────
|
|
101
|
+
# Try local installs first, then dist build, then global PATH install.
|
|
102
|
+
REA_CLI_ARGS=()
|
|
103
|
+
if [[ -f "${REA_ROOT}/node_modules/.bin/rea" ]]; then
|
|
104
|
+
REA_CLI_ARGS=(node "${REA_ROOT}/node_modules/.bin/rea")
|
|
105
|
+
elif [[ -f "${REA_ROOT}/dist/cli/index.js" ]]; then
|
|
106
|
+
REA_CLI_ARGS=(node "${REA_ROOT}/dist/cli/index.js")
|
|
107
|
+
elif command -v rea >/dev/null 2>&1; then
|
|
108
|
+
REA_CLI_ARGS=(rea)
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# ── 10. Check review cache for all non-trivial commits ────────────────────────
|
|
112
|
+
# Compute SHA and branch here so both standard and significant tiers share them.
|
|
113
|
+
STAGED_SHA=$(cd "$REA_ROOT" && git diff --cached | shasum -a 256 | cut -d' ' -f1 2>/dev/null || echo "")
|
|
114
|
+
BRANCH=$(cd "$REA_ROOT" && git branch --show-current 2>/dev/null || echo "")
|
|
115
|
+
CACHE_FILE="${REA_ROOT}/.rea/review-cache.json"
|
|
116
|
+
|
|
117
|
+
if [[ -n "$STAGED_SHA" ]]; then
|
|
118
|
+
CACHE_HIT=false
|
|
119
|
+
|
|
120
|
+
# Primary: use CLI when available — handles TTL, expiry, and branch-scoped keys
|
|
121
|
+
if [[ ${#REA_CLI_ARGS[@]} -gt 0 ]]; then
|
|
122
|
+
CACHE_RESULT=$("${REA_CLI_ARGS[@]}" cache check "$STAGED_SHA" --branch "$BRANCH" 2>/dev/null || echo '{"hit":false}')
|
|
123
|
+
if printf '%s' "$CACHE_RESULT" | jq -e '.hit == true' >/dev/null 2>&1; then
|
|
124
|
+
CACHE_HIT=true
|
|
125
|
+
fi
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# Fallback: read cache JSON directly — works when rea is not on PATH.
|
|
129
|
+
# Checks branch-scoped key ("branch:sha") first, then bare SHA (empty-branch case).
|
|
130
|
+
if [[ "$CACHE_HIT" == "false" ]] && [[ -f "$CACHE_FILE" ]]; then
|
|
131
|
+
CACHE_KEY="${BRANCH}:${STAGED_SHA}"
|
|
132
|
+
DIRECT_HIT=$(jq -r --arg k1 "$CACHE_KEY" --arg k2 "$STAGED_SHA" \
|
|
133
|
+
'(.entries[$k1] // .entries[$k2]) | if . == null then "miss" elif .result == "pass" then "hit" else "miss" end' \
|
|
134
|
+
"$CACHE_FILE" 2>/dev/null || echo "miss")
|
|
135
|
+
if [[ "$DIRECT_HIT" == "hit" ]]; then
|
|
136
|
+
CACHE_HIT=true
|
|
137
|
+
fi
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
if [[ "$CACHE_HIT" == "true" ]]; then
|
|
141
|
+
exit 0
|
|
142
|
+
fi
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# ── 11. Block and request review ──────────────────────────────────────────────
|
|
146
|
+
{
|
|
147
|
+
printf 'COMMIT REVIEW GATE: Review required before committing\n'
|
|
148
|
+
printf '\n'
|
|
149
|
+
printf ' Score: %s (%s changed lines)\n' "$SCORE" "$LINE_COUNT"
|
|
150
|
+
if [[ $SENSITIVE -eq 1 ]]; then
|
|
151
|
+
printf ' Sensitive paths detected:\n'
|
|
152
|
+
printf '%s\n' "$SENSITIVE_FILES"
|
|
153
|
+
fi
|
|
154
|
+
printf '\n'
|
|
155
|
+
printf ' YOU (the agent) are the reviewer. Do not ask the user to commit manually.\n'
|
|
156
|
+
printf ' Review the staged diff, make a pass/fail decision, then proceed:\n'
|
|
157
|
+
printf '\n'
|
|
158
|
+
printf ' 1. Inspect: git diff --cached\n'
|
|
159
|
+
printf ' 2. Decide: Is this safe to commit? (initial commits, refactors, and\n'
|
|
160
|
+
printf ' feature work are normal — use judgement, not ceremony)\n'
|
|
161
|
+
printf ' 3. Approve: rea cache set %s pass\n' "$STAGED_SHA"
|
|
162
|
+
printf ' 4. Retry the git commit command\n'
|
|
163
|
+
printf '\n'
|
|
164
|
+
printf ' Only escalate to the user if you find a genuine problem in the diff.\n'
|
|
165
|
+
} >&2
|
|
166
|
+
exit 2
|