@bookedsolid/reagent 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 +118 -0
- package/agents/reagent-orchestrator.md +66 -0
- package/bin/init.js +818 -0
- package/commands/rea.md +76 -0
- package/commands/restart.md +105 -0
- package/cursor/rules/001-no-hallucination.mdc +28 -0
- package/cursor/rules/002-verify-before-act.mdc +28 -0
- package/cursor/rules/003-attribution.mdc +36 -0
- package/hooks/attribution-advisory.sh +74 -0
- package/hooks/dangerous-bash-interceptor.sh +287 -0
- package/hooks/env-file-protection.sh +110 -0
- package/hooks/secret-scanner.sh +229 -0
- package/husky/commit-msg.sh +50 -0
- package/husky/pre-commit.sh +57 -0
- package/husky/pre-push.sh +75 -0
- package/package.json +60 -0
- package/profiles/bst-internal.json +30 -0
- package/profiles/client-engagement.json +30 -0
- package/templates/CLAUDE.md +55 -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 'REAGENT 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
|
+
REAGENT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
31
|
+
HALT_FILE="${REAGENT_ROOT}/.reagent/HALT"
|
|
32
|
+
if [ -f "$HALT_FILE" ]; then
|
|
33
|
+
printf 'REAGENT HALT: %s\nAll agent operations suspended. Run: reagent unfreeze\n' \
|
|
34
|
+
"$(cat "$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,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 'REAGENT 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
|
+
REAGENT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
33
|
+
HALT_FILE="${REAGENT_ROOT}/.reagent/HALT"
|
|
34
|
+
if [ -f "$HALT_FILE" ]; then
|
|
35
|
+
printf 'REAGENT HALT: %s\nAll agent operations suspended. Run: reagent unfreeze\n' \
|
|
36
|
+
"$(cat "$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}/reagent-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}/reagent-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,50 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# .husky/commit-msg — strips AI attribution from commit messages
|
|
3
|
+
#
|
|
4
|
+
# Removes lines added by AI coding assistants (Claude Code, etc.) that would
|
|
5
|
+
# expose AI tooling in client-facing or public git history.
|
|
6
|
+
#
|
|
7
|
+
# Stripped patterns:
|
|
8
|
+
# Co-Authored-By: Claude ...
|
|
9
|
+
# Co-Authored-By: Sonnet ...
|
|
10
|
+
# Generated with Claude Code
|
|
11
|
+
# 🤖 Generated with ...
|
|
12
|
+
# claude.ai
|
|
13
|
+
#
|
|
14
|
+
# SAFETY: set -e ensures any unexpected error BLOCKS the commit rather than
|
|
15
|
+
# silently passing a message with attribution intact.
|
|
16
|
+
|
|
17
|
+
set -e
|
|
18
|
+
|
|
19
|
+
COMMIT_MSG_FILE="$1"
|
|
20
|
+
|
|
21
|
+
# Validate input
|
|
22
|
+
if [ -z "$COMMIT_MSG_FILE" ]; then
|
|
23
|
+
echo "ERROR: commit-msg hook received no file path" >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
if [ ! -f "$COMMIT_MSG_FILE" ]; then
|
|
27
|
+
echo "ERROR: commit message file not found: $COMMIT_MSG_FILE" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Atomic write: grep to temp, mv to original (no partial-write risk)
|
|
32
|
+
TMPFILE=$(mktemp) || { echo "ERROR: mktemp failed" >&2; exit 1; }
|
|
33
|
+
cleanup() { rm -f "$TMPFILE"; }
|
|
34
|
+
trap cleanup EXIT
|
|
35
|
+
|
|
36
|
+
grep -v \
|
|
37
|
+
-e "Co-Authored-By: Claude" \
|
|
38
|
+
-e "Co-Authored-By: Sonnet" \
|
|
39
|
+
-e "Generated with Claude Code" \
|
|
40
|
+
-e "🤖 Generated with" \
|
|
41
|
+
-e "claude.ai" \
|
|
42
|
+
"$COMMIT_MSG_FILE" > "$TMPFILE" || true
|
|
43
|
+
# grep -v exits 1 when no lines survive (empty file after stripping) — that's OK
|
|
44
|
+
|
|
45
|
+
mv "$TMPFILE" "$COMMIT_MSG_FILE" || { echo "ERROR: could not write cleaned message" >&2; exit 1; }
|
|
46
|
+
|
|
47
|
+
# Normalize trailing newlines (cosmetic, non-fatal)
|
|
48
|
+
perl -i -0777 -pe 's/\n+$/\n/' "$COMMIT_MSG_FILE" 2>/dev/null || true
|
|
49
|
+
|
|
50
|
+
exit 0
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# .husky/pre-commit — secret scan on staged files before commit
|
|
3
|
+
#
|
|
4
|
+
# Gates (in order):
|
|
5
|
+
# 1. gitleaks protect --staged (REQUIRED — hard fail if not installed)
|
|
6
|
+
# 2. .env and .envrc staged file check (always — no gitleaks dependency)
|
|
7
|
+
#
|
|
8
|
+
# Exit codes:
|
|
9
|
+
# 0 = all gates passed
|
|
10
|
+
# 1 = gate failed — commit blocked
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
# ── Gate 1: gitleaks staged scan ──────────────────────────────────────────────
|
|
15
|
+
if command -v gitleaks >/dev/null 2>&1; then
|
|
16
|
+
echo "pre-commit: running gitleaks on staged files..."
|
|
17
|
+
if ! gitleaks protect --staged --no-banner 2>&1; then
|
|
18
|
+
echo ""
|
|
19
|
+
echo "ERROR: gitleaks detected potential secrets in staged files."
|
|
20
|
+
echo "Review the output above, remove credentials, then re-stage."
|
|
21
|
+
echo ""
|
|
22
|
+
echo "To allowlist a false positive, add it to .gitleaks.toml [allowlist]"
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
echo "pre-commit: gitleaks scan passed"
|
|
26
|
+
else
|
|
27
|
+
echo ""
|
|
28
|
+
echo "ERROR: gitleaks is required but not installed."
|
|
29
|
+
echo "Install: https://github.com/gitleaks/gitleaks#installing"
|
|
30
|
+
echo " macOS: brew install gitleaks"
|
|
31
|
+
echo " Linux: download from GitHub releases"
|
|
32
|
+
echo ""
|
|
33
|
+
echo "Secret scanning is mandatory. Install gitleaks and retry."
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# ── Gate 2: .env / .envrc staged file check ───────────────────────────────────
|
|
38
|
+
# Check for .env* or .envrc files anywhere in the repo (including subdirectories)
|
|
39
|
+
STAGED_ENV=$(git diff --cached --name-only 2>/dev/null \
|
|
40
|
+
| grep -E '(^|/)\.env(rc|$|\.)' \
|
|
41
|
+
| grep -v '\.example$' \
|
|
42
|
+
| grep -v '\.sample$' \
|
|
43
|
+
|| true)
|
|
44
|
+
|
|
45
|
+
if [ -n "$STAGED_ENV" ]; then
|
|
46
|
+
echo ""
|
|
47
|
+
echo "ERROR: Staged files include credential files:"
|
|
48
|
+
echo "$STAGED_ENV" | while IFS= read -r f; do echo " $f"; done
|
|
49
|
+
echo ""
|
|
50
|
+
echo "These files must not be committed. Add them to .gitignore:"
|
|
51
|
+
echo " echo '.env' >> .gitignore"
|
|
52
|
+
echo " echo '.envrc' >> .gitignore"
|
|
53
|
+
echo ""
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
exit 0
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# .husky/pre-push — full quality gate before push
|
|
3
|
+
#
|
|
4
|
+
# Enforces zero-bad-code policy: NOTHING reaches the remote without passing
|
|
5
|
+
# all available quality gates. Every gate that exists in the project runs.
|
|
6
|
+
#
|
|
7
|
+
# Gates (skipped gracefully if script not present in package.json):
|
|
8
|
+
# 1. Format check (prettier --check .)
|
|
9
|
+
# 2. Lint (eslint .)
|
|
10
|
+
# 3. Type check (tsc --noEmit)
|
|
11
|
+
# 4. Tests (vitest run / jest / node --test)
|
|
12
|
+
# 5. Build (build script)
|
|
13
|
+
#
|
|
14
|
+
# Exit codes:
|
|
15
|
+
# 0 = all applicable gates passed
|
|
16
|
+
# 1 = a gate failed — push blocked
|
|
17
|
+
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
if [ ! -f "package.json" ]; then
|
|
21
|
+
echo "pre-push: no package.json found — quality gates skipped (non-npm project)"
|
|
22
|
+
echo "pre-push: consider adding project-specific gates to .husky/pre-push"
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Detect package manager
|
|
27
|
+
PKG_MANAGER="npm"
|
|
28
|
+
if [ -f "pnpm-lock.yaml" ]; then
|
|
29
|
+
PKG_MANAGER="pnpm"
|
|
30
|
+
elif [ -f "yarn.lock" ]; then
|
|
31
|
+
PKG_MANAGER="yarn"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
FAILED=""
|
|
35
|
+
OUT=$(mktemp)
|
|
36
|
+
cleanup() { rm -f "$OUT"; }
|
|
37
|
+
trap cleanup EXIT
|
|
38
|
+
|
|
39
|
+
script_exists() {
|
|
40
|
+
local SCRIPT_NAME="$1"
|
|
41
|
+
node -e "const p=require('./package.json');if(!p.scripts||!p.scripts[process.argv[1]])process.exit(1);" "$SCRIPT_NAME" 2>/dev/null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
run_gate() {
|
|
45
|
+
local SCRIPT_NAME="$1"
|
|
46
|
+
local LABEL="$2"
|
|
47
|
+
|
|
48
|
+
if script_exists "$SCRIPT_NAME"; then
|
|
49
|
+
echo "pre-push: running ${LABEL}..."
|
|
50
|
+
if ! $PKG_MANAGER run "$SCRIPT_NAME" > "$OUT" 2>&1; then
|
|
51
|
+
echo ""
|
|
52
|
+
echo "GATE FAILED: ${LABEL}"
|
|
53
|
+
cat "$OUT"
|
|
54
|
+
echo ""
|
|
55
|
+
FAILED="${FAILED} ${SCRIPT_NAME}"
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# ── Gates ─────────────────────────────────────────────────────────────────────
|
|
61
|
+
run_gate "format:check" "Prettier format check"
|
|
62
|
+
run_gate "lint" "ESLint"
|
|
63
|
+
run_gate "type-check" "TypeScript type check"
|
|
64
|
+
run_gate "test" "Test suite"
|
|
65
|
+
run_gate "build" "Build"
|
|
66
|
+
|
|
67
|
+
# ── Report ────────────────────────────────────────────────────────────────────
|
|
68
|
+
if [ -n "$FAILED" ]; then
|
|
69
|
+
echo "pre-push: FAILED gates:${FAILED}"
|
|
70
|
+
echo "All quality gates must pass before push. Fix failures and retry."
|
|
71
|
+
exit 1
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
echo "pre-push: all quality gates passed"
|
|
75
|
+
exit 0
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bookedsolid/reagent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-trust agentic infrastructure — hooks, cursor rules, attribution removal, and behavioral enforcement for any AI-assisted project",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Booked Solid Technology <oss@bookedsolid.tech> (https://bookedsolid.tech)",
|
|
7
|
+
"homepage": "https://github.com/bookedsolidtech/reagent#readme",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/bookedsolidtech/reagent/issues"
|
|
10
|
+
},
|
|
11
|
+
"type": "commonjs",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/bookedsolidtech/reagent.git"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"reagent": "bin/init.js"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=22"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"bin/",
|
|
24
|
+
"hooks/",
|
|
25
|
+
"cursor/",
|
|
26
|
+
"husky/",
|
|
27
|
+
"profiles/",
|
|
28
|
+
"templates/",
|
|
29
|
+
"agents/",
|
|
30
|
+
"commands/"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"prepare": "husky",
|
|
34
|
+
"prepublishOnly": "node scripts/block-local-publish.mjs",
|
|
35
|
+
"preflight": "./scripts/preflight.sh",
|
|
36
|
+
"changeset": "changeset",
|
|
37
|
+
"changeset:version": "changeset version",
|
|
38
|
+
"changeset:publish": "changeset publish",
|
|
39
|
+
"lint": "eslint .",
|
|
40
|
+
"format": "prettier --write .",
|
|
41
|
+
"format:check": "prettier --check .",
|
|
42
|
+
"test": "echo 'No tests yet — add tests before 0.2.0' && exit 0",
|
|
43
|
+
"type-check": "echo 'No TypeScript config — skipping' && exit 0"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"claude",
|
|
47
|
+
"ai-safety",
|
|
48
|
+
"hooks",
|
|
49
|
+
"cursor",
|
|
50
|
+
"attribution",
|
|
51
|
+
"zero-trust",
|
|
52
|
+
"agentic"
|
|
53
|
+
],
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@changesets/cli": "^2.30.0",
|
|
56
|
+
"eslint": "^10.2.0",
|
|
57
|
+
"husky": "^9.1.7",
|
|
58
|
+
"prettier": "^3.8.1"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bst-internal",
|
|
3
|
+
"description": "BST internal project setup — full safety hooks, CI enforcement, attribution stripper",
|
|
4
|
+
"huskyCommitMsg": true,
|
|
5
|
+
"huskyPreCommit": true,
|
|
6
|
+
"huskyPrePush": true,
|
|
7
|
+
"cursorRules": ["001-no-hallucination", "002-verify-before-act", "003-attribution"],
|
|
8
|
+
"gitignoreEntries": [".claude/agents/", ".claude/hooks/", ".claude/settings.json", "RESTART.md"],
|
|
9
|
+
"claudeMd": {
|
|
10
|
+
"preflightCmd": "pnpm preflight",
|
|
11
|
+
"attributionRule": "Attribution in internal BST projects is permitted in `.claude/` files and approved team documentation. Strip attribution from any client-facing commits, PR bodies, and public-facing content."
|
|
12
|
+
},
|
|
13
|
+
"claudeHooks": {
|
|
14
|
+
"PreToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Bash",
|
|
17
|
+
"hooks": ["dangerous-bash-interceptor", "env-file-protection"]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"matcher": "Write|Edit",
|
|
21
|
+
"hooks": ["secret-scanner"]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"matcher": "Bash",
|
|
25
|
+
"hooks": ["attribution-advisory"]
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"PostToolUse": []
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "client-engagement",
|
|
3
|
+
"description": "Zero-trust setup for client project engagements — no AI attribution, hallucination guards, safety hooks",
|
|
4
|
+
"huskyCommitMsg": true,
|
|
5
|
+
"huskyPreCommit": true,
|
|
6
|
+
"huskyPrePush": true,
|
|
7
|
+
"cursorRules": ["001-no-hallucination", "002-verify-before-act", "003-attribution"],
|
|
8
|
+
"gitignoreEntries": [".claude/agents/", ".claude/hooks/", ".claude/settings.json", "RESTART.md"],
|
|
9
|
+
"claudeMd": {
|
|
10
|
+
"preflightCmd": "pnpm preflight",
|
|
11
|
+
"attributionRule": "Do NOT include AI attribution in commits, PR bodies, code comments, or any client-facing content. The commit-msg hook strips attribution from git commits automatically. PR bodies and code comments must be cleaned manually before submission."
|
|
12
|
+
},
|
|
13
|
+
"claudeHooks": {
|
|
14
|
+
"PreToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Bash",
|
|
17
|
+
"hooks": ["dangerous-bash-interceptor", "env-file-protection"]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"matcher": "Write|Edit",
|
|
21
|
+
"hooks": ["secret-scanner"]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"matcher": "Bash",
|
|
25
|
+
"hooks": ["attribution-advisory"]
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"PostToolUse": []
|
|
29
|
+
}
|
|
30
|
+
}
|