@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,362 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: dangerous-bash-interceptor.sh
3
+ # Fires BEFORE every Bash tool call.
4
+ # Detects destructive shell commands and blocks them (exit 2) or warns (exit 0).
5
+ #
6
+ # Compatible with: interactive sessions + headless Docker (no TTY required).
7
+ # All diagnostic output goes to stderr only.
8
+ #
9
+ # Content extraction:
10
+ # Bash tool → tool_input.command
11
+ #
12
+ # Exit codes:
13
+ # 0 = safe or advisory-only — allow the command to run
14
+ # 2 = HIGH severity danger detected — block the command with feedback
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. Parse tool_input.command from the hook payload ─────────────────────────
38
+ CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
39
+
40
+ if [[ -z "$CMD" ]]; then
41
+ exit 0
42
+ fi
43
+
44
+ # ── 5. Helper: truncate command for display ────────────────────────────────────
45
+ truncate_cmd() {
46
+ local STR="$1"
47
+ local MAX=200
48
+ if [[ ${#STR} -gt $MAX ]]; then
49
+ printf '%s' "${STR:0:$MAX}..."
50
+ else
51
+ printf '%s' "$STR"
52
+ fi
53
+ }
54
+
55
+ # ── 6. Violation accumulators ──────────────────────────────────────────────────
56
+ HIGH_FILE=$(mktemp "${TMPDIR:-/tmp}/rea-bash-high-XXXXXX")
57
+ MEDIUM_FILE=$(mktemp "${TMPDIR:-/tmp}/rea-bash-medium-XXXXXX")
58
+
59
+ cleanup_violations() {
60
+ rm -f "$HIGH_FILE" "$MEDIUM_FILE"
61
+ }
62
+ trap cleanup_violations EXIT
63
+
64
+ add_high() {
65
+ local LABEL="$1"
66
+ local DETAIL="$2"
67
+ shift 2
68
+ printf 'HIGH|%s|%s\n' "$LABEL" "$DETAIL" >> "$HIGH_FILE"
69
+ for ALT in "$@"; do
70
+ printf 'ALT:%s\n' "$ALT" >> "$HIGH_FILE"
71
+ done
72
+ printf 'END_VIOLATION\n' >> "$HIGH_FILE"
73
+ }
74
+
75
+ add_medium() {
76
+ local LABEL="$1"
77
+ local DETAIL="$2"
78
+ shift 2
79
+ printf 'MEDIUM|%s|%s\n' "$LABEL" "$DETAIL" >> "$MEDIUM_FILE"
80
+ for ALT in "$@"; do
81
+ printf 'ALT:%s\n' "$ALT" >> "$MEDIUM_FILE"
82
+ done
83
+ printf 'END_VIOLATION\n' >> "$MEDIUM_FILE"
84
+ }
85
+
86
+ # ── 7. Per-segment evaluation helper ──────────────────────────────────────────
87
+ # Split on &&, ||, ;, and newlines and test a pattern against each segment.
88
+ # Returns 0 if ANY segment matches the pattern.
89
+ any_segment_matches() {
90
+ local PATTERN="$1"
91
+ while IFS= read -r SEG; do
92
+ if printf '%s' "$SEG" | grep -qiE "$PATTERN"; then
93
+ return 0
94
+ fi
95
+ done < <(printf '%s' "$CMD" | sed 's/&&/\n/g; s/||/\n/g; s/;/\n/g')
96
+ return 1
97
+ }
98
+
99
+ # ── 8. Smart exclusion flags ──────────────────────────────────────────────────
100
+ CMD_IS_REBASE_SAFE=0
101
+ if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+(rebase)[[:space:]].*(--abort|--continue)'; then
102
+ CMD_IS_REBASE_SAFE=1
103
+ fi
104
+
105
+ CMD_IS_CLEAN_DRY=0
106
+ if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+clean.*([ \t]-n|--dry-run)'; then
107
+ CMD_IS_CLEAN_DRY=1
108
+ fi
109
+
110
+ # ── 9. HIGH severity checks ────────────────────────────────────────────────────
111
+
112
+ # H1: git push --force or -f (per-segment — prevents --force-with-lease poisoning)
113
+ # A segment containing --force-with-lease is excluded; other segments are not.
114
+ while IFS= read -r SEGMENT; do
115
+ SEGMENT=$(printf '%s' "$SEGMENT" | sed 's/^[[:space:]]*//')
116
+ [[ -z "$SEGMENT" ]] && continue
117
+ # Skip segments that use the safe --force-with-lease
118
+ if printf '%s' "$SEGMENT" | grep -qiE 'git[[:space:]]+push.*--force-with-lease'; then
119
+ continue
120
+ fi
121
+ if printf '%s' "$SEGMENT" | grep -qiE 'git[[:space:]]+push.*(--force|-f[[:space:]])' || \
122
+ printf '%s' "$SEGMENT" | grep -qiE 'git[[:space:]]+push.*(--force|-f)$'; then
123
+ add_high \
124
+ "git push --force — force push detected" \
125
+ "Force-pushing rewrites public history and breaks collaborators' local copies." \
126
+ "Alt: Use 'git push --force-with-lease' — blocks if upstream has new commits you haven't pulled."
127
+ break
128
+ fi
129
+ done < <(printf '%s' "$CMD" | sed 's/&&/\n/g; s/||/\n/g; s/;/\n/g')
130
+
131
+ # H2: git rebase — advisory (MEDIUM)
132
+ if [[ $CMD_IS_REBASE_SAFE -eq 0 ]]; then
133
+ if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+rebase([[:space:]]|$)'; then
134
+ add_medium \
135
+ "git rebase — rewrites commit history (advisory)" \
136
+ "Rebase changes commit SHAs. Safe on local feature branches; dangerous on shared/published branches." \
137
+ "Alt: 'git merge origin/main' preserves history (creates merge commit)." \
138
+ " 'git rebase --abort' to cancel if in progress."
139
+ fi
140
+ fi
141
+
142
+ # H3: git checkout -- .
143
+ if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+checkout[[:space:]]+--[[:space:]]+\.'; then
144
+ add_high \
145
+ "git checkout -- . — discards all uncommitted changes" \
146
+ "Overwrites working tree changes with HEAD. Uncommitted work is lost permanently." \
147
+ "Alt: 'git stash' to temporarily shelve changes, 'git restore <file>' for individual files."
148
+ fi
149
+
150
+ # H4: git restore . (any form — with or without --staged flag)
151
+ if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+restore[[:space:]].*[[:space:]]\.([[:space:]]|$)' || \
152
+ printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+restore[[:space:]]+\.[[:space:]]*$'; then
153
+ add_high \
154
+ "git restore . — discards all uncommitted changes" \
155
+ "Restores every tracked file to HEAD, permanently discarding all working tree modifications." \
156
+ "Alt: 'git stash' to save changes temporarily, or restore individual files: 'git restore <file>'."
157
+ fi
158
+
159
+ # H5: git clean -f
160
+ if [[ $CMD_IS_CLEAN_DRY -eq 0 ]]; then
161
+ if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+clean[[:space:]]+-[a-zA-Z]*f'; then
162
+ add_high \
163
+ "git clean -f — removes untracked files" \
164
+ "Permanently deletes untracked files from the working tree. Cannot be undone via git." \
165
+ "Alt: 'git clean -n' (dry-run) to preview what would be deleted before committing."
166
+ fi
167
+ fi
168
+
169
+ # H6: DROP TABLE or DROP DATABASE in psql
170
+ if printf '%s' "$CMD" | grep -qiE '(psql|pgcli)[^|&;]*DROP[[:space:]]+(TABLE|DATABASE|SCHEMA)'; then
171
+ add_high \
172
+ "DROP TABLE/DATABASE via psql — destructive DDL" \
173
+ "Running destructive DDL directly in psql bypasses migration pipeline safety checks." \
174
+ "Alt: Use your project's migration tool. Never run DROP via ad-hoc psql."
175
+ fi
176
+
177
+ # H7: kill -9 with pgrep subshell
178
+ if printf '%s' "$CMD" | grep -qiE 'kill[[:space:]]+-9[[:space:]]+(\$\(|`)'; then
179
+ add_high \
180
+ "kill -9 with pgrep subshell — aggressive process termination" \
181
+ "Sends SIGKILL to processes matched by name, which may kill unintended processes." \
182
+ "Alt: 'kill -15 <pid>' (SIGTERM) for graceful shutdown."
183
+ fi
184
+
185
+ # H8: killall -9
186
+ if printf '%s' "$CMD" | grep -qiE 'killall[[:space:]]+-9[[:space:]]+\S'; then
187
+ add_high \
188
+ "killall -9 — SIGKILL all matching processes" \
189
+ "Immediately terminates all processes with the given name without cleanup." \
190
+ "Alt: 'killall -15 <name>' (SIGTERM) allows graceful shutdown."
191
+ fi
192
+
193
+ # H9: git commit --no-verify
194
+ if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+commit.*--no-verify'; then
195
+ add_high \
196
+ "git commit --no-verify — skipping pre-commit hooks" \
197
+ "Bypasses all pre-commit safety gates including secret scanning and linting." \
198
+ "Alt: Fix the underlying hook failure rather than bypassing it."
199
+ fi
200
+
201
+ # H10: HUSKY=0 bypass — suppresses all git hooks without --no-verify
202
+ if printf '%s' "$CMD" | grep -qiE '(^|[[:space:];]|&&|\|\|)HUSKY=0[[:space:]]+git[[:space:]]+(commit|push|tag)'; then
203
+ add_high \
204
+ "HUSKY=0 — bypasses all husky git hooks" \
205
+ "Setting HUSKY=0 disables pre-commit, commit-msg, and pre-push safety gates without --no-verify." \
206
+ "Alt: Fix the underlying hook failure rather than suppressing all hooks."
207
+ fi
208
+
209
+ # H11: rm -rf with broad targets
210
+ # Covers combined flags (rm -rf, rm -fr), split flags (rm -r -f), and long flags (rm --recursive --force)
211
+ BROAD_TARGETS='(\/|~\/|\.\/\*|\*|\.|src|dist|build|node_modules)'
212
+ if printf '%s' "$CMD" | grep -qiE "rm[[:space:]]+-[a-zA-Z]*r[a-zA-Z]*f[[:space:]]+${BROAD_TARGETS}" || \
213
+ printf '%s' "$CMD" | grep -qiE "rm[[:space:]]+-[a-zA-Z]*f[a-zA-Z]*r[[:space:]]+${BROAD_TARGETS}" || \
214
+ printf '%s' "$CMD" | grep -qiE "rm[[:space:]]+-[a-zA-Z]*r[[:space:]]+-[a-zA-Z]*f[[:space:]]+${BROAD_TARGETS}" || \
215
+ printf '%s' "$CMD" | grep -qiE "rm[[:space:]]+-[a-zA-Z]*f[[:space:]]+-[a-zA-Z]*r[[:space:]]+${BROAD_TARGETS}" || \
216
+ printf '%s' "$CMD" | grep -qiE "rm[[:space:]]+--recursive[[:space:]]+--force[[:space:]]+${BROAD_TARGETS}" || \
217
+ printf '%s' "$CMD" | grep -qiE "rm[[:space:]]+--force[[:space:]]+--recursive[[:space:]]+${BROAD_TARGETS}"; then
218
+ add_high \
219
+ "rm -rf with broad target — mass file deletion" \
220
+ "Permanently deletes files and directories. Cannot be undone." \
221
+ "Alt: Move to a temp location first, or use 'rm -ri' for interactive deletion."
222
+ fi
223
+
224
+ # H12: curl/wget piped directly to shell (supply chain attack vector)
225
+ if printf '%s' "$CMD" | grep -qiE '(curl|wget)[^|]*\|[[:space:]]*(bash|sh|zsh|fish)'; then
226
+ add_high \
227
+ "curl/wget piped to shell — remote code execution" \
228
+ "Executing remote scripts without inspection is a major supply chain risk." \
229
+ "Alt: Download first, inspect the script, then execute: curl -o script.sh URL && cat script.sh && bash script.sh"
230
+ fi
231
+
232
+ # H13: git push --no-verify — bypasses pre-push hooks
233
+ if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+push.*--no-verify'; then
234
+ add_high \
235
+ "git push --no-verify — skipping pre-push hooks" \
236
+ "Bypasses all pre-push safety gates including CI checks." \
237
+ "Alt: Fix the underlying hook failure rather than bypassing it."
238
+ fi
239
+
240
+ # H14: git -c core.hooksPath= — redirects or disables hook execution
241
+ if printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+-c[[:space:]]+core\.hookspath'; then
242
+ add_high \
243
+ "git -c core.hooksPath — overriding hooks directory" \
244
+ "Redirecting the hooks path can disable all safety hooks." \
245
+ "Alt: Fix the underlying hook issue. Do not bypass the hooks directory."
246
+ fi
247
+
248
+ # H15: REA_BYPASS env var — attempted escape hatch
249
+ if printf '%s' "$CMD" | grep -qiE '(^|[[:space:];]|&&|\|\|)REA_BYPASS[[:space:]]*='; then
250
+ add_high \
251
+ "REA_BYPASS env var — unauthorized bypass attempt" \
252
+ "Setting REA_BYPASS is not a supported escape mechanism and indicates a bypass attempt." \
253
+ "Alt: If you need to override a gate, request human escalation."
254
+ fi
255
+
256
+ # H16: alias/function definitions containing bypass strings
257
+ if printf '%s' "$CMD" | grep -qiE '(alias|function)[[:space:]]+[a-zA-Z_]+.*(--(no-verify|force)|HUSKY=0|core\.hookspath)'; then
258
+ add_high \
259
+ "Alias/function definition with bypass — circumventing safety gates" \
260
+ "Defining aliases or functions that embed bypass flags defeats safety hooks." \
261
+ "Alt: Do not wrap bypass patterns in aliases or functions."
262
+ fi
263
+
264
+ # H17: context_protection — block commands that should be delegated to subagents
265
+ # Reads context_protection.delegate_to_subagent from .rea/policy.yaml.
266
+ # These commands produce excessive output that exhausts coordinator context windows.
267
+ POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
268
+ if [[ -f "$POLICY_FILE" ]]; then
269
+ DELEGATE_PATTERNS=()
270
+ IN_DELEGATE_BLOCK=0
271
+ while IFS= read -r line; do
272
+ if printf '%s' "$line" | grep -qE '^[[:space:]]*delegate_to_subagent:'; then
273
+ # Check for inline empty array
274
+ if printf '%s' "$line" | grep -qE 'delegate_to_subagent:[[:space:]]*\[\]'; then
275
+ break
276
+ fi
277
+ IN_DELEGATE_BLOCK=1
278
+ continue
279
+ fi
280
+ if [[ $IN_DELEGATE_BLOCK -eq 1 ]]; then
281
+ # Block sequence items start with " - "
282
+ if printf '%s' "$line" | grep -qE '^[[:space:]]*-[[:space:]]'; then
283
+ pattern=$(printf '%s' "$line" | sed "s/^[[:space:]]*-[[:space:]]*//; s/^[\"']//; s/[\"']$//")
284
+ if [[ -n "$pattern" ]]; then
285
+ DELEGATE_PATTERNS+=("$pattern")
286
+ fi
287
+ else
288
+ # Non-continuation line = end of block
289
+ break
290
+ fi
291
+ fi
292
+ done < "$POLICY_FILE"
293
+
294
+ for pattern in "${DELEGATE_PATTERNS[@]+"${DELEGATE_PATTERNS[@]}"}"; do
295
+ # Use fixed-string match — these are command prefixes, not regex
296
+ if printf '%s' "$CMD" | grep -qF "$pattern"; then
297
+ add_high \
298
+ "Context protection — command must run in a subagent" \
299
+ "This command produces excessive output that will exhaust the coordinator context window. Delegate it to a subagent instead of running it directly." \
300
+ "Alt: Use the Agent tool to delegate: Agent(subagent_type: 'qa-engineer-automation', prompt: 'Run $pattern and report pass/fail summary only.')" \
301
+ "Alt: The context_protection policy in .rea/policy.yaml lists commands that must be delegated."
302
+ break
303
+ fi
304
+ done
305
+ fi
306
+
307
+ # ── 10. MEDIUM severity checks ────────────────────────────────────────────────
308
+
309
+ # M1: npm install --force
310
+ if printf '%s' "$CMD" | grep -qiE 'npm[[:space:]]+(install|i)[[:space:]].*--force'; then
311
+ add_medium \
312
+ "npm install --force — bypasses dependency resolution" \
313
+ "--force skips conflict checks and can install incompatible package versions." \
314
+ "Alt: Resolve the dependency conflict explicitly. Use --legacy-peer-deps if needed."
315
+ fi
316
+
317
+ # ── 11. Evaluate and report ───────────────────────────────────────────────────
318
+
319
+ TRUNCATED_CMD=$(truncate_cmd "$CMD")
320
+
321
+ print_violations() {
322
+ local VF="$1"
323
+ local NOTE_LABEL="$2"
324
+ while IFS= read -r LINE; do
325
+ case "$LINE" in
326
+ HIGH\|*|MEDIUM\|*)
327
+ local SEV LABEL DETAIL
328
+ SEV=$(printf '%s' "$LINE" | cut -d'|' -f1)
329
+ LABEL=$(printf '%s' "$LINE" | cut -d'|' -f2)
330
+ DETAIL=$(printf '%s' "$LINE" | cut -d'|' -f3)
331
+ printf ' %s: %s\n' "$SEV" "$LABEL"
332
+ printf ' %s: %s\n' "$NOTE_LABEL" "$DETAIL"
333
+ ;;
334
+ ALT:*)
335
+ printf ' %s\n' "${LINE#ALT:}"
336
+ ;;
337
+ END_VIOLATION)
338
+ printf '\n'
339
+ ;;
340
+ esac
341
+ done < "$VF"
342
+ }
343
+
344
+ if [[ -s "$HIGH_FILE" ]]; then
345
+ {
346
+ printf 'BASH INTERCEPTED: Dangerous command blocked\n'
347
+ print_violations "$HIGH_FILE" "Reason"
348
+ printf ' BLOCKED COMMAND: %s\n' "$TRUNCATED_CMD"
349
+ } >&2
350
+ exit 2
351
+ fi
352
+
353
+ if [[ -s "$MEDIUM_FILE" ]]; then
354
+ {
355
+ printf 'BASH ADVISORY: Potentially risky command (not blocked)\n'
356
+ print_violations "$MEDIUM_FILE" "Note"
357
+ printf ' COMMAND: %s\n' "$TRUNCATED_CMD"
358
+ } >&2
359
+ exit 0
360
+ fi
361
+
362
+ exit 0
@@ -0,0 +1,118 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: dependency-audit-gate.sh
3
+ # Fires BEFORE every Bash tool call.
4
+ # Detects package install commands (npm install, pnpm add, yarn add) and
5
+ # verifies the package exists on the registry before allowing the install.
6
+ #
7
+ # Exit codes:
8
+ # 0 = allow (not an install command, or package verified)
9
+ # 2 = block (package not found on registry)
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
+ # ── 5. Detect package install commands ────────────────────────────────────────
40
+ # Match: npm install <pkg>, npm i <pkg>, pnpm add <pkg>, yarn add <pkg>
41
+ # Skip: npm install (no args), npm ci, npm install --save-dev (without new pkg)
42
+
43
+ extract_packages() {
44
+ local cmd="$1"
45
+
46
+ # npm install/add with packages (skip flags and local paths)
47
+ if printf '%s' "$cmd" | grep -qiE '(npm[[:space:]]+(install|i|add)|pnpm[[:space:]]+(add|install)|yarn[[:space:]]+add)[[:space:]]'; then
48
+ # Extract the part after the install command
49
+ local after_cmd
50
+ after_cmd=$(printf '%s' "$cmd" | sed -E 's/.*(npm[[:space:]]+(install|i|add)|pnpm[[:space:]]+(add|install)|yarn[[:space:]]+add)[[:space:]]+//')
51
+
52
+ # Split on spaces and filter
53
+ for token in $after_cmd; do
54
+ # Skip flags
55
+ if [[ "$token" == -* ]]; then continue; fi
56
+ # Skip local paths
57
+ if [[ "$token" == ./* || "$token" == /* || "$token" == ../* ]]; then continue; fi
58
+ # Skip empty
59
+ if [[ -z "$token" ]]; then continue; fi
60
+ # Strip version specifier for lookup
61
+ local pkg_name
62
+ pkg_name=$(printf '%s' "$token" | sed -E 's/@[^@/]+$//')
63
+ # Handle scoped packages (@scope/name)
64
+ if [[ -z "$pkg_name" ]]; then
65
+ pkg_name="$token"
66
+ fi
67
+ printf '%s\n' "$pkg_name"
68
+ done
69
+ fi
70
+ }
71
+
72
+ PACKAGES=$(extract_packages "$CMD")
73
+
74
+ if [[ -z "$PACKAGES" ]]; then
75
+ exit 0
76
+ fi
77
+
78
+ # ── 6. Verify packages exist on registry ──────────────────────────────────────
79
+ FAILED=""
80
+ CHECKED=0
81
+
82
+ while IFS= read -r pkg; do
83
+ [[ -z "$pkg" ]] && continue
84
+ CHECKED=$((CHECKED + 1))
85
+
86
+ # Cap at 5 packages per command to avoid slow hook
87
+ if [[ $CHECKED -gt 5 ]]; then
88
+ break
89
+ fi
90
+
91
+ # Use npm view to check if package exists
92
+ # macOS doesn't have `timeout` by default, use a background process with kill
93
+ if command -v timeout >/dev/null 2>&1; then
94
+ if ! timeout 5 npm view "$pkg" name >/dev/null 2>&1; then
95
+ FAILED="${FAILED} - ${pkg}\n"
96
+ fi
97
+ else
98
+ # Fallback: run npm view without timeout (still fast for simple checks)
99
+ if ! npm view "$pkg" name >/dev/null 2>&1; then
100
+ FAILED="${FAILED} - ${pkg}\n"
101
+ fi
102
+ fi
103
+ done <<< "$PACKAGES"
104
+
105
+ if [[ -n "$FAILED" ]]; then
106
+ {
107
+ printf 'DEPENDENCY AUDIT: Package not found on npm registry\n'
108
+ printf '\n'
109
+ printf ' The following packages could not be verified:\n'
110
+ printf '%b' "$FAILED"
111
+ printf '\n'
112
+ printf ' Rule: All packages must exist on the npm registry before installation.\n'
113
+ printf ' Check: Is the package name spelled correctly? Does it exist on npmjs.com?\n'
114
+ } >&2
115
+ exit 2
116
+ fi
117
+
118
+ exit 0
@@ -0,0 +1,110 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: env-file-protection.sh
3
+ # Fires BEFORE every Bash tool call.
4
+ # Blocks commands that read .env* / .envrc files via shell text utilities.
5
+ #
6
+ # Rationale: .env files contain credentials. Reading them via Bash exposes
7
+ # the values in command output, logs, and agent transcripts. Load credentials
8
+ # in code only (process.env, os.environ, etc.) — never via shell reads.
9
+ #
10
+ # Trigger: command matches ALL of:
11
+ # 1. Uses a text-reading utility (list below)
12
+ # 2. References a .env* or .envrc filename
13
+ #
14
+ # Exit codes:
15
+ # 0 = allow
16
+ # 2 = block (env file read detected)
17
+
18
+ set -uo pipefail
19
+
20
+ INPUT=$(cat)
21
+
22
+ # ── Dependency check ──────────────────────────────────────────────────────────
23
+ if ! command -v jq >/dev/null 2>&1; then
24
+ printf 'REA ERROR: jq is required but not installed.\n' >&2
25
+ printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
26
+ exit 2
27
+ fi
28
+
29
+ # ── HALT check ────────────────────────────────────────────────────────────────
30
+ REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
31
+ HALT_FILE="${REA_ROOT}/.rea/HALT"
32
+ if [ -f "$HALT_FILE" ]; then
33
+ printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
34
+ "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
35
+ exit 2
36
+ fi
37
+
38
+ CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
39
+
40
+ if [[ -z "$CMD" ]]; then
41
+ exit 0
42
+ fi
43
+
44
+ truncate_cmd() {
45
+ local STR="$1"
46
+ local MAX=100
47
+ if [[ ${#STR} -gt $MAX ]]; then
48
+ printf '%s' "${STR:0:$MAX}..."
49
+ else
50
+ printf '%s' "$STR"
51
+ fi
52
+ }
53
+
54
+ # Text-reading utilities (shell and common alternatives)
55
+ # Defense-in-depth: this list catches the most common shell-based exfiltration
56
+ # vectors. It is NOT exhaustive. Known gaps include:
57
+ # - Docker volume mounts (docker run -v .env:/...) — separate concern
58
+ # - Editor commands (vim, nano, code) — not typically used by agents
59
+ # - Redirects/process substitution (< .env) without a listed utility
60
+ # - Network tools (curl file://, nc) — low-risk in agent context
61
+ # The goal is to block casual and accidental reads, not defeat a determined
62
+ # adversary with shell access.
63
+ PATTERN_UTILITY='(cat|head|tail|less|more|grep|sed|awk|bat|strings|printf|xargs|tee|jq|python3?[[:space:]]+-c|ruby[[:space:]]+-e)[[:space:]]'
64
+ # Also catch: source/., cp (reads then writes elsewhere)
65
+ PATTERN_SOURCE='(source|\.)[[:space:]]+[^;|&]*\.env'
66
+ PATTERN_CP_ENV='cp[[:space:]]+[^;|&]*\.env'
67
+ # .env* files or .envrc (direnv)
68
+ PATTERN_ENV_FILE='(\.env[a-zA-Z0-9._-]*|\.envrc)([[:space:]]|"|'"'"'|$)'
69
+
70
+ MATCHES_UTILITY=0
71
+ MATCHES_ENV_FILE=0
72
+
73
+ if printf '%s' "$CMD" | grep -qE "$PATTERN_UTILITY"; then
74
+ MATCHES_UTILITY=1
75
+ fi
76
+
77
+ if printf '%s' "$CMD" | grep -qE "$PATTERN_ENV_FILE"; then
78
+ MATCHES_ENV_FILE=1
79
+ fi
80
+
81
+ # Direct source/cp of .env files — always block
82
+ if printf '%s' "$CMD" | grep -qE "$PATTERN_SOURCE" || \
83
+ printf '%s' "$CMD" | grep -qE "$PATTERN_CP_ENV"; then
84
+ TRUNCATED_CMD=$(truncate_cmd "$CMD")
85
+ {
86
+ printf 'ENV FILE PROTECTION: Direct sourcing or copying of .env files is blocked.\n'
87
+ printf '\n'
88
+ printf ' Command: %s\n' "$TRUNCATED_CMD"
89
+ printf '\n'
90
+ printf ' Rule: Load credentials in code only — never via shell source or cp.\n'
91
+ printf ' Use: process.env.VAR_NAME, os.environ["VAR_NAME"], etc.\n'
92
+ } >&2
93
+ exit 2
94
+ fi
95
+
96
+ if [[ $MATCHES_UTILITY -eq 1 && $MATCHES_ENV_FILE -eq 1 ]]; then
97
+ TRUNCATED_CMD=$(truncate_cmd "$CMD")
98
+ {
99
+ printf 'ENV FILE PROTECTION: Reading .env files via Bash is blocked.\n'
100
+ printf '\n'
101
+ printf ' Command: %s\n' "$TRUNCATED_CMD"
102
+ printf '\n'
103
+ printf ' Rule: Load credentials in code only, never via shell.\n'
104
+ printf ' Use: process.env.VAR_NAME, os.environ["VAR_NAME"], etc.\n'
105
+ printf ' .env files must not be read via shell utilities in agent sessions.\n'
106
+ } >&2
107
+ exit 2
108
+ fi
109
+
110
+ exit 0
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bash
2
+ # pr-issue-link-gate.sh — PreToolUse: Bash
3
+ #
4
+ # Ensures every `gh pr create` command references at least one GitHub issue
5
+ # via closes/fixes/resolves #N syntax in the PR body. When the magic keyword
6
+ # is present, GitHub automatically closes the linked issue when the PR merges
7
+ # to the default branch and creates a cross-reference in the issue timeline.
8
+ #
9
+ # This gate is ADVISORY (exit 0) — it warns but does not block. Some PRs
10
+ # legitimately have no linked issue (chores, hotfixes, release PRs). The
11
+ # advisory gives the agent an opportunity to add the link before proceeding.
12
+ #
13
+ # Only active for Bash tool calls containing `gh pr create`.
14
+ # JSONL-only projects (no GitHub) are unaffected — gh is unavailable there.
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
+ INPUT="$(cat)"
26
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
27
+
28
+ if [[ "$TOOL_NAME" != "Bash" ]]; then
29
+ exit 0
30
+ fi
31
+
32
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
33
+
34
+ # Only intercept gh pr create
35
+ if ! echo "$COMMAND" | grep -qE 'gh\s+pr\s+create'; then
36
+ exit 0
37
+ fi
38
+
39
+ require_jq
40
+
41
+ # Check for closing keywords followed by an issue number
42
+ # Accepted: closes #N, fixes #N, resolves #N (case-insensitive, any spacing)
43
+ if echo "$COMMAND" | grep -qiE '(closes|fixes|resolves)\s+#[0-9]+'; then
44
+ exit 0
45
+ fi
46
+
47
+ # Advisory — warn but do not block.
48
+ # Chore PRs, release PRs, and hotfixes may legitimately have no linked issue.
49
+ printf 'PR ISSUE LINK ADVISORY: This PR does not reference a GitHub issue.\n' >&2
50
+ printf '\n' >&2
51
+ printf 'When a PR body includes a closing reference, GitHub automatically:\n' >&2
52
+ printf ' - Closes the issue when the PR merges to the default branch\n' >&2
53
+ printf ' - Creates a cross-reference in the issue timeline\n' >&2
54
+ printf ' - Links the PR in the CHANGELOG context\n' >&2
55
+ printf '\n' >&2
56
+ printf 'Add to the --body:\n' >&2
57
+ printf ' closes #N closes one issue\n' >&2
58
+ printf ' fixes #N same effect\n' >&2
59
+ printf ' resolves #N same effect\n' >&2
60
+ printf ' closes #N, closes #M closes multiple issues\n' >&2
61
+ printf '\n' >&2
62
+ printf 'If this is a chore, release, or hotfix PR with no upstream issue, you may proceed.\n' >&2
63
+
64
+ # Exit 0 — advisory only, does not block the PR creation
65
+ exit 0