Security-hardened hook script implementations — ready-to-paste templates for security-guard, auto-format, inject-context, session-init, on-stop, and lessons-learned-capture
From claude-code-expertnpx claudepluginhub markus41/claude --plugin claude-code-expertThis skill is limited to using the following tools:
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Six production-ready hook scripts with security hardening. Copy to .claude/hooks/ and register in .claude/settings.json.
All scripts follow these rules:
set -euo pipefail at the topjq for JSON (never string concatenation)realpath for path validation (prevents traversal)flock for atomic file writes (prevents interleaved concurrent writes)- (flag injection prevention)printf '%s' for untrusted data, not echoRegistered on: PreToolUse with matcher Bash
Blocks hardcoded dangerous commands before Claude executes them. This is defense-in-depth only — use settings.json deny list as the primary control.
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(head -c 65536)
if ! printf '%s' "$INPUT" | jq -e . >/dev/null 2>&1; then
echo '{"decision": "approve"}'
exit 0
fi
TOOL_INPUT=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""')
# Hardcoded blocklist — do NOT source from external files
BLOCKED_PATTERNS=(
"rm -rf /"
"sudo rm"
"mkfs"
"dd if="
"> /dev/sd"
"chmod -R 777"
"curl.*| sh"
"curl.*| bash"
"wget.*| sh"
"wget.*| bash"
)
for pattern in "${BLOCKED_PATTERNS[@]}"; do
if printf '%s' "$TOOL_INPUT" | grep -qF "$pattern"; then
jq -n --arg p "$pattern" '{"decision":"block","reason":("Blocked dangerous command: "+$p)}'
exit 0
fi
done
echo '{"decision": "approve"}'
Registered on: PostToolUse with matcher Write|Edit
Runs the appropriate formatter immediately after Claude writes a file. Includes path traversal protection.
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(head -c 65536)
FILE=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
# Validate: file must exist, be a regular file, be inside project
if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then
echo '{"decision": "approve"}'
exit 0
fi
REAL=$(realpath "$FILE" 2>/dev/null) || { echo '{"decision": "approve"}'; exit 0; }
WORKDIR=$(realpath "$PWD")
# Reject paths outside project root (path traversal guard)
if [[ "$REAL" != "$WORKDIR"/* ]]; then
echo '{"decision": "approve"}'
exit 0
fi
# Reject filenames starting with dash (flag injection guard)
BASENAME=$(basename "$REAL")
if [[ "$BASENAME" == -* ]]; then
echo '{"decision": "approve"}'
exit 0
fi
# Format based on extension
case "$REAL" in
*.ts|*.tsx|*.js|*.jsx|*.json|*.css|*.scss|*.md)
npx prettier --write "$REAL" 2>/dev/null || true ;;
*.py)
black "$REAL" 2>/dev/null || ruff format "$REAL" 2>/dev/null || true ;;
*.rs)
rustfmt "$REAL" 2>/dev/null || true ;;
*.go)
gofmt -w "$REAL" 2>/dev/null || true ;;
*.sh)
shfmt -w "$REAL" 2>/dev/null || true ;;
esac
echo '{"decision": "approve"}'
Registered on: UserPromptSubmit
Injects dynamic context (date, branch, uncommitted file count) on every prompt. Claude receives this as additional context before processing.
#!/usr/bin/env bash
set -euo pipefail
# Inject dynamic context — stdout is added to Claude's context
DATE=$(date '+%Y-%m-%d %H:%M')
BRANCH=$(git branch --show-current 2>/dev/null || echo "no-git")
UNCOMMITTED=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
LAST_COMMIT=$(git log --oneline -1 2>/dev/null || echo "no commits")
echo "[Session Context] Date: $DATE | Branch: $BRANCH | Uncommitted: $UNCOMMITTED files | Last commit: $LAST_COMMIT"
echo '{"decision": "approve"}'
How it works: On UserPromptSubmit, stdout is prepended to the user's message as context. Use this for date injection, active branch, workspace state — anything Claude should know before processing each turn.
Registered on: SessionStart
Fires when a session begins or resumes. Outputs status information to stderr (shown as system messages) and checks for stale memory files.
#!/usr/bin/env bash
set -euo pipefail
echo "Session started: $(date '+%Y-%m-%d %H:%M')" >&2
echo "Branch: $(git branch --show-current 2>/dev/null || echo 'no-git')" >&2
echo "Last commit: $(git log --oneline -1 2>/dev/null || echo 'no commits')" >&2
# Warn about stale memory rotation
LESSONS=".claude/rules/lessons-learned.md"
if [ -f "$LESSONS" ]; then
LINES=$(wc -l < "$LESSONS")
if [ "$LINES" -gt 200 ]; then
echo "WARNING: lessons-learned.md has $LINES lines — run /cc-memory --rotate to prune resolved entries" >&2
fi
fi
echo '{"decision": "approve"}'
Registered on: Stop
Fires when Claude finishes a response turn. Use for reminders, notifications, or light cleanup.
#!/usr/bin/env bash
set -euo pipefail
# Remind about uncommitted work at end of each turn
UNCOMMITTED=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
if [ "$UNCOMMITTED" -gt 0 ]; then
echo "Reminder: $UNCOMMITTED uncommitted files" >&2
fi
echo '{"decision": "approve"}'
Registered on: PostToolUseFailure
The most important hook. Auto-captures every tool failure to .claude/rules/lessons-learned.md for the self-healing loop. Uses flock for atomic writes and sanitizes inputs to prevent injection.
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(head -c 65536)
if ! printf '%s' "$INPUT" | jq -e . >/dev/null 2>&1; then
echo '{"decision": "approve"}'
exit 0
fi
TOOL=$(printf '%s' "$INPUT" | jq -r '.tool_name // ""')
ERROR=$(printf '%s' "$INPUT" | jq -r '.error // ""')
if [ -z "$ERROR" ] || [ "$ERROR" = "null" ]; then
echo '{"decision": "approve"}'
exit 0
fi
# Sanitize: strip shell metacharacters to prevent injection
SAFE_TOOL=$(printf '%s' "$TOOL" | head -c 50 | tr -d '`$()\\!"'"'"'')
SAFE_ERROR=$(printf '%s' "$ERROR" | head -c 200 | tr -d '`$()\\!"'"'"'')
TIMESTAMP=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
LESSONS=".claude/rules/lessons-learned.md"
# flock ensures atomic append — prevents interleaved writes if hooks run concurrently
(
flock -x 200
printf '\n### Error: %s failure (%s)\n- **Tool:** %s\n- **Error:** %s\n- **Status:** NEEDS_FIX - Claude should document the fix here after resolving\n' \
"$SAFE_TOOL" "$TIMESTAMP" "$SAFE_TOOL" "$SAFE_ERROR" \
>> "$LESSONS"
) 200>/tmp/lessons-learned.lock
echo '{"decision": "approve"}'
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/security-guard.sh" }]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/auto-format.sh" }]
}
],
"PostToolUseFailure": [
{
"matcher": "*",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/lessons-learned-capture.sh" }]
}
],
"Stop": [
{
"matcher": "",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/on-stop.sh" }]
}
],
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/inject-context.sh" }]
}
],
"SessionStart": [
{
"matcher": "",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/session-init.sh" }]
}
]
}
}
# Create hooks directory
mkdir -p .claude/hooks
# Make all hooks executable after writing
chmod +x .claude/hooks/*.sh
# Verify hook syntax before registering
bash -n .claude/hooks/security-guard.sh && echo "OK"
bash -n .claude/hooks/lessons-learned-capture.sh && echo "OK"
| Detected Stack | Hook | Event | Matcher | Action |
|---|---|---|---|---|
| TypeScript | auto-typecheck.sh | PostToolUse | Write|Edit | tsc --noEmit |
| ESLint | auto-lint.sh | PostToolUse | Write|Edit | eslint --fix |
| Docker | no-latest-tag.sh | PreToolUse | Bash | Block :latest tags |
| Git | no-env-commit.sh | PreToolUse | Bash | Block .env commits |
| Python (Black) | auto-format-py.sh | PostToolUse | Write|Edit | black |
| Rust | auto-clippy.sh | PostToolUse | Write|Edit | cargo clippy |
See skills/lsp-integration/SKILL.md for TypeScript, Python, and Rust diagnostics hook implementations.