Agent Skills: Hook Writer

Expert assistant for authoring hook scripts. Creates Python hooks with correct JSON output schema, validates against PreToolUse/PostToolUse/UserPromptSubmit formats, and applies fail-open principles. Triggers on keywords: writing hooks, creating hooks, hook authoring, pretooluse hook, posttooluse hook, new hook, hook template, hook validation, hook schema

UncategorizedID: MatiasComercio/agentic-config/ac-meta-hook-writer

Install this agent skill to your local

pnpm dlx add-skill https://github.com/WaterplanAI/agentic-config/tree/HEAD/packages/pi-ac-meta/skills/ac-meta-hook-writer

Skill Files

Browse the full folder contents for ac-meta-hook-writer.

Download Skill

Loading file tree…

packages/pi-ac-meta/skills/ac-meta-hook-writer/SKILL.md

Skill Metadata

Name
ac-meta-hook-writer
Description
"Expert assistant for authoring hook scripts. Creates Python hooks with correct JSON output schema, validates against PreToolUse/PostToolUse/UserPromptSubmit formats, and applies fail-open principles. Triggers on keywords: writing hooks, creating hooks, hook authoring, pretooluse hook, posttooluse hook, new hook, hook template, hook validation, hook schema"

Hook Writer

Creates Python hooks with correct JSON output schemas and fail-open patterns.

Compatibility Note

This pi wrapper preserves the original hook authoring target. The wrapper only adapts invocation to pi; it does not retarget the generated hook format.

Hook Types

| 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 |

JSON Output Schema

PreToolUse/PostToolUse (Permission Decisions)

{
  "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}

UserPromptSubmit

{
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "modifiedPrompt": "Transformed user prompt"
  }
}

Stop/SubagentStop

{
  "hookSpecificOutput": {
    "hookEventName": "Stop"
  }
}

Python TypedDict Templates

PreToolUse/PostToolUse

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

UserPromptSubmit

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

Hook Input Schema

Hooks receive JSON via stdin:

class ToolInput(TypedDict, total=False):
    """Tool parameters from the hook host."""
    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

Complete Hook Template

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
"""
Pretooluse hook for the target host 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 the hook host."""
    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 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()

settings.json Configuration

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": "uv run --no-project --script /path/to/hook.py"
      }
    ]
  }
}

Matcher Options

| Matcher | Matches | |---------|---------| | "Bash" | Bash tool only | | "Write" | Write tool only | | "*" | All tools | | ["Bash", "Write"] | Multiple specific tools |

Fail-Open Principle

Hooks MUST fail-open to prevent blocking legitimate operations due to hook errors:

  1. Wrap main logic in try/except
  2. Exception handler outputs "permissionDecision": "allow"
  3. Log error to stderr (optional)
  4. Exit with 0 (not error code)
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

Reference Implementations

| 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.

Workflow

  1. Identify the tool(s) to monitor
  2. Define blocking conditions (patterns, state checks)
  3. Create hook using template above
  4. Test manually: echo '{"tool_name":"Bash","tool_input":{"command":"test"}}' | python hook.py
  5. Configure in settings.json
  6. Verify hook triggers on actual tool use