@bookedsolid/reagent 0.5.0 → 0.6.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,84 @@
1
+ #!/bin/bash
2
+ # PostToolUse hook: ci-config-protection.sh
3
+ # Fires AFTER every Write tool call.
4
+ # Scans CI workflow files for dangerous permission or trust-boundary patterns.
5
+ # Advisory only (exit 0) — warns loudly but does not block.
6
+ #
7
+ # Triggers only when file_path matches .github/workflows/
8
+ #
9
+ # Patterns checked:
10
+ # - permissions: write-all
11
+ # - pull_request_target (trust escalation trigger)
12
+ # - secrets: inherit
13
+ #
14
+ # Exit codes:
15
+ # 0 = OK (including advisory warnings — not blocking)
16
+
17
+ set -uo pipefail
18
+
19
+ INPUT=$(cat)
20
+
21
+ # ── Dependency check ──────────────────────────────────────────────────────────
22
+ if ! command -v jq >/dev/null 2>&1; then
23
+ printf 'REAGENT 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
+ # ── HALT check ────────────────────────────────────────────────────────────────
29
+ REAGENT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
30
+ HALT_FILE="${REAGENT_ROOT}/.reagent/HALT"
31
+ if [ -f "$HALT_FILE" ]; then
32
+ printf 'REAGENT HALT: %s\nAll agent operations suspended. Run: reagent unfreeze\n' \
33
+ "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
34
+ exit 2
35
+ fi
36
+
37
+ FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
38
+
39
+ # ── Only trigger for .github/workflows/ paths ─────────────────────────────────
40
+ if [[ -z "$FILE_PATH" ]]; then
41
+ exit 0
42
+ fi
43
+
44
+ if [[ "$FILE_PATH" != *".github/workflows/"* ]]; then
45
+ exit 0
46
+ fi
47
+
48
+ # ── Extract written content ───────────────────────────────────────────────────
49
+ CONTENT=$(printf '%s' "$INPUT" | jq -r '.tool_input.content // empty' 2>/dev/null)
50
+
51
+ if [[ -z "$CONTENT" ]]; then
52
+ exit 0
53
+ fi
54
+
55
+ # ── Scan for dangerous CI patterns ───────────────────────────────────────────
56
+ WARNINGS=()
57
+
58
+ if printf '%s' "$CONTENT" | grep -qE 'permissions:[[:space:]]*write-all'; then
59
+ WARNINGS+=("permissions: write-all — grants all GitHub Actions permissions (read+write). Scope to only required permissions.")
60
+ fi
61
+
62
+ if printf '%s' "$CONTENT" | grep -qE 'pull_request_target'; then
63
+ WARNINGS+=("pull_request_target — runs with write permissions from the base repo in context of a PR. Dangerous if combined with checkout of PR head code.")
64
+ fi
65
+
66
+ if printf '%s' "$CONTENT" | grep -qE 'secrets:[[:space:]]*inherit'; then
67
+ WARNINGS+=("secrets: inherit — passes all secrets to called workflow. Only use with trusted reusable workflows in the same org.")
68
+ fi
69
+
70
+ if [[ ${#WARNINGS[@]} -eq 0 ]]; then
71
+ exit 0
72
+ fi
73
+
74
+ # ── Print advisory ────────────────────────────────────────────────────────────
75
+ {
76
+ printf 'CI-CONFIG-PROTECTION: Potentially dangerous CI pattern in %s\n' "$(basename "$FILE_PATH")"
77
+ for WARNING in "${WARNINGS[@]}"; do
78
+ printf ' ADVISORY: %s\n' "$WARNING"
79
+ done
80
+ printf 'Note: This is advisory only. Review the workflow carefully before merging.\n'
81
+ printf 'Reference: https://securitylab.github.com/research/github-actions-preventing-pwn-requests/\n'
82
+ } >&2
83
+
84
+ exit 0
@@ -0,0 +1,64 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: file-size-guard.sh
3
+ # Fires BEFORE every Write or Edit tool call.
4
+ # Blocks writes where content byte length exceeds 512KB (524288 bytes).
5
+ #
6
+ # Content extraction:
7
+ # Write tool → tool_input.content
8
+ # Edit tool → tool_input.new_string (checks the replacement only)
9
+ #
10
+ # Exit codes:
11
+ # 0 = file size within limits — allow
12
+ # 2 = file size exceeds limit — block
13
+
14
+ set -uo pipefail
15
+
16
+ INPUT=$(cat)
17
+
18
+ # ── Dependency check ──────────────────────────────────────────────────────────
19
+ if ! command -v jq >/dev/null 2>&1; then
20
+ printf 'REAGENT ERROR: jq is required but not installed.\n' >&2
21
+ printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
22
+ exit 2
23
+ fi
24
+
25
+ # ── HALT check ────────────────────────────────────────────────────────────────
26
+ REAGENT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
27
+ HALT_FILE="${REAGENT_ROOT}/.reagent/HALT"
28
+ if [ -f "$HALT_FILE" ]; then
29
+ printf 'REAGENT HALT: %s\nAll agent operations suspended. Run: reagent unfreeze\n' \
30
+ "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
31
+ exit 2
32
+ fi
33
+
34
+ TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
35
+ FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
36
+
37
+ # Extract content based on tool type
38
+ if [[ "$TOOL_NAME" == "Write" ]]; then
39
+ CONTENT=$(printf '%s' "$INPUT" | jq -r '.tool_input.content // empty' 2>/dev/null)
40
+ elif [[ "$TOOL_NAME" == "Edit" ]]; then
41
+ CONTENT=$(printf '%s' "$INPUT" | jq -r '.tool_input.new_string // empty' 2>/dev/null)
42
+ else
43
+ exit 0
44
+ fi
45
+
46
+ if [[ -z "$CONTENT" ]]; then
47
+ exit 0
48
+ fi
49
+
50
+ # ── Byte length check ─────────────────────────────────────────────────────────
51
+ LIMIT=524288 # 512KB
52
+
53
+ # Use printf + wc for portable byte counting
54
+ BYTE_LENGTH=$(printf '%s' "$CONTENT" | wc -c | tr -d ' ')
55
+
56
+ if [[ "$BYTE_LENGTH" -gt "$LIMIT" ]]; then
57
+ printf 'File size guard: %s bytes exceeds 512KB limit\n' "$BYTE_LENGTH" >&2
58
+ printf ' File: %s\n' "${FILE_PATH:-unknown}" >&2
59
+ printf 'Block reason: Writing files larger than 512KB risks memory issues and slow tool calls.\n' >&2
60
+ printf 'Fix: Split the content into multiple smaller files or chunks.\n' >&2
61
+ exit 2
62
+ fi
63
+
64
+ exit 0
@@ -0,0 +1,81 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: git-config-guard.sh
3
+ # Fires BEFORE every Bash tool call.
4
+ # Blocks git config commands that modify security-critical git settings.
5
+ #
6
+ # Blocked patterns:
7
+ # - git config core.hooksPath (redirects/disables hooks)
8
+ # - git config http.sslVerify (disables TLS verification)
9
+ # - git config safe.directory (bypasses ownership checks)
10
+ # - git config user.email/user.name (alters identity for commit signing)
11
+ #
12
+ # Exit codes:
13
+ # 0 = safe — allow the command
14
+ # 2 = dangerous git config detected — block
15
+
16
+ set -uo pipefail
17
+
18
+ INPUT=$(cat)
19
+
20
+ # ── Dependency check ──────────────────────────────────────────────────────────
21
+ if ! command -v jq >/dev/null 2>&1; then
22
+ printf 'REAGENT ERROR: jq is required but not installed.\n' >&2
23
+ printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
24
+ exit 2
25
+ fi
26
+
27
+ # ── HALT check ────────────────────────────────────────────────────────────────
28
+ REAGENT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
29
+ HALT_FILE="${REAGENT_ROOT}/.reagent/HALT"
30
+ if [ -f "$HALT_FILE" ]; then
31
+ printf 'REAGENT HALT: %s\nAll agent operations suspended. Run: reagent unfreeze\n' \
32
+ "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
33
+ exit 2
34
+ fi
35
+
36
+ CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
37
+
38
+ if [[ -z "$CMD" ]]; then
39
+ exit 0
40
+ fi
41
+
42
+ # Only proceed if git config is present
43
+ if ! printf '%s' "$CMD" | grep -qiE 'git[[:space:]]+(config|(-[a-zA-Z]+[[:space:]]+)*config)'; then
44
+ exit 0
45
+ fi
46
+
47
+ # ── Check for dangerous settings ──────────────────────────────────────────────
48
+ BLOCKED_REASON=""
49
+
50
+ # core.hooksPath — redirects or disables git hooks
51
+ if printf '%s' "$CMD" | grep -qiE 'git[[:space:]].*config.*core\.hookspath'; then
52
+ BLOCKED_REASON="core.hooksPath — redirecting the hooks directory disables all safety gates"
53
+ fi
54
+
55
+ # http.sslVerify false — disables TLS cert verification
56
+ if [[ -z "$BLOCKED_REASON" ]] && printf '%s' "$CMD" | grep -qiE 'git[[:space:]].*config.*http\.sslverify'; then
57
+ if printf '%s' "$CMD" | grep -qiE 'http\.sslverify[[:space:]]+(false|0)'; then
58
+ BLOCKED_REASON="http.sslVerify false — disabling TLS verification enables MITM attacks"
59
+ fi
60
+ fi
61
+
62
+ # safe.directory — bypasses ownership safety checks
63
+ if [[ -z "$BLOCKED_REASON" ]] && printf '%s' "$CMD" | grep -qiE 'git[[:space:]].*config.*safe\.directory'; then
64
+ BLOCKED_REASON="safe.directory — modifying this setting bypasses git ownership security checks"
65
+ fi
66
+
67
+ # user.email or user.name — altering commit identity
68
+ if [[ -z "$BLOCKED_REASON" ]] && printf '%s' "$CMD" | grep -qiE 'git[[:space:]].*config[[:space:]].*(--global|--system)[[:space:]].*user\.(email|name)'; then
69
+ BLOCKED_REASON="user.email/user.name with --global/--system flag — alters git identity globally, affecting all repos"
70
+ fi
71
+
72
+ if [[ -n "$BLOCKED_REASON" ]]; then
73
+ printf 'GIT-CONFIG-GUARD: Dangerous git config command blocked\n' >&2
74
+ printf ' Reason: %s\n' "$BLOCKED_REASON" >&2
75
+ printf ' Command: %s\n' "$(printf '%s' "$CMD" | head -c 200)" >&2
76
+ printf 'Block reason: Modifying this git setting undermines repository security.\n' >&2
77
+ printf 'If you need to change this setting, request human escalation.\n' >&2
78
+ exit 2
79
+ fi
80
+
81
+ exit 0
@@ -0,0 +1,99 @@
1
+ #!/bin/bash
2
+ # PostToolUse hook: import-guard.sh
3
+ # Fires AFTER every Write tool call.
4
+ # Scans written JS/TS/MJS content for dangerous import and eval patterns.
5
+ # Advisory only (exit 0) — warns loudly but does not block.
6
+ #
7
+ # Triggers only for .ts, .js, .mjs files.
8
+ #
9
+ # Patterns checked:
10
+ # - require('child_process') or require("child_process")
11
+ # - require('vm') or require("vm")
12
+ # - eval(
13
+ # - new Function(
14
+ # - dynamic require with variable: require(variable)
15
+ #
16
+ # Exit codes:
17
+ # 0 = OK (including advisory warnings — not blocking)
18
+
19
+ set -uo pipefail
20
+
21
+ INPUT=$(cat)
22
+
23
+ # ── Dependency check ──────────────────────────────────────────────────────────
24
+ if ! command -v jq >/dev/null 2>&1; then
25
+ printf 'REAGENT ERROR: jq is required but not installed.\n' >&2
26
+ printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
27
+ exit 2
28
+ fi
29
+
30
+ # ── HALT check ────────────────────────────────────────────────────────────────
31
+ REAGENT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
32
+ HALT_FILE="${REAGENT_ROOT}/.reagent/HALT"
33
+ if [ -f "$HALT_FILE" ]; then
34
+ printf 'REAGENT HALT: %s\nAll agent operations suspended. Run: reagent unfreeze\n' \
35
+ "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
36
+ exit 2
37
+ fi
38
+
39
+ FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
40
+
41
+ # ── Only trigger for .ts, .js, .mjs files ────────────────────────────────────
42
+ if [[ -z "$FILE_PATH" ]]; then
43
+ exit 0
44
+ fi
45
+
46
+ case "$FILE_PATH" in
47
+ *.ts|*.js|*.mjs) ;;
48
+ *) exit 0 ;;
49
+ esac
50
+
51
+ # ── Extract written content ───────────────────────────────────────────────────
52
+ CONTENT=$(printf '%s' "$INPUT" | jq -r '.tool_input.content // empty' 2>/dev/null)
53
+
54
+ if [[ -z "$CONTENT" ]]; then
55
+ exit 0
56
+ fi
57
+
58
+ # ── Scan for dangerous patterns ───────────────────────────────────────────────
59
+ WARNINGS=()
60
+
61
+ # require('child_process') or require("child_process")
62
+ if printf '%s' "$CONTENT" | grep -qE "require\(['\"]child_process['\"]"; then
63
+ WARNINGS+=("require('child_process') — grants shell execution capability. Use safer alternatives or document the necessity.")
64
+ fi
65
+
66
+ # require('vm') or require("vm")
67
+ if printf '%s' "$CONTENT" | grep -qE "require\(['\"]vm['\"]"; then
68
+ WARNINGS+=("require('vm') — Node.js VM module allows dynamic code execution. Ensure input is fully trusted and sandboxed.")
69
+ fi
70
+
71
+ # eval(
72
+ if printf '%s' "$CONTENT" | grep -qE '\beval\('; then
73
+ WARNINGS+=("eval( — dynamic code evaluation is a code injection risk. Avoid unless the input is fully controlled and sanitized.")
74
+ fi
75
+
76
+ # new Function(
77
+ if printf '%s' "$CONTENT" | grep -qE '\bnew[[:space:]]+Function\('; then
78
+ WARNINGS+=("new Function( — dynamic function construction is equivalent to eval. Avoid unless the input is fully controlled.")
79
+ fi
80
+
81
+ # Dynamic require with variable: require(variable) — not require('literal')
82
+ if printf '%s' "$CONTENT" | grep -qE "require\([^'\"][^)]*\)"; then
83
+ WARNINGS+=("Dynamic require(variable) — loading modules by variable name can be exploited for path traversal. Prefer static imports.")
84
+ fi
85
+
86
+ if [[ ${#WARNINGS[@]} -eq 0 ]]; then
87
+ exit 0
88
+ fi
89
+
90
+ # ── Print advisory ────────────────────────────────────────────────────────────
91
+ {
92
+ printf 'IMPORT-GUARD: Potentially dangerous import pattern in %s\n' "$(basename "$FILE_PATH")"
93
+ for WARNING in "${WARNINGS[@]}"; do
94
+ printf ' ADVISORY: %s\n' "$WARNING"
95
+ done
96
+ printf 'Note: This is advisory only. These patterns are not always unsafe but require review.\n'
97
+ } >&2
98
+
99
+ exit 0
@@ -0,0 +1,118 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: network-exfil-guard.sh
3
+ # Fires BEFORE every Bash tool call.
4
+ # Blocks curl/wget commands that could exfiltrate data or execute remote code.
5
+ #
6
+ # Blocked patterns:
7
+ # - curl/wget piped to sh/bash (remote code execution)
8
+ # - curl/wget -d @file (posting file contents)
9
+ # - curl/wget to hosts not in the allowlist
10
+ #
11
+ # Allowlisted hosts:
12
+ # registry.npmjs.org, github.com, api.github.com, raw.githubusercontent.com
13
+ #
14
+ # Exit codes:
15
+ # 0 = safe — allow the command
16
+ # 2 = dangerous network pattern — block
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
+ "$(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
+ # Only proceed if curl or wget is present
45
+ if ! printf '%s' "$CMD" | grep -qiE '(^|[[:space:];]|&&|\|\|)(curl|wget)[[:space:]]'; then
46
+ exit 0
47
+ fi
48
+
49
+ # ── Check 1: Piped to shell (remote code execution) ───────────────────────────
50
+ if printf '%s' "$CMD" | grep -qiE '(curl|wget)[^|]*\|[[:space:]]*(bash|sh|zsh|fish|dash)'; then
51
+ printf 'NETWORK-EXFIL-GUARD: Remote code execution pattern blocked\n' >&2
52
+ printf ' Pattern: curl/wget output piped to shell interpreter\n' >&2
53
+ printf ' Command: %s\n' "$(printf '%s' "$CMD" | head -c 200)" >&2
54
+ printf 'Block reason: Executing remote scripts without inspection is a supply chain risk.\n' >&2
55
+ printf 'Fix: Download first, inspect the script, then execute manually.\n' >&2
56
+ exit 2
57
+ fi
58
+
59
+ # ── Check 2: Posting file contents (-d @filename) ─────────────────────────────
60
+ if printf '%s' "$CMD" | grep -qiE '(curl|wget).*-d[[:space:]]+@'; then
61
+ printf 'NETWORK-EXFIL-GUARD: File content upload pattern blocked\n' >&2
62
+ printf ' Pattern: curl -d @file posts file contents to remote host\n' >&2
63
+ printf ' Command: %s\n' "$(printf '%s' "$CMD" | head -c 200)" >&2
64
+ printf 'Block reason: Uploading local file contents to a remote host may exfiltrate sensitive data.\n' >&2
65
+ exit 2
66
+ fi
67
+
68
+ # ── Check 3: Host allowlist ───────────────────────────────────────────────────
69
+ ALLOWLIST=(
70
+ "registry.npmjs.org"
71
+ "github.com"
72
+ "api.github.com"
73
+ "raw.githubusercontent.com"
74
+ "objects.githubusercontent.com"
75
+ "codeload.github.com"
76
+ )
77
+
78
+ # Extract URLs/hosts from the command
79
+ # Look for http/https URLs and bare hostnames after curl/wget flags
80
+ URLS=$(printf '%s' "$CMD" | grep -oE 'https?://[^[:space:]"'"'"']+' | head -20)
81
+
82
+ if [[ -z "$URLS" ]]; then
83
+ # No parseable URLs — allow (could be a variable-based URL we can't inspect)
84
+ exit 0
85
+ fi
86
+
87
+ BLOCKED_HOST=""
88
+ while IFS= read -r URL; do
89
+ [[ -z "$URL" ]] && continue
90
+ # Extract hostname
91
+ HOST=$(printf '%s' "$URL" | sed -E 's|https?://([^/:?#]+).*|\1|')
92
+
93
+ # Check against allowlist
94
+ ALLOWED=0
95
+ for ALLOWED_HOST in "${ALLOWLIST[@]}"; do
96
+ if [[ "$HOST" == "$ALLOWED_HOST" ]] || [[ "$HOST" == *".$ALLOWED_HOST" ]]; then
97
+ ALLOWED=1
98
+ break
99
+ fi
100
+ done
101
+
102
+ if [[ $ALLOWED -eq 0 ]]; then
103
+ BLOCKED_HOST="$HOST"
104
+ break
105
+ fi
106
+ done <<< "$URLS"
107
+
108
+ if [[ -n "$BLOCKED_HOST" ]]; then
109
+ printf 'NETWORK-EXFIL-GUARD: Request to non-allowlisted host blocked\n' >&2
110
+ printf ' Host: %s\n' "$BLOCKED_HOST" >&2
111
+ printf ' Command: %s\n' "$(printf '%s' "$CMD" | head -c 200)" >&2
112
+ printf 'Block reason: Network requests are restricted to known safe registries and GitHub.\n' >&2
113
+ printf 'Allowlisted hosts: registry.npmjs.org, github.com, api.github.com, raw.githubusercontent.com\n' >&2
114
+ printf 'If this host is needed, request human escalation to update the allowlist.\n' >&2
115
+ exit 2
116
+ fi
117
+
118
+ exit 0
@@ -0,0 +1,101 @@
1
+ #!/bin/bash
2
+ # PostToolUse hook: output-validation.sh
3
+ # Fires AFTER every Bash tool call.
4
+ # Scans tool stdout for credential patterns and blocks (exit 2) if found.
5
+ #
6
+ # Content extraction:
7
+ # PostToolUse → tool_response (stdout from Bash)
8
+ #
9
+ # Exit codes:
10
+ # 0 = no credential patterns detected — allow
11
+ # 2 = credential pattern detected — block
12
+
13
+ set -uo pipefail
14
+
15
+ INPUT=$(cat)
16
+
17
+ # ── Dependency check ──────────────────────────────────────────────────────────
18
+ if ! command -v jq >/dev/null 2>&1; then
19
+ printf 'REAGENT ERROR: jq is required but not installed.\n' >&2
20
+ printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
21
+ exit 2
22
+ fi
23
+
24
+ # ── HALT check ────────────────────────────────────────────────────────────────
25
+ REAGENT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
26
+ HALT_FILE="${REAGENT_ROOT}/.reagent/HALT"
27
+ if [ -f "$HALT_FILE" ]; then
28
+ printf 'REAGENT HALT: %s\nAll agent operations suspended. Run: reagent unfreeze\n' \
29
+ "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
30
+ exit 2
31
+ fi
32
+
33
+ # ── Extract tool output ───────────────────────────────────────────────────────
34
+ # PostToolUse payload has tool_response field containing the output
35
+ OUTPUT=$(printf '%s' "$INPUT" | jq -r '
36
+ if .tool_response then
37
+ if (.tool_response | type) == "array" then
38
+ [.tool_response[] | .text // ""] | join("\n")
39
+ elif (.tool_response | type) == "string" then
40
+ .tool_response
41
+ else
42
+ (.tool_response.content // .tool_response.text // "") | if type == "array" then [.[] | .text // ""] | join("\n") else . end
43
+ end
44
+ else
45
+ ""
46
+ end
47
+ ' 2>/dev/null)
48
+
49
+ if [[ -z "$OUTPUT" ]]; then
50
+ exit 0
51
+ fi
52
+
53
+ # ── Scan for credential patterns ──────────────────────────────────────────────
54
+ FOUND=0
55
+ PATTERN_LABEL=""
56
+
57
+ # AWS access key (AKIA...)
58
+ if printf '%s' "$OUTPUT" | grep -qE 'AKIA[0-9A-Z]{16}'; then
59
+ FOUND=1
60
+ PATTERN_LABEL="AWS Access Key ID (AKIA...)"
61
+ fi
62
+
63
+ # GitHub tokens
64
+ if [[ $FOUND -eq 0 ]] && printf '%s' "$OUTPUT" | grep -qE 'gh[puors]_[A-Za-z0-9]{36}'; then
65
+ FOUND=1
66
+ PATTERN_LABEL="GitHub Personal Access Token (ghp_/ghs_/...)"
67
+ fi
68
+
69
+ # GitHub fine-grained PAT
70
+ if [[ $FOUND -eq 0 ]] && printf '%s' "$OUTPUT" | grep -qE 'github_pat_[A-Za-z0-9_]{82}'; then
71
+ FOUND=1
72
+ PATTERN_LABEL="GitHub fine-grained PAT (github_pat_...)"
73
+ fi
74
+
75
+ # Generic API key pattern: sk-... (OpenAI, Anthropic, Stripe test)
76
+ if [[ $FOUND -eq 0 ]] && printf '%s' "$OUTPUT" | grep -qE 'sk-[A-Za-z0-9_-]{20,}'; then
77
+ FOUND=1
78
+ PATTERN_LABEL="Generic API key (sk-...)"
79
+ fi
80
+
81
+ # Bearer token (with value)
82
+ if [[ $FOUND -eq 0 ]] && printf '%s' "$OUTPUT" | grep -qE 'Bearer [A-Za-z0-9._-]{20,}'; then
83
+ FOUND=1
84
+ PATTERN_LABEL="Bearer token"
85
+ fi
86
+
87
+ # Private key header
88
+ if [[ $FOUND -eq 0 ]] && printf '%s' "$OUTPUT" | grep -qE -- '-----BEGIN (RSA|EC|OPENSSH|PGP) PRIVATE KEY-----'; then
89
+ FOUND=1
90
+ PATTERN_LABEL="Private key block"
91
+ fi
92
+
93
+ if [[ $FOUND -eq 1 ]]; then
94
+ printf 'OUTPUT-VALIDATION: Credential pattern detected in Bash output\n' >&2
95
+ printf ' Pattern matched: %s\n' "$PATTERN_LABEL" >&2
96
+ printf 'Block reason: Tool output contains what appears to be a live credential.\n' >&2
97
+ printf 'The credential must not be logged, forwarded, or stored. Rotate it immediately if real.\n' >&2
98
+ exit 2
99
+ fi
100
+
101
+ exit 0
@@ -0,0 +1,75 @@
1
+ #!/bin/bash
2
+ # PreToolUse hook: rate-limit-guard.sh
3
+ # Fires BEFORE every Bash and Write tool call.
4
+ # Blocks if more than 20 calls to the same tool occur within 60 seconds.
5
+ # Uses a log file per tool in /tmp/reagent-rate-limit-{tool}.log.
6
+ #
7
+ # Exit codes:
8
+ # 0 = within rate limit — allow
9
+ # 2 = rate limit exceeded — block
10
+
11
+ set -uo pipefail
12
+
13
+ INPUT=$(cat)
14
+
15
+ # ── Dependency check ──────────────────────────────────────────────────────────
16
+ if ! command -v jq >/dev/null 2>&1; then
17
+ printf 'REAGENT ERROR: jq is required but not installed.\n' >&2
18
+ printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
19
+ exit 2
20
+ fi
21
+
22
+ # ── HALT check ────────────────────────────────────────────────────────────────
23
+ REAGENT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
24
+ HALT_FILE="${REAGENT_ROOT}/.reagent/HALT"
25
+ if [ -f "$HALT_FILE" ]; then
26
+ printf 'REAGENT HALT: %s\nAll agent operations suspended. Run: reagent unfreeze\n' \
27
+ "$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
28
+ exit 2
29
+ fi
30
+
31
+ TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
32
+
33
+ if [[ -z "$TOOL_NAME" ]]; then
34
+ exit 0
35
+ fi
36
+
37
+ # Sanitize tool name for use in filename
38
+ SAFE_TOOL=$(printf '%s' "$TOOL_NAME" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9-')
39
+ LOG_FILE="/tmp/reagent-rate-limit-${SAFE_TOOL}.log"
40
+
41
+ LIMIT=20
42
+ WINDOW=60 # seconds
43
+
44
+ NOW=$(date +%s)
45
+ CUTOFF=$(( NOW - WINDOW ))
46
+
47
+ # ── Prune old entries and count recent calls ──────────────────────────────────
48
+ RECENT_COUNT=0
49
+
50
+ if [[ -f "$LOG_FILE" ]]; then
51
+ # Filter to only entries within the window, count them
52
+ RECENT_LINES=$(awk -v cutoff="$CUTOFF" '$1 > cutoff' "$LOG_FILE" 2>/dev/null || true)
53
+ RECENT_COUNT=$(printf '%s' "$RECENT_LINES" | grep -c '[0-9]' 2>/dev/null || echo "0")
54
+
55
+ # Rewrite log file with only recent entries (prune old ones)
56
+ printf '%s\n' "$RECENT_LINES" > "$LOG_FILE" 2>/dev/null || true
57
+ else
58
+ # Create log file
59
+ touch "$LOG_FILE" 2>/dev/null || true
60
+ fi
61
+
62
+ # ── Check rate limit ──────────────────────────────────────────────────────────
63
+ if [[ "$RECENT_COUNT" -ge "$LIMIT" ]]; then
64
+ printf 'RATE-LIMIT-GUARD: Tool call rate limit exceeded\n' >&2
65
+ printf ' Tool: %s\n' "$TOOL_NAME" >&2
66
+ printf ' Calls in last 60s: %d (limit: %d)\n' "$RECENT_COUNT" "$LIMIT" >&2
67
+ printf 'Block reason: More than %d %s calls within 60 seconds indicates a runaway loop.\n' "$LIMIT" "$TOOL_NAME" >&2
68
+ printf 'The session will resume normally once the rate window resets.\n' >&2
69
+ exit 2
70
+ fi
71
+
72
+ # ── Record this invocation ────────────────────────────────────────────────────
73
+ printf '%d\n' "$NOW" >> "$LOG_FILE" 2>/dev/null || true
74
+
75
+ exit 0