Install
$ agentstack add skill-costa-marcello-skillkit-cc-hooks ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Claude Code Hooks -- Reference
This skill provides the definitive reference for creating and improving Claude Code hooks -- event-driven scripts that respond to lifecycle events.
When to use:
- Building or improving event-driven automation for Claude Code
- Creating or refactoring PreToolUse guards to block dangerous commands
- Debugging hooks that fail silently or produce unexpected results
- Implementing PostToolUse formatters, linters, or auditors
- Adding Stop hooks for testing or notifications
- Setting up SessionStart/SessionEnd for environment management
- Integrating Claude Code with CI/CD pipelines (headless mode)
Quick Reference
| Event | Decision Control | Hook Types | Use Case | |-------|-----------------|------------|----------| | SessionStart | No | command | Initialise environment, set CLAUDE_ENV_FILE | | UserPromptSubmit | No | command, prompt | Preprocess/validate input | | PreToolUse | allow/deny/ask + updatedInput | command, prompt | Validate, block dangerous commands | | PermissionRequest | allow/deny/ask | command, prompt | Auto-allow/deny permissions | | PostToolUse | block | command, prompt | Format, audit, notify | | PostToolUseFailure | block | command, prompt | Capture failures, add guidance | | Notification | No | command | Alert integrations | | SubagentStart | No | command | Inspect subagent metadata | | SubagentStop | block | command, prompt, agent | Verify subagent completion | | Stop | block | command, prompt, agent | Run tests, summarise | | TeammateIdle | exit code 2 only | command | Enforce quality gates before idle | | TaskCompleted | exit code 2 only | command | Block task completion if criteria not met | | PreCompact | No | command | Preserve critical context (matchers: manual, auto) | | SessionEnd | No | command | Cleanup, save state (matchers: clear, logout, prompt_input_exit) |
Hook Structure
.claude/hooks/
├── pre-tool-validate.sh
├── post-tool-format.sh
├── post-tool-audit.sh
├── stop-run-tests.sh
└── session-start-init.sh
Configuration
settings.json
{ "hooks": {
"PostToolUse": [{ "matcher": "Edit|Write", "hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/post-tool-format.sh" }
]}],
"PreToolUse": [{ "matcher": "Bash", "hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/pre-tool-validate.sh" }
]}],
"Stop": [{ "hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/stop-run-tests.sh" }
]}]
}}
Execution Model
Hooks receive a JSON payload via stdin (treat it as untrusted input) and run with your user permissions (outside the Bash tool sandbox). Default timeout is 60s per hook command. All matching hooks run in parallel. Identical commands are deduplicated.
Hook Input (stdin)
{ "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": { "command": "ls -la" } }
Environment Variables (shell)
| Variable | Description | |----------|-------------| | CLAUDE_PROJECT_DIR | Absolute project root where Claude Code started | | CLAUDE_PLUGIN_ROOT | Plugin root (plugin hooks only) | | CLAUDE_CODE_REMOTE | "true" in remote/web environments; empty/local otherwise | | CLAUDE_ENV_FILE | File path to persist export ... lines (available in SessionStart; check docs for Setup support) |
Exit Codes
| Code | Meaning | Notes | |------|---------|------| | 0 | Success | JSON written to stdout is parsed for structured control | | 2 | Blocking error | stderr becomes the message; JSON in stdout is ignored | | Other | Non-blocking error | Execution continues; stderr is visible in verbose mode |
Stdout injection note: for UserPromptSubmit, SessionStart, and Setup, non-JSON stdout (exit 0) is injected into Claude's context. Most other events show stdout only in verbose mode.
Decision Control + Input Modification
PreToolUse hooks can allow/deny/ask and optionally modify the tool input via updatedInput.
Hook Output Schema
{ "hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Reason shown to user (and to Claude on deny)",
"updatedInput": { "command": "echo 'modified'" },
"additionalContext": "Extra context added before tool runs"
}}
Older decision/reason fields are deprecated. Use the hookSpecificOutput.* fields.
Redirect Sensitive File Edits
Input: PreToolUse event for an Edit targeting package-lock.json. Output: Redirect the edit to /dev/null with an allow decision.
#!/bin/bash
set -euo pipefail
INPUT="$(cat)"
FILE_PATH="$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')"
# Redirect package-lock.json edits to /dev/null
if [[ "$FILE_PATH" == *"package-lock.json" ]]; then
UPDATED_INPUT="$(echo "$INPUT" | jq -c '.tool_input | .file_path = "/dev/null"')"
jq -cn --argjson updatedInput "$UPDATED_INPUT" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "allow",
permissionDecisionReason: "Redirected write to /dev/null",
updatedInput: $updatedInput
}
}'
exit 0
fi
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
Strip Sensitive Files from Git Add
Input: PreToolUse event for a Bash command starting with git add that includes .env files. Output: Modified command with .env references removed.
#!/bin/bash
set -euo pipefail
INPUT="$(cat)"
TOOL_NAME="$(echo "$INPUT" | jq -r '.tool_name')"
CMD="$(echo "$INPUT" | jq -r '.tool_input.command // empty')"
if [[ "$TOOL_NAME" == "Bash" && "$CMD" =~ ^git[[:space:]]+add ]]; then
# Remove .env files from staging
SAFE_CMD="$(echo "$CMD" | sed 's/\.env[^ ]*//g')"
if [[ "$SAFE_CMD" != "$CMD" ]]; then
echo '{}' | jq -cn --arg cmd "$SAFE_CMD" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "allow",
permissionDecisionReason: "Removed .env from git add",
updatedInput: { command: $cmd }
}
}'
exit 0
fi
fi
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
Prompt-Based Stop Hook
Input: Stop event with task context. Output: LLM evaluates whether all tasks are complete.
{ "hooks": { "Stop": [{ "hooks": [{ "type": "prompt", "prompt": "Evaluate whether Claude should stop. Context JSON: $ARGUMENTS. Return {\"ok\": true} if all tasks are complete, otherwise {\"ok\": false, \"reason\": \"what remains\"}.", "timeout": 30 }] }] }}
Response schema:
- Allow:
{"ok": true} - Block:
{"ok": false, "reason": "Explanation shown to Claude"}
PostToolUse Audit Logger
Input: PostToolUse event after any file write. Output: Append a JSON line to an audit log (non-blocking).
#!/bin/bash
set -euo pipefail
INPUT="$(cat)"
TOOL_NAME="$(echo "$INPUT" | jq -r '.tool_name')"
FILE_PATH="$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')"
TIMESTAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
if [[ -n "$FILE_PATH" ]]; then
echo "{\"timestamp\":\"$TIMESTAMP\",\"tool\":\"$TOOL_NAME\",\"file\":\"$FILE_PATH\"}" \
>> "$CLAUDE_PROJECT_DIR/.claude/edit-audit.jsonl"
fi
exit 0
Settings matcher: "matcher": "Edit|Write" with "async": true.
SessionStart Environment Setup
Input: SessionStart event at session launch. Output: Write environment variables to CLAUDE_ENV_FILE so they persist across Bash calls.
#!/bin/bash
set -euo pipefail
cd "$CLAUDE_PROJECT_DIR"
# Persist environment variables for the session
if [[ -n "${CLAUDE_ENV_FILE:-}" ]]; then
echo "export PROJECT_NAME=$(basename "$CLAUDE_PROJECT_DIR")" >> "$CLAUDE_ENV_FILE"
echo "export NODE_ENV=development" >> "$CLAUDE_ENV_FILE"
fi
# Inject context into Claude's conversation
echo "=== Session Context ==="
git branch --show-current 2>/dev/null || echo "Not a git repo"
echo "Node: $(node --version 2>/dev/null || echo 'not installed')"
exit 0
Stdout is injected into Claude's context. CLAUDE_ENV_FILE exports persist across all Bash commands in the session.
Prompt-Based Hooks
For complex decisions, use LLM-evaluated hooks (type: "prompt") instead of bash scripts. They are most useful for Stop and SubagentStop decisions.
Default to command hooks for fast, deterministic checks. Use prompt hooks only when the decision requires natural language reasoning:
{ "Stop": [{ "hooks": [
{ "type": "command", "command": ".claude/hooks/quick-check.sh" },
{ "type": "prompt", "prompt": "Verify code quality meets standards" }
]}] }
Matchers
Matchers filter which tool triggers the hook:
- Exact match:
Writematches only the Write tool - Regex:
Edit|WriteorNotebook.* - MCP tools:
mcp__github__.*ormcp____(regex patterns) - Match all:
*(also works with""or omitted matcher) - Event-specific matchers:
PreCompactsupportsmanual/auto;SessionEndsupportsclear/logout/prompt_input_exit
Security Best Practices
All hook templates already demonstrate set -euo pipefail, quoted variables, and absolute paths. Beyond those defaults, check for:
- No
evalwith untrusted input (stdin JSON is attacker-controlled) - Target
References
- [references/hook-templates.md](references/hook-templates.md) -- 15 ready-to-use templates: pre-tool validation, post-tool formatting, security audit, stop hooks, session start/end, context re-injection, async test runner, session state persistence, MCP audit logging, infinite loop guard, desktop notifications, bash logging, protected files, sprint context, session archiving
- [references/input-output-schemas.md](references/input-output-schemas.md) -- Per-event JSON schemas (stdin input and stdout output) for all 14 hook events, plus tool-specific
tool_inputfields for Bash, Write, Edit, Read, Grep, Glob, and MCP tools - [references/command-vs-prompt.md](references/command-vs-prompt.md) -- Decision tree for choosing between command, prompt, and agent hook types. Performance comparison, prompt authoring guidance, and combining strategies
- [references/tool-names.md](references/tool-names.md) -- Complete tool name reference (19+ built-in tools, MCP naming convention), advanced matcher patterns (anchored, negative lookahead, case-insensitive), and common matcher mistakes
- [references/troubleshooting.md](references/troubleshooting.md) -- Step-by-step diagnostics for hooks not triggering, command failures, prompt issues, infinite loops, output visibility, timeouts, environment variables, and common pitfalls
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: costa-marcello
- Source: costa-marcello/skillkit
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.