@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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +339 -0
  3. package/SECURITY.md +104 -0
  4. package/THREAT_MODEL.md +245 -0
  5. package/agents/accessibility-engineer.md +101 -0
  6. package/agents/backend-engineer.md +126 -0
  7. package/agents/code-reviewer.md +144 -0
  8. package/agents/codex-adversarial.md +107 -0
  9. package/agents/frontend-specialist.md +84 -0
  10. package/agents/qa-engineer.md +138 -0
  11. package/agents/rea-orchestrator.md +101 -0
  12. package/agents/security-engineer.md +108 -0
  13. package/agents/technical-writer.md +140 -0
  14. package/agents/typescript-specialist.md +111 -0
  15. package/commands/codex-review.md +104 -0
  16. package/commands/freeze.md +81 -0
  17. package/commands/halt-check.md +120 -0
  18. package/commands/rea.md +52 -0
  19. package/commands/review.md +79 -0
  20. package/dist/cli/check.d.ts +1 -0
  21. package/dist/cli/check.js +66 -0
  22. package/dist/cli/doctor.d.ts +1 -0
  23. package/dist/cli/doctor.js +93 -0
  24. package/dist/cli/freeze.d.ts +8 -0
  25. package/dist/cli/freeze.js +61 -0
  26. package/dist/cli/index.d.ts +2 -0
  27. package/dist/cli/index.js +65 -0
  28. package/dist/cli/init.d.ts +6 -0
  29. package/dist/cli/init.js +237 -0
  30. package/dist/cli/serve.d.ts +1 -0
  31. package/dist/cli/serve.js +19 -0
  32. package/dist/cli/utils.d.ts +23 -0
  33. package/dist/cli/utils.js +51 -0
  34. package/dist/config/tier-map.d.ts +11 -0
  35. package/dist/config/tier-map.js +108 -0
  36. package/dist/config/types.d.ts +24 -0
  37. package/dist/config/types.js +1 -0
  38. package/dist/gateway/circuit-breaker.d.ts +43 -0
  39. package/dist/gateway/circuit-breaker.js +86 -0
  40. package/dist/gateway/middleware/audit-types.d.ts +16 -0
  41. package/dist/gateway/middleware/audit-types.js +1 -0
  42. package/dist/gateway/middleware/audit.d.ts +12 -0
  43. package/dist/gateway/middleware/audit.js +98 -0
  44. package/dist/gateway/middleware/blocked-paths.d.ts +12 -0
  45. package/dist/gateway/middleware/blocked-paths.js +117 -0
  46. package/dist/gateway/middleware/chain.d.ts +28 -0
  47. package/dist/gateway/middleware/chain.js +40 -0
  48. package/dist/gateway/middleware/circuit-breaker.d.ts +11 -0
  49. package/dist/gateway/middleware/circuit-breaker.js +43 -0
  50. package/dist/gateway/middleware/injection.d.ts +22 -0
  51. package/dist/gateway/middleware/injection.js +128 -0
  52. package/dist/gateway/middleware/kill-switch.d.ts +10 -0
  53. package/dist/gateway/middleware/kill-switch.js +58 -0
  54. package/dist/gateway/middleware/policy.d.ts +12 -0
  55. package/dist/gateway/middleware/policy.js +70 -0
  56. package/dist/gateway/middleware/rate-limit.d.ts +12 -0
  57. package/dist/gateway/middleware/rate-limit.js +31 -0
  58. package/dist/gateway/middleware/redact.d.ts +16 -0
  59. package/dist/gateway/middleware/redact.js +128 -0
  60. package/dist/gateway/middleware/result-size-cap.d.ts +13 -0
  61. package/dist/gateway/middleware/result-size-cap.js +48 -0
  62. package/dist/gateway/middleware/session.d.ts +10 -0
  63. package/dist/gateway/middleware/session.js +18 -0
  64. package/dist/gateway/middleware/tier.d.ts +6 -0
  65. package/dist/gateway/middleware/tier.js +10 -0
  66. package/dist/gateway/rate-limiter.d.ts +36 -0
  67. package/dist/gateway/rate-limiter.js +75 -0
  68. package/dist/index.d.ts +3 -0
  69. package/dist/index.js +2 -0
  70. package/dist/policy/loader.d.ts +80 -0
  71. package/dist/policy/loader.js +146 -0
  72. package/dist/policy/types.d.ts +34 -0
  73. package/dist/policy/types.js +19 -0
  74. package/hooks/_lib/common.sh +105 -0
  75. package/hooks/_lib/halt-check.sh +39 -0
  76. package/hooks/_lib/policy-read.sh +79 -0
  77. package/hooks/architecture-review-gate.sh +84 -0
  78. package/hooks/attribution-advisory.sh +126 -0
  79. package/hooks/blocked-paths-enforcer.sh +176 -0
  80. package/hooks/changeset-security-gate.sh +143 -0
  81. package/hooks/commit-review-gate.sh +166 -0
  82. package/hooks/dangerous-bash-interceptor.sh +362 -0
  83. package/hooks/dependency-audit-gate.sh +118 -0
  84. package/hooks/env-file-protection.sh +110 -0
  85. package/hooks/pr-issue-link-gate.sh +65 -0
  86. package/hooks/push-review-gate.sh +120 -0
  87. package/hooks/secret-scanner.sh +229 -0
  88. package/hooks/security-disclosure-gate.sh +146 -0
  89. package/hooks/settings-protection.sh +147 -0
  90. package/package.json +93 -0
@@ -0,0 +1,120 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: push-review-gate.sh
3
+ # Fires BEFORE every Bash tool call that matches "git push".
4
+ # Runs a full diff analysis against the target branch and requests
5
+ # security + code review before allowing the push.
6
+ #
7
+ # Exit codes:
8
+ # 0 = allow (no meaningful diff, or review cached)
9
+ # 2 = block (needs review)
10
+
11
+ set -uo pipefail
12
+
13
+ # ── 1. Read ALL stdin immediately ─────────────────────────────────────────────
14
+ INPUT=$(cat)
15
+
16
+ # ── 2. Dependency check ──────────────────────────────────────────────────────
17
+ if ! command -v jq >/dev/null 2>&1; then
18
+ printf 'REA ERROR: jq is required but not installed.\n' >&2
19
+ printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
20
+ exit 2
21
+ fi
22
+
23
+ # ── 3. HALT check ────────────────────────────────────────────────────────────
24
+ REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
25
+ HALT_FILE="${REA_ROOT}/.rea/HALT"
26
+ if [ -f "$HALT_FILE" ]; then
27
+ printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
28
+ "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
29
+ exit 2
30
+ fi
31
+
32
+ # ── 4. Parse command ──────────────────────────────────────────────────────────
33
+ CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
34
+
35
+ if [[ -z "$CMD" ]]; then
36
+ exit 0
37
+ fi
38
+
39
+ # Only trigger on git push commands
40
+ if ! printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+push'; then
41
+ exit 0
42
+ fi
43
+
44
+ # ── 5. Check if quality gates are enabled ─────────────────────────────────────
45
+ POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
46
+ if [[ -f "$POLICY_FILE" ]]; then
47
+ if grep -qE 'push_review:[[:space:]]*false' "$POLICY_FILE" 2>/dev/null; then
48
+ exit 0
49
+ fi
50
+ fi
51
+
52
+ # ── 6. Determine target branch ───────────────────────────────────────────────
53
+ CURRENT_BRANCH=$(cd "$REA_ROOT" && git branch --show-current 2>/dev/null || echo "")
54
+ TARGET_BRANCH="main"
55
+
56
+ # Try to extract target from push command (git push origin <branch>)
57
+ PUSH_TARGET=$(printf '%s' "$CMD" | grep -oE 'git[[:space:]]+push[[:space:]]+[a-zA-Z_-]+[[:space:]]+([a-zA-Z0-9/_-]+)' | awk '{print $NF}' 2>/dev/null || echo "")
58
+ if [[ -n "$PUSH_TARGET" ]]; then
59
+ TARGET_BRANCH="$PUSH_TARGET"
60
+ fi
61
+
62
+ # ── 7. Get diff against target ───────────────────────────────────────────────
63
+ MERGE_BASE=$(cd "$REA_ROOT" && git merge-base "$TARGET_BRANCH" HEAD 2>/dev/null || echo "")
64
+
65
+ if [[ -z "$MERGE_BASE" ]]; then
66
+ # Can't determine merge base — fail-open
67
+ exit 0
68
+ fi
69
+
70
+ DIFF_FULL=$(cd "$REA_ROOT" && git diff "$MERGE_BASE"...HEAD 2>/dev/null || echo "")
71
+
72
+ if [[ -z "$DIFF_FULL" ]]; then
73
+ # No diff — nothing to review
74
+ exit 0
75
+ fi
76
+
77
+ LINE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -cE '^\+[^+]|^-[^-]' 2>/dev/null || echo "0")
78
+
79
+ # ── 8. Check review cache ────────────────────────────────────────────────────
80
+ PUSH_SHA=$(printf '%s' "$DIFF_FULL" | shasum -a 256 | cut -d' ' -f1 2>/dev/null || echo "")
81
+
82
+ # Resolve rea CLI (node_modules/.bin first, dist fallback)
83
+ REA_CLI_ARGS=()
84
+ if [[ -f "${REA_ROOT}/node_modules/.bin/rea" ]]; then
85
+ REA_CLI_ARGS=(node "${REA_ROOT}/node_modules/.bin/rea")
86
+ elif [[ -f "${REA_ROOT}/dist/cli/index.js" ]]; then
87
+ REA_CLI_ARGS=(node "${REA_ROOT}/dist/cli/index.js")
88
+ fi
89
+
90
+ if [[ -n "$PUSH_SHA" ]] && [[ ${#REA_CLI_ARGS[@]} -gt 0 ]]; then
91
+ CACHE_RESULT=$("${REA_CLI_ARGS[@]}" cache check "$PUSH_SHA" --branch "$CURRENT_BRANCH" --base "$TARGET_BRANCH" 2>/dev/null || echo '{"hit":false}')
92
+ if printf '%s' "$CACHE_RESULT" | jq -e '.hit == true' >/dev/null 2>&1; then
93
+ # Review was already approved — notify and allow the push through
94
+ DISCORD_LIB="${REA_ROOT}/hooks/_lib/discord.sh"
95
+ if [ -f "$DISCORD_LIB" ]; then
96
+ # shellcheck source=/dev/null
97
+ source "$DISCORD_LIB"
98
+ discord_notify "dev" "Push passed quality gates on \`${CURRENT_BRANCH}\` -- $(cd "$REA_ROOT" && git log -1 --oneline 2>/dev/null)" "green"
99
+ fi
100
+ exit 0
101
+ fi
102
+ fi
103
+
104
+ # ── 9. Block and request review ──────────────────────────────────────────────
105
+ FILE_COUNT=$(printf '%s' "$DIFF_FULL" | grep -c '^\+\+\+ ' 2>/dev/null || echo "0")
106
+
107
+ {
108
+ printf 'PUSH REVIEW GATE: Review required before pushing\n'
109
+ printf '\n'
110
+ printf ' Branch: %s → %s\n' "$CURRENT_BRANCH" "$TARGET_BRANCH"
111
+ printf ' Scope: %s files changed, %s lines\n' "$FILE_COUNT" "$LINE_COUNT"
112
+ printf '\n'
113
+ printf ' Action required:\n'
114
+ printf ' 1. Spawn a code-reviewer agent to review: git diff %s...HEAD\n' "$MERGE_BASE"
115
+ printf ' 2. Spawn a security-engineer agent for security review\n'
116
+ printf ' 3. After both pass, cache the result:\n'
117
+ printf ' rea cache set %s pass --branch %s --base %s\n' "$PUSH_SHA" "$CURRENT_BRANCH" "$TARGET_BRANCH"
118
+ printf '\n'
119
+ } >&2
120
+ exit 2
@@ -0,0 +1,229 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: secret-scanner.sh
3
+ # Fires BEFORE every Write or Edit tool call.
4
+ # Scans content about to be written for credential patterns and blocks (exit 2)
5
+ # if real secrets are detected — before they ever touch disk.
6
+ #
7
+ # Content extraction:
8
+ # Write tool → tool_input.content
9
+ # Edit tool → tool_input.new_string
10
+ #
11
+ # NOTE: This hook is a last-resort pre-write guard. The primary secret gate is
12
+ # gitleaks running in the pre-commit hook. This hook stops obvious credentials
13
+ # before they hit disk. It cannot catch all encoding tricks — rely on gitleaks
14
+ # for comprehensive coverage.
15
+ #
16
+ # Exit codes:
17
+ # 0 = no secrets detected — allow the tool to proceed
18
+ # 2 = secrets detected — block the tool call
19
+
20
+ set -uo pipefail
21
+
22
+ INPUT=$(cat)
23
+
24
+ # ── Dependency check ──────────────────────────────────────────────────────────
25
+ if ! command -v jq >/dev/null 2>&1; then
26
+ printf 'REA ERROR: jq is required but not installed.\n' >&2
27
+ printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
28
+ exit 2
29
+ fi
30
+
31
+ # ── HALT check ────────────────────────────────────────────────────────────────
32
+ REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
33
+ HALT_FILE="${REA_ROOT}/.rea/HALT"
34
+ if [ -f "$HALT_FILE" ]; then
35
+ printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
36
+ "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
37
+ exit 2
38
+ fi
39
+
40
+ FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
41
+ CONTENT_WRITE=$(printf '%s' "$INPUT" | jq -r '.tool_input.content // empty' 2>/dev/null)
42
+ CONTENT_EDIT=$(printf '%s' "$INPUT" | jq -r '.tool_input.new_string // empty' 2>/dev/null)
43
+
44
+ if [[ -n "$CONTENT_WRITE" ]]; then
45
+ CONTENT="$CONTENT_WRITE"
46
+ elif [[ -n "$CONTENT_EDIT" ]]; then
47
+ CONTENT="$CONTENT_EDIT"
48
+ else
49
+ exit 0
50
+ fi
51
+
52
+ # Smart file-path exclusions (suffix-based only — no directory exclusions)
53
+ if [[ -n "$FILE_PATH" ]]; then
54
+ if [[ "$FILE_PATH" == *.env.example || "$FILE_PATH" == *.env.sample ]]; then
55
+ exit 0
56
+ fi
57
+ # Test files are NOT excluded — real secrets in test files must be caught.
58
+ # The is_placeholder() function handles false positives from test fixtures.
59
+ fi
60
+
61
+ # Build line-filtered content
62
+ # Strip: shell comment lines (#) and lines where process.env.VAR is the RHS of an assignment
63
+ # NOT stripped: lines that merely mention process.env somewhere (bypass vector if too broad)
64
+ FILTERED_FILE=$(mktemp "${TMPDIR:-/tmp}/rea-secret-scan-XXXXXX") || {
65
+ printf 'SECRET-SCAN ERROR: Failed to create temp file — blocking write (fail-secure)\n' >&2
66
+ exit 2
67
+ }
68
+
69
+ VIOLATIONS_FILE=""
70
+
71
+ cleanup() {
72
+ rm -f "$FILTERED_FILE"
73
+ [[ -n "$VIOLATIONS_FILE" ]] && rm -f "$VIOLATIONS_FILE"
74
+ }
75
+ trap cleanup EXIT
76
+
77
+ printf '%s' "$CONTENT" | awk '
78
+ {
79
+ line = $0
80
+ trimmed = line
81
+ sub(/^[[:space:]]+/, "", trimmed)
82
+ # Skip shell comment lines only
83
+ if (substr(trimmed, 1, 1) == "#") next
84
+ # Skip lines where process.env.VAR is the RHS of an assignment
85
+ # Pattern: = process.env.SOMETHING (not just any mention of process.env)
86
+ if (trimmed ~ /=[[:space:]]*process\.env\.[A-Z_]+[^a-zA-Z]?$/) next
87
+ if (trimmed ~ /=[[:space:]]*process\.env\.[A-Z_]+[[:space:]]*[;,)]/) next
88
+ if (trimmed ~ /os\.environ\[/) next
89
+ print line
90
+ }
91
+ ' > "$FILTERED_FILE" 2>/dev/null
92
+
93
+ if [[ ! -s "$FILTERED_FILE" ]]; then
94
+ exit 0
95
+ fi
96
+
97
+ is_placeholder() {
98
+ local MATCH
99
+ MATCH=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')
100
+ [[ "$MATCH" =~ \<[a-z_]+\> ]] && return 0
101
+ [[ "$MATCH" =~ your_key_here ]] && return 0
102
+ [[ "$MATCH" =~ your_api_key ]] && return 0
103
+ [[ "$MATCH" =~ your_secret ]] && return 0
104
+ [[ "$MATCH" =~ placeholder ]] && return 0
105
+ [[ "$MATCH" =~ changeme ]] && return 0
106
+ [[ "$MATCH" =~ insert.*here ]] && return 0
107
+ # Prefix checks: require full placeholder compound, not just a prefix
108
+ [[ "$MATCH" =~ ^(test|fake|mock|demo|example)_(key|token|secret|credential|api)$ ]] && return 0
109
+ [[ "$MATCH" =~ ^test_[a-z_]+_key$ ]] && return 0
110
+ # Repeated-character dummies (aaaaaaa, 1111111, etc.)
111
+ if printf '%s' "$MATCH" | grep -qE '^(.)\1{7,}$'; then return 0; fi
112
+ return 1
113
+ }
114
+
115
+ VIOLATIONS_FILE=$(mktemp "${TMPDIR:-/tmp}/rea-secret-violations-XXXXXX") || {
116
+ printf 'SECRET-SCAN ERROR: Failed to create violations file — blocking write (fail-secure)\n' >&2
117
+ exit 2
118
+ }
119
+
120
+ scan_pattern() {
121
+ local SEVERITY="$1"
122
+ local LABEL="$2"
123
+ local PATTERN="$3"
124
+ local MATCHES GREP_EXIT MATCH SNIPPET
125
+ MATCHES=$(grep -oE -e "$PATTERN" "$FILTERED_FILE" 2>/dev/null)
126
+ GREP_EXIT=$?
127
+ [[ $GREP_EXIT -ne 0 ]] && return 0
128
+ [[ -z "$MATCHES" ]] && return 0
129
+ MATCHES=$(printf '%s\n' "$MATCHES" | head -5)
130
+ while IFS= read -r MATCH; do
131
+ [[ -z "$MATCH" ]] && continue
132
+ if is_placeholder "$MATCH"; then continue; fi
133
+ if [[ ${#MATCH} -gt 60 ]]; then
134
+ SNIPPET="${MATCH:0:60}..."
135
+ else
136
+ SNIPPET="$MATCH"
137
+ fi
138
+ printf '%s|%s|%s\n' "$SEVERITY" "$LABEL" "$SNIPPET" >> "$VIOLATIONS_FILE"
139
+ done <<< "$MATCHES"
140
+ }
141
+
142
+ # ── HIGH severity patterns ─────────────────────────────────────────────────────
143
+
144
+ scan_pattern "HIGH" "AWS Access Key ID" \
145
+ 'AKIA[0-9A-Z]{16}'
146
+
147
+ scan_pattern "HIGH" "AWS Secret Access Key" \
148
+ '[Aa][Ww][Ss]_SECRET_ACCESS_KEY[[:space:]]*=[[:space:]]*[A-Za-z0-9/+]{40}'
149
+
150
+ scan_pattern "HIGH" "Private key block" \
151
+ '-----BEGIN (RSA|EC|OPENSSH|PGP) PRIVATE KEY-----'
152
+
153
+ scan_pattern "HIGH" "Anthropic API key" \
154
+ 'sk-ant-api03-[A-Za-z0-9_-]{93}'
155
+
156
+ scan_pattern "HIGH" "Anthropic OAuth token" \
157
+ 'sk-ant-oat01-[A-Za-z0-9_-]{86}'
158
+
159
+ scan_pattern "HIGH" "GitHub classic Personal Access Token" \
160
+ 'gh[puors]_[A-Za-z0-9]{36}'
161
+
162
+ scan_pattern "HIGH" "GitHub fine-grained Personal Access Token" \
163
+ 'github_pat_[A-Za-z0-9_]{82}'
164
+
165
+ scan_pattern "HIGH" "Stripe live secret/restricted key" \
166
+ '(sk|rk)_live_[A-Za-z0-9]{24,}'
167
+
168
+ scan_pattern "HIGH" "Stripe webhook signing secret" \
169
+ 'whsec_[A-Za-z0-9+/]{40,}'
170
+
171
+ scan_pattern "HIGH" "Generic secret assignment (double-quoted)" \
172
+ '(SECRET|PASSWORD|PRIVATE_KEY|API_SECRET)[[:space:]]*=[[:space:]]*"[^"]{20,}"'
173
+
174
+ scan_pattern "HIGH" "Generic secret assignment (single-quoted)" \
175
+ "(SECRET|PASSWORD|PRIVATE_KEY|API_SECRET)[[:space:]]*=[[:space:]]*'[^']{20,}'"
176
+
177
+ scan_pattern "HIGH" "Supabase service role key (JWT)" \
178
+ 'SUPABASE_SERVICE_ROLE_KEY[[:space:]]*=[[:space:]]*["\'"'"']eyJ[A-Za-z0-9._-]{50,}'
179
+
180
+ # ── MEDIUM severity patterns ───────────────────────────────────────────────────
181
+
182
+ scan_pattern "MEDIUM" ".env credential assignment" \
183
+ '^(ANTHROPIC_API_KEY|SUPABASE_SERVICE_ROLE_KEY|DATABASE_URL|STRIPE_SECRET)[[:space:]]*=[[:space:]]*[^[:space:]]+'
184
+
185
+ scan_pattern "MEDIUM" "Stripe test API key (real credential, test env)" \
186
+ '(sk|pk|rk)_test_[A-Za-z0-9]{24,}'
187
+
188
+ scan_pattern "MEDIUM" "Stripe live publishable key" \
189
+ 'pk_live_[A-Za-z0-9]{24,}'
190
+
191
+ scan_pattern "MEDIUM" "Hardcoded DB connection string with password" \
192
+ 'postgresql://[^:]+:[^@]{8,}@'
193
+
194
+ scan_pattern "MEDIUM" "Supabase anon key in non-client context" \
195
+ 'SUPABASE_ANON_KEY[[:space:]]*=[[:space:]]*["\'"'"']eyJ[A-Za-z0-9._-]{50,}'
196
+
197
+ if [[ ! -s "$VIOLATIONS_FILE" ]]; then
198
+ exit 0
199
+ fi
200
+
201
+ FILE_BASENAME=$(basename "${FILE_PATH:-unknown}")
202
+ HIGH_COUNT=$(grep -cF 'HIGH|' "$VIOLATIONS_FILE" 2>/dev/null || true)
203
+ : "${HIGH_COUNT:=0}"
204
+
205
+ if [[ "$HIGH_COUNT" -gt 0 ]]; then
206
+ {
207
+ printf 'SECRET DETECTED: Potential credential in %s\n' "$FILE_BASENAME"
208
+ COUNT=0
209
+ while IFS='|' read -r SEVERITY LABEL SNIPPET; do
210
+ [[ -z "$SEVERITY" ]] && continue
211
+ COUNT=$(( COUNT + 1 ))
212
+ if [[ $COUNT -gt 5 ]]; then break; fi
213
+ printf ' %s: %s — '"'"'%s'"'"'\n' "$SEVERITY" "$LABEL" "$SNIPPET"
214
+ done < "$VIOLATIONS_FILE"
215
+ printf 'Block reason: Writing credentials to disk risks exposure via git history.\n'
216
+ printf 'Fix: Load credentials from environment variables — never hardcode secrets.\n'
217
+ } >&2
218
+ exit 2
219
+ fi
220
+
221
+ {
222
+ printf 'SECRET-SCAN WARN: Low-confidence credential pattern in %s (advisory — not blocking)\n' "$FILE_BASENAME"
223
+ while IFS='|' read -r SEVERITY LABEL SNIPPET; do
224
+ [[ -z "$SEVERITY" ]] && continue
225
+ printf ' %s: %s — '"'"'%s'"'"'\n' "$SEVERITY" "$LABEL" "$SNIPPET"
226
+ done < "$VIOLATIONS_FILE"
227
+ printf 'Note: Heuristic match — may be a false positive. If real, load from environment.\n'
228
+ } >&2
229
+ exit 0
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env bash
2
+ # security-disclosure-gate.sh — PreToolUse: Bash
3
+ #
4
+ # Intercepts `gh issue create` commands that contain security-sensitive
5
+ # keywords and blocks them. Routing depends on REA_DISCLOSURE_MODE:
6
+ #
7
+ # advisory (default) — redirect to GitHub Security Advisories (private)
8
+ # Use for public OSS repos
9
+ # issues — redirect to gh issue create with security + internal labels
10
+ # Use for permanently private client repos
11
+ # disabled — pass through (not recommended)
12
+ #
13
+ # Set REA_DISCLOSURE_MODE in .rea/policy.yaml (written to settings.json
14
+ # env by rea init). Defaults to "advisory" when unset.
15
+ #
16
+ # Triggered by: PreToolUse — Bash tool
17
+
18
+ set -euo pipefail
19
+
20
+ # shellcheck source=_lib/common.sh
21
+ source "$(dirname "$0")/_lib/common.sh"
22
+
23
+ check_halt
24
+
25
+ # Read disclosure mode — default to advisory
26
+ DISCLOSURE_MODE="${REA_DISCLOSURE_MODE:-advisory}"
27
+
28
+ # Disabled mode: pass through entirely
29
+ if [[ "$DISCLOSURE_MODE" == "disabled" ]]; then
30
+ exit 0
31
+ fi
32
+
33
+ INPUT="$(cat)"
34
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
35
+
36
+ if [[ "$TOOL_NAME" != "Bash" ]]; then
37
+ exit 0
38
+ fi
39
+
40
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
41
+
42
+ # Only intercept gh issue create
43
+ if ! echo "$COMMAND" | grep -qE 'gh\s+issue\s+create'; then
44
+ exit 0
45
+ fi
46
+
47
+ require_jq
48
+
49
+ # Security-sensitive keywords that should not appear in public issues —
50
+ # these terms suggest a vulnerability, exploit path, or bypass technique
51
+ SECURITY_PATTERNS=(
52
+ # Vulnerability classes
53
+ 'bypass'
54
+ 'exploit'
55
+ 'injection'
56
+ 'traversal'
57
+ 'exfiltrat'
58
+ 'escalat'
59
+ 'privilege'
60
+ 'rce'
61
+ 'remote.code.exec'
62
+ 'arbitrary.code'
63
+ 'code.execution'
64
+ 'zero.day'
65
+ '0day'
66
+ 'CVE-'
67
+ 'CVSS'
68
+ 'GHSA-'
69
+ # Reagent-specific sensitive terms
70
+ 'hook.bypass'
71
+ 'HALT.bypass'
72
+ 'redaction.bypass'
73
+ 'policy.bypass'
74
+ 'middleware.bypass'
75
+ 'skip.*gate'
76
+ 'evad'
77
+ # Credential/secret exposure
78
+ 'secret.*leak'
79
+ 'credential.*leak'
80
+ 'token.*leak'
81
+ 'key.*expos'
82
+ 'expos.*secret'
83
+ # Prompt injection
84
+ 'prompt.inject'
85
+ 'jailbreak'
86
+ 'jail.break'
87
+ )
88
+
89
+ # Scan the full command text (title + body + flags) for sensitive patterns
90
+ FULL_TEXT=$(echo "$COMMAND" | tr '[:upper:]' '[:lower:]')
91
+
92
+ MATCHED_PATTERN=""
93
+ for PATTERN in "${SECURITY_PATTERNS[@]}"; do
94
+ if echo "$FULL_TEXT" | grep -qiE "$PATTERN"; then
95
+ MATCHED_PATTERN="$PATTERN"
96
+ break
97
+ fi
98
+ done
99
+
100
+ if [[ -z "$MATCHED_PATTERN" ]]; then
101
+ exit 0
102
+ fi
103
+
104
+ # ─── Route based on disclosure mode ──────────────────────────────────────────
105
+
106
+ if [[ "$DISCLOSURE_MODE" == "issues" ]]; then
107
+ # Private repo mode: redirect to labeled internal issue
108
+ json_output "block" \
109
+ "SECURITY DISCLOSURE GATE: This issue appears to describe a security finding (matched: '${MATCHED_PATTERN}').
110
+
111
+ This project is configured for PRIVATE disclosure (REA_DISCLOSURE_MODE=issues).
112
+
113
+ CORRECT PATH for security findings in this private repo:
114
+ Use: gh issue create --label 'security,internal' --title '...' --body '...'
115
+
116
+ The 'security' and 'internal' labels keep this off public project boards and
117
+ mark it for maintainer-only triage. Do NOT use the public issue queue without
118
+ these labels for security findings.
119
+
120
+ If this is NOT a security finding, rephrase the title/body to avoid triggering
121
+ security patterns, then retry."
122
+
123
+ else
124
+ # Advisory mode (default): redirect to GitHub Security Advisories
125
+ json_output "block" \
126
+ "SECURITY DISCLOSURE GATE: This issue appears to describe a security vulnerability (matched: '${MATCHED_PATTERN}'). Do NOT create a public GitHub issue for security vulnerabilities.
127
+
128
+ CORRECT DISCLOSURE PATH:
129
+ 1. Use GitHub Security Advisories (private):
130
+ gh api repos/{owner}/{repo}/security-advisories --method POST --input - <<'JSON'
131
+ { \"summary\": \"...\", \"description\": \"...\", \"severity\": \"medium|high|critical\",
132
+ \"vulnerabilities\": [{\"package\": {\"name\": \"@pkg\", \"ecosystem\": \"npm\"}}] }
133
+ JSON
134
+ 2. Or navigate to: Security tab → Advisories → 'Report a vulnerability'
135
+ 3. Or email security@bookedsolid.tech (see SECURITY.md)
136
+
137
+ The finding will be publicly disclosed AFTER a patch is released (coordinated disclosure).
138
+
139
+ WHY: Public issues expose vulnerabilities before users can patch. This is enforced by the
140
+ security-disclosure-gate hook (REA_DISCLOSURE_MODE=${DISCLOSURE_MODE}).
141
+
142
+ If this is NOT a security vulnerability, rephrase the issue to avoid triggering
143
+ security patterns, then retry."
144
+ fi
145
+
146
+ exit 2
@@ -0,0 +1,147 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: settings-protection.sh
3
+ # Fires BEFORE every Write or Edit tool call.
4
+ # Blocks modifications to critical configuration files that, if tampered with,
5
+ # would disable the entire hook safety layer.
6
+ #
7
+ # Protected paths (security controls and hook infrastructure ONLY):
8
+ # .claude/settings.json — hook configuration
9
+ # .claude/settings.local.json — local hook overrides
10
+ # .claude/hooks/* — hook scripts themselves
11
+ # .husky/* — git hook scripts
12
+ # .rea/policy.yaml — autonomy/blocking policy
13
+ # .rea/HALT — kill switch file
14
+ #
15
+ # NOT protected (operational files agents may legitimately write):
16
+ # .rea/review-cache.json — cache file, writable by CLI and agents
17
+ # .rea/tasks.jsonl — task store, managed by task MCP tools
18
+ #
19
+ # Exit codes:
20
+ # 0 = allow (path not protected)
21
+ # 2 = block (protected path modification attempt)
22
+
23
+ set -uo pipefail
24
+
25
+ # ── 1. Read ALL stdin immediately ─────────────────────────────────────────────
26
+ INPUT=$(cat)
27
+
28
+ # ── 2. Dependency check ──────────────────────────────────────────────────────
29
+ if ! command -v jq >/dev/null 2>&1; then
30
+ printf 'REA ERROR: jq is required but not installed.\n' >&2
31
+ printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
32
+ exit 2
33
+ fi
34
+
35
+ # ── 3. HALT check ────────────────────────────────────────────────────────────
36
+ REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
37
+ HALT_FILE="${REA_ROOT}/.rea/HALT"
38
+ if [ -f "$HALT_FILE" ]; then
39
+ printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
40
+ "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
41
+ exit 2
42
+ fi
43
+
44
+ # ── 4. Extract file path from payload ─────────────────────────────────────────
45
+ FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
46
+
47
+ if [[ -z "$FILE_PATH" ]]; then
48
+ exit 0
49
+ fi
50
+
51
+ # ── 5. Normalize path for comparison ──────────────────────────────────────────
52
+ # Convert to relative path from project root for consistent matching
53
+ normalize_path() {
54
+ local p="$1"
55
+ local root="$REA_ROOT"
56
+
57
+ # Strip project root prefix if present
58
+ if [[ "$p" == "$root"/* ]]; then
59
+ p="${p#$root/}"
60
+ fi
61
+
62
+ # URL decode common sequences
63
+ p=$(printf '%s' "$p" | sed 's/%2[Ff]/\//g; s/%2[Ee]/./g; s/%20/ /g')
64
+
65
+ # Collapse path traversals
66
+ # Remove ./ components
67
+ p=$(printf '%s' "$p" | sed 's|\./||g')
68
+
69
+ # Remove leading ./
70
+ p="${p#./}"
71
+
72
+ printf '%s' "$p"
73
+ }
74
+
75
+ NORMALIZED=$(normalize_path "$FILE_PATH")
76
+
77
+ # ── 6. Protected path patterns ────────────────────────────────────────────────
78
+ PROTECTED_PATTERNS=(
79
+ '.claude/settings.json'
80
+ '.claude/settings.local.json'
81
+ '.claude/hooks/'
82
+ '.husky/'
83
+ '.rea/policy.yaml'
84
+ '.rea/HALT'
85
+ )
86
+
87
+ for pattern in "${PROTECTED_PATTERNS[@]}"; do
88
+ # Exact match
89
+ if [[ "$NORMALIZED" == "$pattern" ]]; then
90
+ {
91
+ printf 'SETTINGS PROTECTION: Modification blocked\n'
92
+ printf '\n'
93
+ printf ' File: %s\n' "$FILE_PATH"
94
+ printf ' Rule: This file is protected from agent modification.\n'
95
+ printf '\n'
96
+ printf ' Protected files include hook scripts, settings, policy,\n'
97
+ printf ' and kill switch files. These must be modified by humans\n'
98
+ printf ' via rea CLI or direct editing.\n'
99
+ printf '\n'
100
+ printf ' Use: rea init (to update hooks/settings)\n'
101
+ printf ' rea freeze/unfreeze (for HALT file)\n'
102
+ printf ' Edit .rea/policy.yaml manually\n'
103
+ } >&2
104
+ exit 2
105
+ fi
106
+
107
+ # Directory prefix match (patterns ending in /)
108
+ if [[ "$pattern" == */ ]] && [[ "$NORMALIZED" == "$pattern"* ]]; then
109
+ {
110
+ printf 'SETTINGS PROTECTION: Modification blocked\n'
111
+ printf '\n'
112
+ printf ' File: %s\n' "$FILE_PATH"
113
+ printf ' Rule: Files under %s are protected from agent modification.\n' "$pattern"
114
+ printf '\n'
115
+ printf ' These files control the hook safety layer and must be\n'
116
+ printf ' modified by humans via rea CLI or direct editing.\n'
117
+ } >&2
118
+ exit 2
119
+ fi
120
+ done
121
+
122
+ # ── 7. Case-insensitive fallback check ────────────────────────────────────────
123
+ # Catch case-manipulation bypass attempts (e.g., .Claude/Settings.json)
124
+ LOWER_NORM=$(printf '%s' "$NORMALIZED" | tr '[:upper:]' '[:lower:]')
125
+ for pattern in "${PROTECTED_PATTERNS[@]}"; do
126
+ LOWER_PATTERN=$(printf '%s' "$pattern" | tr '[:upper:]' '[:lower:]')
127
+ if [[ "$LOWER_NORM" == "$LOWER_PATTERN" ]]; then
128
+ {
129
+ printf 'SETTINGS PROTECTION: Modification blocked (case-insensitive match)\n'
130
+ printf '\n'
131
+ printf ' File: %s\n' "$FILE_PATH"
132
+ printf ' Matched: %s\n' "$pattern"
133
+ } >&2
134
+ exit 2
135
+ fi
136
+ if [[ "$LOWER_PATTERN" == */ ]] && [[ "$LOWER_NORM" == "$LOWER_PATTERN"* ]]; then
137
+ {
138
+ printf 'SETTINGS PROTECTION: Modification blocked (case-insensitive match)\n'
139
+ printf '\n'
140
+ printf ' File: %s\n' "$FILE_PATH"
141
+ printf ' Matched: %s*\n' "$pattern"
142
+ } >&2
143
+ exit 2
144
+ fi
145
+ done
146
+
147
+ exit 0