plugins/claude-code-expert/archive/v7.6.0/skills/hook-script-library/SKILL.md
Security-hardened hook script implementations — ready-to-paste templates for security-guard, auto-format, inject-context, session-init, on-stop, and lessons-learned-capture
npx skillsauth add markus41/claude plugins/claude-code-expert/archive/v7.6.0/skills/hook-script-libraryInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
development
Enhanced plan-authoring skill with Pre-Writing context gathering, task metadata, non-TDD templates, Red Flags, telemetry, and an automated plan linter. Use when you have a spec or requirements for a multi-step task, before touching code.
tools
Documentation intelligence engine with graph-based API docs, algorithm library, and drift detection
tools
Ultraplan cloud planning — kick off a plan in the cloud from your terminal, review and revise in the browser, then execute remotely or send back to CLI
tools
--- name: mcp description: Configure MCP servers for Claude Code — stdio vs HTTP, authentication, Tools/Resources/Prompts distinction, channels (CI webhook, mobile relay, Discord bridge, fakechat), and cost of always-loaded tools. Use this skill whenever adding an MCP server, debugging connection issues, choosing between MCP Tools vs Prompts vs Resources, installing channel servers, or managing .mcp.json. Triggers on: "MCP server", "mcp config", "add Obsidian MCP", "install context7", "channels"