From ac-meta
Authors Python hooks for Claude Code with JSON output schemas, TypedDict templates for PreToolUse, PostToolUse, UserPromptSubmit events. Validates formats and applies fail-open principles.
npx claudepluginhub waterplanai/agentic-config --plugin ac-metaThis skill is limited to using the following tools:
Creates Python hooks for Claude Code with correct JSON output schemas and fail-open patterns.
Guides authoring Claude Code hooks: event selection, types (command, prompt, agent), matcher patterns, blocking vs advisory. Use for quality gates, automation, policy enforcement.
Guides development of event-driven hooks for Claude Code plugins using prompt-based and command-based configurations in hooks.json for events like PreToolUse, PostToolUse, Stop, and SessionStart to validate tools and automate workflows.
Guides creation of Claude Code plugin hooks with prompt-based and bash command types for PreToolUse, PostToolUse, Stop, and other events. Covers plugin hooks.json and settings.json formats.
Share bugs, ideas, or general feedback.
Creates Python hooks for Claude Code with correct JSON output schemas and fail-open patterns.
| Event | Trigger | Use Case |
|---|---|---|
| PreToolUse | Before tool execution | Block dangerous commands, validate inputs |
| PostToolUse | After tool execution | Log results, trigger follow-up actions |
| UserPromptSubmit | Before prompt processing | Validate/transform user input |
| Stop | Session ends | Cleanup, summary generation |
| SubagentStop | Subagent completes | Aggregate results, status reporting |
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Optional explanation"
}
}
| Field | Values | Required |
|---|---|---|
hookEventName | "PreToolUse" or "PostToolUse" | Yes |
permissionDecision | "allow", "deny", "ask" | Yes |
permissionDecisionReason | String explanation | No (recommended for deny) |
DEPRECATED FORMAT (DO NOT USE):
{"decision": "allow", "message": null}
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"modifiedPrompt": "Transformed user prompt"
}
}
{
"hookSpecificOutput": {
"hookEventName": "Stop"
}
}
from typing import TypedDict
class HookSpecificOutput(TypedDict, total=False):
"""Inner hook output structure."""
hookEventName: str
permissionDecision: str # "allow" | "deny" | "ask"
permissionDecisionReason: str
class HookOutput(TypedDict):
"""JSON output returned via stdout."""
hookSpecificOutput: HookSpecificOutput
from typing import TypedDict
class HookSpecificOutput(TypedDict, total=False):
"""Inner hook output structure."""
hookEventName: str
modifiedPrompt: str
class HookOutput(TypedDict):
"""JSON output returned via stdout."""
hookSpecificOutput: HookSpecificOutput
Hooks receive JSON via stdin:
class ToolInput(TypedDict, total=False):
"""Tool parameters from Claude Code."""
command: str # Bash commands
file_path: str # Write/Edit/Read targets
content: str # Write content
old_string: str # Edit source
new_string: str # Edit replacement
class HookInput(TypedDict):
"""JSON input received via stdin."""
tool_name: str # "Bash", "Write", "Edit", "Read", etc.
tool_input: ToolInput
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
"""
Pretooluse hook for Claude Code that [PURPOSE].
[DETAILED DESCRIPTION]
Fail-open principle: allow operations if hook encounters errors.
"""
import json
import sys
from typing import TypedDict
class ToolInput(TypedDict, total=False):
"""Tool parameters from Claude Code."""
command: str
class HookInput(TypedDict):
"""JSON input received via stdin."""
tool_name: str
tool_input: ToolInput
class HookSpecificOutput(TypedDict, total=False):
"""Inner hook output structure."""
hookEventName: str
permissionDecision: str # "allow" | "deny"
permissionDecisionReason: str
class HookOutput(TypedDict):
"""JSON output returned via stdout."""
hookSpecificOutput: HookSpecificOutput
def should_block_tool(tool_name: str, tool_input: ToolInput) -> tuple[bool, str | None]:
"""
Determine if tool should be blocked.
Returns:
(should_block, message): Tuple of block decision and optional message
"""
# Only inspect specific tools
if tool_name != "Bash":
return False, None
command = tool_input.get("command", "")
# Add detection logic here
if "dangerous_pattern" in command:
return True, "Blocked: explanation of why this is blocked."
return False, None
def main() -> None:
"""Main hook execution."""
try:
# Read input from stdin
input_data: HookInput = json.load(sys.stdin)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
# Determine if should block
should_block, message = should_block_tool(tool_name, tool_input)
# Return decision in Claude Code hook format
hook_output: HookSpecificOutput = {
"hookEventName": "PreToolUse",
"permissionDecision": "deny" if should_block else "allow",
}
if message:
hook_output["permissionDecisionReason"] = message
output: HookOutput = {"hookSpecificOutput": hook_output}
print(json.dumps(output))
except Exception as e:
# Fail-open: if hook crashes, allow the operation
output: HookOutput = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
}
}
print(json.dumps(output))
print(f"Hook error: {e}", file=sys.stderr)
sys.exit(0)
if __name__ == "__main__":
main()
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "uv run --no-project --script /path/to/hook.py"
}
]
}
}
| Matcher | Matches |
|---|---|
"Bash" | Bash tool only |
"Write" | Write tool only |
"*" | All tools |
["Bash", "Write"] | Multiple specific tools |
Hooks MUST fail-open to prevent blocking legitimate operations due to hook errors:
"permissionDecision": "allow"except Exception as e:
# Fail-open: if hook crashes, allow the operation
output: HookOutput = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
}
}
print(json.dumps(output))
print(f"Hook error: {e}", file=sys.stderr)
sys.exit(0) # Exit cleanly even on error
| Hook | Purpose | Bundled In |
|---|---|---|
| dry-run-guard.py | Block file-writing in dry-run mode | dry-run skill |
| git-commit-guard.py | Block --no-verify flag | project .claude/hooks/ |
| gsuite-public-asset-guard.py | Block public asset creation | gsuite skill |
| mux-orchestrator-guard.py | Block forbidden tools in MUX orchestrator | mux skill |
| mux-subagent-guard.py | Block TaskOutput for MUX subagents | mux-subagent skill |
Hooks are bundled within their respective skill directories under hooks/ subdirectory.
For standalone hooks not tied to a skill, see the project's .claude/hooks/ directory.
echo '{"tool_name":"Bash","tool_input":{"command":"test"}}' | python hook.py