@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.
- package/dist/cli/commands/init/github.d.ts +13 -0
- package/dist/cli/commands/init/github.d.ts.map +1 -0
- package/dist/cli/commands/init/github.js +81 -0
- package/dist/cli/commands/init/github.js.map +1 -0
- package/dist/cli/commands/init/index.d.ts.map +1 -1
- package/dist/cli/commands/init/index.js +11 -0
- package/dist/cli/commands/init/index.js.map +1 -1
- package/dist/gateway/native-tools.d.ts.map +1 -1
- package/dist/gateway/native-tools.js +38 -0
- package/dist/gateway/native-tools.js.map +1 -1
- package/dist/pm/github-bridge.d.ts +35 -0
- package/dist/pm/github-bridge.d.ts.map +1 -1
- package/dist/pm/github-bridge.js +185 -0
- package/dist/pm/github-bridge.js.map +1 -1
- package/dist/pm/types.d.ts +7 -7
- package/hooks/ci-config-protection.sh +84 -0
- package/hooks/file-size-guard.sh +64 -0
- package/hooks/git-config-guard.sh +81 -0
- package/hooks/import-guard.sh +99 -0
- package/hooks/network-exfil-guard.sh +118 -0
- package/hooks/output-validation.sh +101 -0
- package/hooks/rate-limit-guard.sh +75 -0
- package/hooks/symlink-guard.sh +96 -0
- package/package.json +1 -1
|
@@ -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
|