feat: initial companion repo for CC-024 Hooks
This commit is contained in:
commit
cd834a7476
7 changed files with 790 additions and 0 deletions
51
hooks/post-tool-use.sh
Normal file
51
hooks/post-tool-use.sh
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
#!/bin/bash
|
||||
# post-tool-use.sh: Append an audit log entry after every tool call.
|
||||
#
|
||||
# Claude Code calls this script AFTER every tool execution.
|
||||
# The script receives a JSON object on stdin with two fields:
|
||||
# - tool_name: the name of the tool that ran (e.g., "Bash", "Read", "Write")
|
||||
# - tool_input: the arguments passed to the tool
|
||||
#
|
||||
# This script writes one line to hooks/audit.log:
|
||||
# 2026-03-30T04:12:33Z | Bash | ls -la /tmp
|
||||
#
|
||||
# The log is append-only. It grows during the session and persists between sessions.
|
||||
# Read it any time with: cat hooks/audit.log
|
||||
#
|
||||
# Exit code:
|
||||
# 0 Always. This hook does not block anything.
|
||||
#
|
||||
# Make this script executable: chmod +x hooks/post-tool-use.sh
|
||||
|
||||
# Read the full JSON payload from stdin
|
||||
input=$(cat)
|
||||
|
||||
# Extract the tool name
|
||||
tool_name=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name','unknown'))" 2>/dev/null || echo "unknown")
|
||||
|
||||
# Extract the most relevant part of the tool input.
|
||||
# Different tools use different argument keys:
|
||||
# Bash -> command
|
||||
# Read -> file_path
|
||||
# Write -> file_path
|
||||
# Grep -> pattern
|
||||
# Fall back to a truncated string representation of the entire input dict.
|
||||
tool_input=$(echo "$input" | python3 -c "
|
||||
import sys, json
|
||||
d = json.load(sys.stdin).get('tool_input', {})
|
||||
value = d.get('command') or d.get('file_path') or d.get('pattern') or str(d)[:120]
|
||||
print(value)
|
||||
" 2>/dev/null || echo "(parse error)")
|
||||
|
||||
# Timestamp in UTC ISO 8601
|
||||
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
# Write the log directory relative to this script's location.
|
||||
# This keeps the log inside the repo regardless of where Claude Code is invoked from.
|
||||
log_dir="$(dirname "$0")"
|
||||
log_file="$log_dir/audit.log"
|
||||
|
||||
# Append one line to the log file
|
||||
echo "$timestamp | $tool_name | $tool_input" >> "$log_file"
|
||||
|
||||
exit 0
|
||||
72
hooks/pre-tool-use.sh
Normal file
72
hooks/pre-tool-use.sh
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
#!/bin/bash
|
||||
# pre-tool-use.sh: Block dangerous shell commands before Claude executes them.
|
||||
#
|
||||
# Claude Code calls this script BEFORE executing any Bash command.
|
||||
# The script receives a JSON object on stdin with two fields:
|
||||
# - tool_name: the name of the tool being called (e.g., "Bash")
|
||||
# - tool_input: the arguments passed to the tool (e.g., {"command": "ls -la"})
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 Allow the command to proceed
|
||||
# 2 Block the command (Claude receives the "reason" field as an error message)
|
||||
#
|
||||
# When blocking, write a JSON object to stdout:
|
||||
# {"decision": "block", "reason": "Human-readable explanation"}
|
||||
#
|
||||
# Make this script executable: chmod +x hooks/pre-tool-use.sh
|
||||
|
||||
# Read the full JSON payload from stdin
|
||||
input=$(cat)
|
||||
|
||||
# Extract tool_name and the command argument from the JSON
|
||||
tool_name=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null)
|
||||
command=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null)
|
||||
|
||||
# Only inspect Bash tool calls. Allow everything else immediately.
|
||||
if [ "$tool_name" != "Bash" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Each blocked pattern is checked separately with a descriptive reason.
|
||||
# Pattern matching uses grep -qiE (case-insensitive extended regex).
|
||||
|
||||
block_if_match() {
|
||||
local pattern="$1"
|
||||
local reason="$2"
|
||||
if echo "$command" | grep -qiE "$pattern"; then
|
||||
echo "{\"decision\": \"block\", \"reason\": \"$reason\"}"
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
# Recursive deletion of the root filesystem
|
||||
block_if_match 'rm -rf /' 'Recursive deletion from root is not allowed'
|
||||
|
||||
# sudo: prevents privilege escalation
|
||||
block_if_match 'sudo ' 'Commands with sudo are not allowed'
|
||||
|
||||
# World-writable permissions: a common misconfiguration that opens security holes
|
||||
block_if_match 'chmod 777' 'chmod 777 is not allowed: use more restrictive permissions'
|
||||
|
||||
# Direct disk write: can wipe an entire disk in seconds
|
||||
block_if_match 'dd if=' 'Direct disk writes with dd are not allowed'
|
||||
|
||||
# Filesystem formatting: permanently destroys data
|
||||
block_if_match 'mkfs' 'Formatting filesystems is not allowed'
|
||||
|
||||
# Fork bomb: crashes the system by spawning processes until resources are exhausted
|
||||
block_if_match ':\(\)\{' 'Fork bombs are not allowed'
|
||||
|
||||
# Piping curl or wget output directly into a shell:
|
||||
# executes arbitrary remote code without review
|
||||
block_if_match 'curl.+\|.+bash' 'Piping curl into bash is not allowed'
|
||||
block_if_match 'curl.+\|.+sh' 'Piping curl output into a shell is not allowed'
|
||||
block_if_match 'wget.+\|.+bash' 'Piping wget into bash is not allowed'
|
||||
block_if_match 'wget.+\|.+sh' 'Piping wget output into a shell is not allowed'
|
||||
|
||||
# Shutdown and reboot: should never be triggered by an automated agent
|
||||
block_if_match '^shutdown' 'Shutdown commands are not allowed'
|
||||
block_if_match '^reboot' 'Reboot commands are not allowed'
|
||||
|
||||
# Nothing matched: allow the command
|
||||
exit 0
|
||||
Loading…
Add table
Add a link
Reference in a new issue