@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.
@@ -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
+ }