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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +311 -0
  3. package/dist/add-O4OSFQ76.js +140 -0
  4. package/dist/chunk-2BZZUQQ3.js +34 -0
  5. package/dist/chunk-LRXKKJDU.js +101 -0
  6. package/dist/chunk-PEDGREZY.js +46 -0
  7. package/dist/chunk-QKT647BI.js +30 -0
  8. package/dist/chunk-XLX5K6TZ.js +113 -0
  9. package/dist/cli.js +76 -0
  10. package/dist/create-DBLA6PTS.js +268 -0
  11. package/dist/doctor-UBK2C2TW.js +137 -0
  12. package/dist/info-FLYMAHDX.js +84 -0
  13. package/dist/init-RHEFGGUF.js +70 -0
  14. package/dist/list-SCSGYOBR.js +54 -0
  15. package/dist/remove-Z5QIW45P.js +109 -0
  16. package/dist/restore-7JQ3CHWZ.js +31 -0
  17. package/dist/test-ZRRLZ62R.js +194 -0
  18. package/package.json +59 -0
  19. package/registry/hooks/cost-tracker.sh +44 -0
  20. package/registry/hooks/error-advisor.sh +114 -0
  21. package/registry/hooks/exit-code-enforcer.sh +76 -0
  22. package/registry/hooks/fixtures/cost-tracker/allow-bash-tool.json +5 -0
  23. package/registry/hooks/fixtures/cost-tracker/allow-no-session.json +5 -0
  24. package/registry/hooks/fixtures/error-advisor/allow-enoent.json +5 -0
  25. package/registry/hooks/fixtures/error-advisor/allow-no-error.json +5 -0
  26. package/registry/hooks/fixtures/exit-code-enforcer/allow-npm-test.json +5 -0
  27. package/registry/hooks/fixtures/exit-code-enforcer/block-rm-rf.json +5 -0
  28. package/registry/hooks/fixtures/post-edit-lint/allow-ts-file.json +5 -0
  29. package/registry/hooks/fixtures/post-edit-lint/allow-unknown-ext.json +5 -0
  30. package/registry/hooks/fixtures/sensitive-path-guard/allow-src.json +5 -0
  31. package/registry/hooks/fixtures/sensitive-path-guard/block-env.json +5 -0
  32. package/registry/hooks/fixtures/ts-check/allow-non-ts.json +5 -0
  33. package/registry/hooks/fixtures/ts-check/allow-ts-file.json +5 -0
  34. package/registry/hooks/fixtures/web-budget-gate/allow-within-budget.json +6 -0
  35. package/registry/hooks/fixtures/web-budget-gate/block-over-budget.json +6 -0
  36. package/registry/hooks/post-edit-lint.sh +82 -0
  37. package/registry/hooks/sensitive-path-guard.sh +103 -0
  38. package/registry/hooks/ts-check.sh +98 -0
  39. package/registry/hooks/web-budget-gate.sh +60 -0
  40. 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,5 @@
1
+ {
2
+ "description": "Tracks Bash tool usage (always exits 0, advisory hook)",
3
+ "input": { "session_id": "fixture-cost-test", "tool_name": "Bash" },
4
+ "expectedExitCode": 0
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "Handles missing session_id gracefully (exits 0)",
3
+ "input": { "tool_name": "Read" },
4
+ "expectedExitCode": 0
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "Provides ENOENT advice to stderr (always exits 0, advisory hook)",
3
+ "input": { "tool_name": "Bash", "tool_response": "ENOENT: no such file or directory" },
4
+ "expectedExitCode": 0
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "Silent when no error detected (exits 0 with no output)",
3
+ "input": { "tool_name": "Bash", "tool_response": "Success" },
4
+ "expectedExitCode": 0
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "Allows safe npm test command",
3
+ "input": { "tool_input": { "command": "npm test" } },
4
+ "expectedExitCode": 0
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "Blocks destructive rm -rf /",
3
+ "input": { "tool_input": { "command": "rm -rf /" } },
4
+ "expectedExitCode": 2
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "Exits 0 for TypeScript file (linter runs externally, hook always succeeds)",
3
+ "input": { "tool_input": { "file_path": "src/app.ts" } },
4
+ "expectedExitCode": 0
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "Exits 0 for unknown file extension (no linter configured)",
3
+ "input": { "tool_input": { "file_path": "file.xyz" } },
4
+ "expectedExitCode": 0
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "Allows writes to source files",
3
+ "input": { "tool_input": { "file_path": "src/app.ts" } },
4
+ "expectedExitCode": 0
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "Blocks writes to .env files",
3
+ "input": { "tool_input": { "file_path": ".env" } },
4
+ "expectedExitCode": 2
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "Exits 0 for non-TypeScript file (hook skips non-TS files)",
3
+ "input": { "tool_input": { "file_path": "app.py" } },
4
+ "expectedExitCode": 0
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "description": "Exits 0 for TypeScript file (tsc runs externally, hook always succeeds)",
3
+ "input": { "tool_input": { "file_path": "src/app.ts" } },
4
+ "expectedExitCode": 0
5
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "description": "Allows call when within budget (fresh session, limit=10)",
3
+ "input": { "session_id": "fixture-allow-test" },
4
+ "expectedExitCode": 0,
5
+ "env": { "CLAUDE_HOOKS_WEB_LIMIT": "10" }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "description": "Blocks when budget limit is 0 (any call immediately exhausts budget)",
3
+ "input": { "session_id": "fixture-block-budget-test" },
4
+ "expectedExitCode": 2,
5
+ "env": { "CLAUDE_HOOKS_WEB_LIMIT": "0" }
6
+ }
@@ -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
+ }