claude-code-hookkit 1.0.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 +311 -0
- package/dist/add-O4OSFQ76.js +140 -0
- package/dist/chunk-2BZZUQQ3.js +34 -0
- package/dist/chunk-LRXKKJDU.js +101 -0
- package/dist/chunk-PEDGREZY.js +46 -0
- package/dist/chunk-QKT647BI.js +30 -0
- package/dist/chunk-XLX5K6TZ.js +113 -0
- package/dist/cli.js +76 -0
- package/dist/create-DBLA6PTS.js +268 -0
- package/dist/doctor-UBK2C2TW.js +137 -0
- package/dist/info-FLYMAHDX.js +84 -0
- package/dist/init-RHEFGGUF.js +70 -0
- package/dist/list-SCSGYOBR.js +54 -0
- package/dist/remove-Z5QIW45P.js +109 -0
- package/dist/restore-7JQ3CHWZ.js +31 -0
- package/dist/test-ZRRLZ62R.js +194 -0
- package/package.json +59 -0
- package/registry/hooks/cost-tracker.sh +44 -0
- package/registry/hooks/error-advisor.sh +114 -0
- package/registry/hooks/exit-code-enforcer.sh +76 -0
- package/registry/hooks/fixtures/cost-tracker/allow-bash-tool.json +5 -0
- package/registry/hooks/fixtures/cost-tracker/allow-no-session.json +5 -0
- package/registry/hooks/fixtures/error-advisor/allow-enoent.json +5 -0
- package/registry/hooks/fixtures/error-advisor/allow-no-error.json +5 -0
- package/registry/hooks/fixtures/exit-code-enforcer/allow-npm-test.json +5 -0
- package/registry/hooks/fixtures/exit-code-enforcer/block-rm-rf.json +5 -0
- package/registry/hooks/fixtures/post-edit-lint/allow-ts-file.json +5 -0
- package/registry/hooks/fixtures/post-edit-lint/allow-unknown-ext.json +5 -0
- package/registry/hooks/fixtures/sensitive-path-guard/allow-src.json +5 -0
- package/registry/hooks/fixtures/sensitive-path-guard/block-env.json +5 -0
- package/registry/hooks/fixtures/ts-check/allow-non-ts.json +5 -0
- package/registry/hooks/fixtures/ts-check/allow-ts-file.json +5 -0
- package/registry/hooks/fixtures/web-budget-gate/allow-within-budget.json +6 -0
- package/registry/hooks/fixtures/web-budget-gate/block-over-budget.json +6 -0
- package/registry/hooks/post-edit-lint.sh +82 -0
- package/registry/hooks/sensitive-path-guard.sh +103 -0
- package/registry/hooks/ts-check.sh +98 -0
- package/registry/hooks/web-budget-gate.sh +60 -0
- package/registry/registry.json +81 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# exit-code-enforcer.sh
|
|
3
|
+
# PreToolUse hook for Bash events
|
|
4
|
+
#
|
|
5
|
+
# Blocks execution of known-dangerous shell commands that could cause
|
|
6
|
+
# irreversible damage to the system (data destruction, privilege escalation,
|
|
7
|
+
# fork bombs, etc.)
|
|
8
|
+
#
|
|
9
|
+
# Claude Code passes JSON via stdin. We extract tool_input.command
|
|
10
|
+
# and check it against dangerous patterns using grep (POSIX, no jq needed).
|
|
11
|
+
#
|
|
12
|
+
# Exit 2 = block (Claude Code spec), Exit 0 = allow
|
|
13
|
+
|
|
14
|
+
# Read all stdin into a variable
|
|
15
|
+
INPUT=$(cat)
|
|
16
|
+
|
|
17
|
+
# Extract command value from tool_input JSON.
|
|
18
|
+
# Strategy: isolate the "command":"..." pair, handling escaped quotes inside the value.
|
|
19
|
+
# 1. grep extracts "command": followed by a JSON string (handling \" escapes)
|
|
20
|
+
# 2. sed strips the key and outer quotes
|
|
21
|
+
# 3. Second sed unescapes JSON string escapes
|
|
22
|
+
COMMAND_RAW=$(printf '%s' "$INPUT" | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"\\]*\(\\.[^"\\]*\)*"' | head -1 | sed 's/^"command"[[:space:]]*:[[:space:]]*"//; s/"$//')
|
|
23
|
+
COMMAND=$(printf '%s' "$COMMAND_RAW" | sed 's/\\"/"/g; s/\\\\/\\/g')
|
|
24
|
+
|
|
25
|
+
# If no command found, allow (can't determine what's being run)
|
|
26
|
+
if [ -z "$COMMAND" ]; then
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# --- Dangerous command patterns ---
|
|
31
|
+
# Each pattern check is explained inline.
|
|
32
|
+
|
|
33
|
+
# Block recursive force-delete of root filesystem
|
|
34
|
+
# This would permanently destroy the entire OS installation
|
|
35
|
+
if printf '%s' "$COMMAND" | grep -qE 'rm[[:space:]].*-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*[[:space:]]+/[[:space:]]*$|rm[[:space:]].*-[a-zA-Z]*f[a-zA-Z]*r[a-zA-Z]*[[:space:]]+/[[:space:]]*$'; then
|
|
36
|
+
printf 'BLOCKED: "rm -rf /" would delete the entire filesystem. Command: %s\n' "$COMMAND" >&2
|
|
37
|
+
exit 2
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Block rm -rf targeting home directory by path or variable
|
|
41
|
+
# These would delete all user files and configuration
|
|
42
|
+
if printf '%s' "$COMMAND" | grep -qE 'rm[[:space:]].*-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*[[:space:]].*(~|\$HOME)[[:space:]]*$|rm[[:space:]].*-[a-zA-Z]*f[a-zA-Z]*r[a-zA-Z]*[[:space:]].*(~|\$HOME)[[:space:]]*$'; then
|
|
43
|
+
printf 'BLOCKED: "rm -rf ~" or "rm -rf $HOME" would delete your home directory. Command: %s\n' "$COMMAND" >&2
|
|
44
|
+
exit 2
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Block chmod 777 on system directories or broad paths
|
|
48
|
+
# World-writable permissions on directories create security vulnerabilities
|
|
49
|
+
if printf '%s' "$COMMAND" | grep -qE 'chmod[[:space:]]+(777|a\+rwx|ugo\+rwx)[[:space:]]+(/[^[:space:]]*)?$'; then
|
|
50
|
+
printf 'BLOCKED: "chmod 777" on system paths creates severe security vulnerabilities. Command: %s\n' "$COMMAND" >&2
|
|
51
|
+
exit 2
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Block writing directly to raw disk devices
|
|
55
|
+
# This would corrupt the filesystem/OS partition table
|
|
56
|
+
if printf '%s' "$COMMAND" | grep -qE '>[[:space:]]*/dev/(sd[a-z]|hd[a-z]|nvme[0-9]|disk[0-9])[[:space:]]*$'; then
|
|
57
|
+
printf 'BLOCKED: Writing directly to a block device would corrupt the filesystem. Command: %s\n' "$COMMAND" >&2
|
|
58
|
+
exit 2
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Block fork bombs — the classic ":(){:|:&};:" pattern and common variants
|
|
62
|
+
# These consume all system resources and require a hard reboot
|
|
63
|
+
if printf '%s' "$COMMAND" | grep -qE ':\(\)\{[[:space:]]*:\|:'; then
|
|
64
|
+
printf 'BLOCKED: Fork bomb detected. This would consume all system resources. Command: %s\n' "$COMMAND" >&2
|
|
65
|
+
exit 2
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# Block dd reading from /dev/zero or /dev/random and writing to disk devices
|
|
69
|
+
# dd if=/dev/zero of=/dev/sda would overwrite a disk with zeros, destroying all data
|
|
70
|
+
if printf '%s' "$COMMAND" | grep -qE 'dd[[:space:]].*of=/dev/(sd[a-z]|hd[a-z]|nvme[0-9]|disk[0-9])'; then
|
|
71
|
+
printf 'BLOCKED: "dd" writing to a block device would destroy all data on that disk. Command: %s\n' "$COMMAND" >&2
|
|
72
|
+
exit 2
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# Allow everything else
|
|
76
|
+
exit 0
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# post-edit-lint.sh
|
|
3
|
+
# PostToolUse hook for Write|Edit events
|
|
4
|
+
#
|
|
5
|
+
# Runs the appropriate linter on the file that Claude just edited,
|
|
6
|
+
# providing immediate feedback on code quality issues.
|
|
7
|
+
#
|
|
8
|
+
# Linter routing by file extension:
|
|
9
|
+
# .ts/.tsx -> ESLint (eslint) or Biome (biome check)
|
|
10
|
+
# .py -> Ruff (ruff check)
|
|
11
|
+
# .sh/.bash -> ShellCheck (shellcheck)
|
|
12
|
+
#
|
|
13
|
+
# If the linter for a file type is not installed, we skip silently.
|
|
14
|
+
# This hook is purely informational — it always exits 0 to avoid blocking.
|
|
15
|
+
#
|
|
16
|
+
# Claude Code passes JSON via stdin. We extract tool_input.file_path.
|
|
17
|
+
#
|
|
18
|
+
# Exit 0 = always allow (informational PostToolUse hook)
|
|
19
|
+
|
|
20
|
+
# Read all stdin into a variable
|
|
21
|
+
INPUT=$(cat)
|
|
22
|
+
|
|
23
|
+
# Extract file_path from tool_input using grep/sed
|
|
24
|
+
FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/"file_path"[[:space:]]*:[[:space:]]*"\(.*\)"/\1/')
|
|
25
|
+
|
|
26
|
+
# If no file_path found, try path field (some tools use different field names)
|
|
27
|
+
if [ -z "$FILE_PATH" ]; then
|
|
28
|
+
FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/"path"[[:space:]]*:[[:space:]]*"\(.*\)"/\1/')
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# If no file path found, nothing to lint
|
|
32
|
+
if [ -z "$FILE_PATH" ]; then
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Check the file still exists (it might be a delete operation)
|
|
37
|
+
if [ ! -f "$FILE_PATH" ]; then
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Get the file extension (lowercase for case-insensitive matching)
|
|
42
|
+
EXTENSION=$(printf '%s' "$FILE_PATH" | sed 's/.*\.//' | tr '[:upper:]' '[:lower:]')
|
|
43
|
+
|
|
44
|
+
case "$EXTENSION" in
|
|
45
|
+
ts|tsx|js|jsx|mjs|cjs)
|
|
46
|
+
# TypeScript/JavaScript: try ESLint first, then Biome
|
|
47
|
+
if command -v eslint > /dev/null 2>&1; then
|
|
48
|
+
printf '[post-edit-lint] Running ESLint on %s\n' "$FILE_PATH" >&2
|
|
49
|
+
eslint --no-eslintrc --rule "{}" "$FILE_PATH" 2>&1 >&2 || true
|
|
50
|
+
elif command -v biome > /dev/null 2>&1; then
|
|
51
|
+
printf '[post-edit-lint] Running Biome check on %s\n' "$FILE_PATH" >&2
|
|
52
|
+
biome check "$FILE_PATH" 2>&1 >&2 || true
|
|
53
|
+
else
|
|
54
|
+
# No linter found — skip silently (don't block for missing tools)
|
|
55
|
+
:
|
|
56
|
+
fi
|
|
57
|
+
;;
|
|
58
|
+
|
|
59
|
+
py)
|
|
60
|
+
# Python: use Ruff (fast, modern Python linter)
|
|
61
|
+
if command -v ruff > /dev/null 2>&1; then
|
|
62
|
+
printf '[post-edit-lint] Running Ruff on %s\n' "$FILE_PATH" >&2
|
|
63
|
+
ruff check "$FILE_PATH" 2>&1 >&2 || true
|
|
64
|
+
fi
|
|
65
|
+
;;
|
|
66
|
+
|
|
67
|
+
sh|bash)
|
|
68
|
+
# Shell scripts: use ShellCheck for static analysis
|
|
69
|
+
if command -v shellcheck > /dev/null 2>&1; then
|
|
70
|
+
printf '[post-edit-lint] Running ShellCheck on %s\n' "$FILE_PATH" >&2
|
|
71
|
+
shellcheck "$FILE_PATH" 2>&1 >&2 || true
|
|
72
|
+
fi
|
|
73
|
+
;;
|
|
74
|
+
|
|
75
|
+
*)
|
|
76
|
+
# Unknown file type — no linter available, skip silently
|
|
77
|
+
:
|
|
78
|
+
;;
|
|
79
|
+
esac
|
|
80
|
+
|
|
81
|
+
# Always exit 0 — this hook informs, it does not block
|
|
82
|
+
exit 0
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# sensitive-path-guard.sh
|
|
3
|
+
# PreToolUse hook for Edit|Write events
|
|
4
|
+
#
|
|
5
|
+
# Blocks writes to sensitive files: .env, credentials, keys, secrets, PEM certs,
|
|
6
|
+
# SSH private keys, and the claude-code-hookkit settings file itself.
|
|
7
|
+
#
|
|
8
|
+
# Claude Code passes JSON via stdin. We extract tool_input.file_path
|
|
9
|
+
# and check it against blocked patterns using grep (POSIX, no jq needed).
|
|
10
|
+
#
|
|
11
|
+
# Exit 2 = block (Claude Code spec), Exit 0 = allow
|
|
12
|
+
|
|
13
|
+
# Read all stdin into a variable (handles multi-line JSON)
|
|
14
|
+
INPUT=$(cat)
|
|
15
|
+
|
|
16
|
+
# Extract file_path from tool_input using grep/sed
|
|
17
|
+
# Claude Code sends: {"tool_input": {"file_path": "/path/to/file", ...}}
|
|
18
|
+
FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/"file_path"[[:space:]]*:[[:space:]]*"\(.*\)"/\1/')
|
|
19
|
+
|
|
20
|
+
# If no file_path found (e.g. Write tool may use different field), try path field
|
|
21
|
+
if [ -z "$FILE_PATH" ]; then
|
|
22
|
+
FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/"path"[[:space:]]*:[[:space:]]*"\(.*\)"/\1/')
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# If still no path found, allow (can't determine what's being written)
|
|
26
|
+
if [ -z "$FILE_PATH" ]; then
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Extract just the filename (basename) for pattern matching
|
|
31
|
+
BASENAME=$(basename "$FILE_PATH")
|
|
32
|
+
|
|
33
|
+
# --- Blocked patterns ---
|
|
34
|
+
# Each pattern is explained inline.
|
|
35
|
+
|
|
36
|
+
# Block .env files (exact match and prefixed variants like .env.local, .env.production)
|
|
37
|
+
# These contain API keys, database URLs, and other secrets.
|
|
38
|
+
case "$BASENAME" in
|
|
39
|
+
.env|.env.*|env)
|
|
40
|
+
printf 'BLOCKED: Writing to env file "%s" is not allowed. Move secrets to a vault or use environment variables.\n' "$FILE_PATH" >&2
|
|
41
|
+
exit 2
|
|
42
|
+
;;
|
|
43
|
+
esac
|
|
44
|
+
|
|
45
|
+
# Block files with "credentials" in the name — cloud provider credential files
|
|
46
|
+
# e.g. ~/.aws/credentials, gcloud credentials.json
|
|
47
|
+
if printf '%s' "$BASENAME" | grep -qi "credential"; then
|
|
48
|
+
printf 'BLOCKED: Writing to credentials file "%s" is not allowed.\n' "$FILE_PATH" >&2
|
|
49
|
+
exit 2
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Block files with "secret" in the name — generic secret storage
|
|
53
|
+
if printf '%s' "$BASENAME" | grep -qi "secret"; then
|
|
54
|
+
printf 'BLOCKED: Writing to secrets file "%s" is not allowed.\n' "$FILE_PATH" >&2
|
|
55
|
+
exit 2
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Block private key files (.key, .pem, .p12, .pfx)
|
|
59
|
+
# These are TLS/SSL private keys and certificate bundles
|
|
60
|
+
case "$BASENAME" in
|
|
61
|
+
*.key|*.pem|*.p12|*.pfx)
|
|
62
|
+
printf 'BLOCKED: Writing to private key/certificate file "%s" is not allowed.\n' "$FILE_PATH" >&2
|
|
63
|
+
exit 2
|
|
64
|
+
;;
|
|
65
|
+
esac
|
|
66
|
+
|
|
67
|
+
# Block SSH private keys by conventional name
|
|
68
|
+
# OpenSSH default key names
|
|
69
|
+
case "$BASENAME" in
|
|
70
|
+
id_rsa|id_ed25519|id_ecdsa|id_dsa)
|
|
71
|
+
printf 'BLOCKED: Writing to SSH private key file "%s" is not allowed.\n' "$FILE_PATH" >&2
|
|
72
|
+
exit 2
|
|
73
|
+
;;
|
|
74
|
+
esac
|
|
75
|
+
|
|
76
|
+
# Block the claude-code-hookkit settings file itself to prevent hooks from modifying
|
|
77
|
+
# their own configuration (self-modification guard)
|
|
78
|
+
case "$BASENAME" in
|
|
79
|
+
settings.json)
|
|
80
|
+
# Only block if it's inside a .claude directory
|
|
81
|
+
if printf '%s' "$FILE_PATH" | grep -q '\.claude'; then
|
|
82
|
+
printf 'BLOCKED: Writing to Claude settings file "%s" is not allowed from within a hook.\n' "$FILE_PATH" >&2
|
|
83
|
+
exit 2
|
|
84
|
+
fi
|
|
85
|
+
;;
|
|
86
|
+
esac
|
|
87
|
+
|
|
88
|
+
# Block .htpasswd files — Apache password files
|
|
89
|
+
case "$BASENAME" in
|
|
90
|
+
.htpasswd)
|
|
91
|
+
printf 'BLOCKED: Writing to password file "%s" is not allowed.\n' "$FILE_PATH" >&2
|
|
92
|
+
exit 2
|
|
93
|
+
;;
|
|
94
|
+
esac
|
|
95
|
+
|
|
96
|
+
# Block keychain/keystore files
|
|
97
|
+
if printf '%s' "$BASENAME" | grep -qi "keystore\|keychain"; then
|
|
98
|
+
printf 'BLOCKED: Writing to keystore/keychain file "%s" is not allowed.\n' "$FILE_PATH" >&2
|
|
99
|
+
exit 2
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# Allow everything else
|
|
103
|
+
exit 0
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ts-check.sh
|
|
3
|
+
# PostToolUse hook for Write|Edit events
|
|
4
|
+
#
|
|
5
|
+
# Runs TypeScript type checking (tsc --noEmit) after Claude edits a .ts or .tsx file.
|
|
6
|
+
# This gives Claude immediate feedback on type errors so it can self-correct.
|
|
7
|
+
#
|
|
8
|
+
# Only runs for .ts and .tsx files. Skips silently for all other file types.
|
|
9
|
+
# If tsc is not available (no TypeScript project), exits 0 silently.
|
|
10
|
+
#
|
|
11
|
+
# Output goes to stderr so Claude can see type errors in the hook output.
|
|
12
|
+
#
|
|
13
|
+
# Claude Code passes JSON via stdin. We extract tool_input.file_path.
|
|
14
|
+
#
|
|
15
|
+
# Exit 0 = always allow (informational PostToolUse hook)
|
|
16
|
+
|
|
17
|
+
# Read all stdin into a variable
|
|
18
|
+
INPUT=$(cat)
|
|
19
|
+
|
|
20
|
+
# Extract file_path from tool_input using grep/sed
|
|
21
|
+
FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/"file_path"[[:space:]]*:[[:space:]]*"\(.*\)"/\1/')
|
|
22
|
+
|
|
23
|
+
# If no file_path found, try path field
|
|
24
|
+
if [ -z "$FILE_PATH" ]; then
|
|
25
|
+
FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/"path"[[:space:]]*:[[:space:]]*"\(.*\)"/\1/')
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# If no file path found, nothing to check
|
|
29
|
+
if [ -z "$FILE_PATH" ]; then
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Get the file extension (lowercase)
|
|
34
|
+
EXTENSION=$(printf '%s' "$FILE_PATH" | sed 's/.*\.//' | tr '[:upper:]' '[:lower:]')
|
|
35
|
+
|
|
36
|
+
# Only run on TypeScript files
|
|
37
|
+
case "$EXTENSION" in
|
|
38
|
+
ts|tsx)
|
|
39
|
+
# TypeScript file — proceed with type check
|
|
40
|
+
;;
|
|
41
|
+
*)
|
|
42
|
+
# Not a TypeScript file — skip silently
|
|
43
|
+
exit 0
|
|
44
|
+
;;
|
|
45
|
+
esac
|
|
46
|
+
|
|
47
|
+
# Check if npx is available (needed to run tsc from node_modules)
|
|
48
|
+
if ! command -v npx > /dev/null 2>&1; then
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Find the tsconfig.json by walking up from the file's directory
|
|
53
|
+
# This handles monorepos and nested TypeScript projects
|
|
54
|
+
FILE_DIR=$(dirname "$FILE_PATH")
|
|
55
|
+
TSCONFIG=""
|
|
56
|
+
SEARCH_DIR="$FILE_DIR"
|
|
57
|
+
|
|
58
|
+
# Walk up directory tree looking for tsconfig.json (max 10 levels to avoid infinite loop)
|
|
59
|
+
i=0
|
|
60
|
+
while [ $i -lt 10 ]; do
|
|
61
|
+
if [ -f "$SEARCH_DIR/tsconfig.json" ]; then
|
|
62
|
+
TSCONFIG="$SEARCH_DIR/tsconfig.json"
|
|
63
|
+
break
|
|
64
|
+
fi
|
|
65
|
+
PARENT=$(dirname "$SEARCH_DIR")
|
|
66
|
+
# Stop if we've reached the filesystem root
|
|
67
|
+
if [ "$PARENT" = "$SEARCH_DIR" ]; then
|
|
68
|
+
break
|
|
69
|
+
fi
|
|
70
|
+
SEARCH_DIR="$PARENT"
|
|
71
|
+
i=$((i + 1))
|
|
72
|
+
done
|
|
73
|
+
|
|
74
|
+
# If no tsconfig.json found, this isn't a TypeScript project — skip
|
|
75
|
+
if [ -z "$TSCONFIG" ]; then
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Get the project root (directory containing tsconfig.json)
|
|
80
|
+
PROJECT_ROOT=$(dirname "$TSCONFIG")
|
|
81
|
+
|
|
82
|
+
printf '[ts-check] Running tsc --noEmit in %s\n' "$PROJECT_ROOT" >&2
|
|
83
|
+
|
|
84
|
+
# Run tsc --noEmit from the project root directory
|
|
85
|
+
# --pretty enables colored output for readability
|
|
86
|
+
# Capture output and send to stderr for Claude to see
|
|
87
|
+
TSC_OUTPUT=$(cd "$PROJECT_ROOT" && npx tsc --noEmit --pretty 2>&1)
|
|
88
|
+
TSC_EXIT=$?
|
|
89
|
+
|
|
90
|
+
if [ $TSC_EXIT -ne 0 ]; then
|
|
91
|
+
printf '[ts-check] TypeScript errors found:\n' >&2
|
|
92
|
+
printf '%s\n' "$TSC_OUTPUT" >&2
|
|
93
|
+
else
|
|
94
|
+
printf '[ts-check] No TypeScript errors.\n' >&2
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Always exit 0 — this hook informs, it does not block
|
|
98
|
+
exit 0
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# web-budget-gate.sh
|
|
3
|
+
# PreToolUse hook for WebSearch|WebFetch events
|
|
4
|
+
#
|
|
5
|
+
# Enforces a per-session cap on web search and fetch calls to control LLM costs.
|
|
6
|
+
# Each call increments a counter stored in /tmp. When the counter exceeds the limit,
|
|
7
|
+
# further web calls are blocked until the session ends.
|
|
8
|
+
#
|
|
9
|
+
# Configuration:
|
|
10
|
+
# CLAUDE_HOOKS_WEB_LIMIT — max web calls per session (default: 10)
|
|
11
|
+
#
|
|
12
|
+
# Counter file: /tmp/claude-code-hookkit-web-count-<session_id>
|
|
13
|
+
#
|
|
14
|
+
# Claude Code passes JSON via stdin. We extract session_id for tracking.
|
|
15
|
+
#
|
|
16
|
+
# Exit codes:
|
|
17
|
+
# 0 - allow (budget not exceeded)
|
|
18
|
+
# 2 - block (budget exceeded, shows reason to Claude)
|
|
19
|
+
|
|
20
|
+
INPUT=$(cat)
|
|
21
|
+
|
|
22
|
+
# Extract session_id from JSON using grep/sed (no jq or python3 required)
|
|
23
|
+
SESSION_ID=$(printf '%s' "$INPUT" | grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/"session_id"[[:space:]]*:[[:space:]]*"\(.*\)"/\1/')
|
|
24
|
+
|
|
25
|
+
# If no session_id, use a fallback key (shouldn't happen in practice)
|
|
26
|
+
if [ -z "$SESSION_ID" ]; then
|
|
27
|
+
SESSION_ID="default"
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Get the web call limit — default 10, overridable via env var
|
|
31
|
+
WEB_LIMIT="${CLAUDE_HOOKS_WEB_LIMIT:-10}"
|
|
32
|
+
|
|
33
|
+
# Counter file path — unique per session
|
|
34
|
+
COUNTER_FILE="/tmp/claude-code-hookkit-web-count-${SESSION_ID}"
|
|
35
|
+
|
|
36
|
+
# Read current count from file (default 0 if file doesn't exist)
|
|
37
|
+
if [ -f "$COUNTER_FILE" ]; then
|
|
38
|
+
CURRENT_COUNT=$(cat "$COUNTER_FILE" 2>/dev/null)
|
|
39
|
+
# Sanitize: ensure it's a non-negative integer
|
|
40
|
+
CURRENT_COUNT=$(printf '%s' "$CURRENT_COUNT" | grep -o '^[0-9]*$')
|
|
41
|
+
if [ -z "$CURRENT_COUNT" ]; then
|
|
42
|
+
CURRENT_COUNT=0
|
|
43
|
+
fi
|
|
44
|
+
else
|
|
45
|
+
CURRENT_COUNT=0
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Check if the budget is already exhausted BEFORE incrementing
|
|
49
|
+
# This prevents off-by-one: the Nth call should be the last allowed, (N+1)th is blocked
|
|
50
|
+
if [ "$CURRENT_COUNT" -ge "$WEB_LIMIT" ]; then
|
|
51
|
+
printf 'Web budget exceeded: %d/%d calls used this session. Set CLAUDE_HOOKS_WEB_LIMIT to increase the limit.\n' "$CURRENT_COUNT" "$WEB_LIMIT" >&2
|
|
52
|
+
exit 2
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# Increment the counter and write back to the file
|
|
56
|
+
NEW_COUNT=$((CURRENT_COUNT + 1))
|
|
57
|
+
printf '%d' "$NEW_COUNT" > "$COUNTER_FILE"
|
|
58
|
+
|
|
59
|
+
# Allow the web call
|
|
60
|
+
exit 0
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"sensitive-path-guard": {
|
|
4
|
+
"name": "sensitive-path-guard",
|
|
5
|
+
"description": "Blocks writes to sensitive paths (.env, credentials, keys, etc.)",
|
|
6
|
+
"event": "PreToolUse",
|
|
7
|
+
"matcher": "Edit|Write",
|
|
8
|
+
"pack": "security-pack",
|
|
9
|
+
"scriptFile": "sensitive-path-guard.sh"
|
|
10
|
+
},
|
|
11
|
+
"exit-code-enforcer": {
|
|
12
|
+
"name": "exit-code-enforcer",
|
|
13
|
+
"description": "Ensures hook scripts use exit code 2 to block (not exit 1)",
|
|
14
|
+
"event": "PreToolUse",
|
|
15
|
+
"matcher": "Bash",
|
|
16
|
+
"pack": "security-pack",
|
|
17
|
+
"scriptFile": "exit-code-enforcer.sh"
|
|
18
|
+
},
|
|
19
|
+
"post-edit-lint": {
|
|
20
|
+
"name": "post-edit-lint",
|
|
21
|
+
"description": "Runs linter on files after Claude edits them",
|
|
22
|
+
"event": "PostToolUse",
|
|
23
|
+
"matcher": "Write|Edit",
|
|
24
|
+
"pack": "quality-pack",
|
|
25
|
+
"scriptFile": "post-edit-lint.sh"
|
|
26
|
+
},
|
|
27
|
+
"ts-check": {
|
|
28
|
+
"name": "ts-check",
|
|
29
|
+
"description": "Runs TypeScript type checking after code changes",
|
|
30
|
+
"event": "PostToolUse",
|
|
31
|
+
"matcher": "Write|Edit",
|
|
32
|
+
"pack": "quality-pack",
|
|
33
|
+
"scriptFile": "ts-check.sh"
|
|
34
|
+
},
|
|
35
|
+
"web-budget-gate": {
|
|
36
|
+
"name": "web-budget-gate",
|
|
37
|
+
"description": "Limits web search/fetch calls per session to control costs",
|
|
38
|
+
"event": "PreToolUse",
|
|
39
|
+
"matcher": "WebSearch|WebFetch",
|
|
40
|
+
"pack": "cost-pack",
|
|
41
|
+
"scriptFile": "web-budget-gate.sh"
|
|
42
|
+
},
|
|
43
|
+
"cost-tracker": {
|
|
44
|
+
"name": "cost-tracker",
|
|
45
|
+
"description": "Tracks tool usage costs per session",
|
|
46
|
+
"event": "PostToolUse",
|
|
47
|
+
"pack": "cost-pack",
|
|
48
|
+
"scriptFile": "cost-tracker.sh"
|
|
49
|
+
},
|
|
50
|
+
"error-advisor": {
|
|
51
|
+
"name": "error-advisor",
|
|
52
|
+
"description": "Provides contextual fix suggestions when commands fail",
|
|
53
|
+
"event": "PostToolUse",
|
|
54
|
+
"matcher": "Bash",
|
|
55
|
+
"pack": "error-pack",
|
|
56
|
+
"scriptFile": "error-advisor.sh"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"packs": {
|
|
60
|
+
"security-pack": {
|
|
61
|
+
"name": "security-pack",
|
|
62
|
+
"description": "Essential security hooks for Claude Code",
|
|
63
|
+
"hooks": ["sensitive-path-guard", "exit-code-enforcer"]
|
|
64
|
+
},
|
|
65
|
+
"quality-pack": {
|
|
66
|
+
"name": "quality-pack",
|
|
67
|
+
"description": "Code quality hooks (linting, type checking)",
|
|
68
|
+
"hooks": ["post-edit-lint", "ts-check"]
|
|
69
|
+
},
|
|
70
|
+
"cost-pack": {
|
|
71
|
+
"name": "cost-pack",
|
|
72
|
+
"description": "Cost control and usage tracking hooks",
|
|
73
|
+
"hooks": ["web-budget-gate", "cost-tracker"]
|
|
74
|
+
},
|
|
75
|
+
"error-pack": {
|
|
76
|
+
"name": "error-pack",
|
|
77
|
+
"description": "Error recovery and fix suggestion hooks",
|
|
78
|
+
"hooks": ["error-advisor"]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|