A comprehensive, type-safe Python library for developing Claude Code hooks with automatic hook type detection and specialized contexts for each hook lifecycle.
The cchooks library provides a complete interface for creating Claude Code hooks - user-defined shell commands that execute at various points in Claude Code's lifecycle. The library supports 8 distinct hook types with specialized contexts and output handlers.
- PreToolUse - Runs before tool execution, can approve or block tools
- PostToolUse - Runs after tool execution, can only provide feedback
- Notification - Processes notifications, no decision control
- UserPromptSubmit - Filters and enriches user prompts before processing
- Stop - Controls Claude's stopping behavior
- SubagentStop - Controls subagent stopping behavior
- PreCompact - Runs before transcript compaction
- SessionStart - Runs when Claude Code starts or resumes sessions, can load development context
Factory function that automatically detects the hook type from JSON input and returns the appropriate specialized context.
from cchooks import create_context
# Read from stdin automatically
context = create_context()
# Or use custom stdin
with open('input.json') as f:
context = create_context(stdin=f)Parameters:
stdin(TextIO, optional): Input stream to read JSON from. Defaults tosys.stdin.
Returns:
HookContext: One of the 8 specialized context types based onhook_event_namein input.
Raises:
ParseError: If JSON is invalid or not an objectInvalidHookTypeError: Ifhook_event_nameis not recognizedHookValidationError: If required fields are missing
Abstract base class for all hook contexts. Provides common functionality and properties.
from cchooks.contexts.base import BaseHookContext
class MyCustomContext(BaseHookContext):
@property
def output(self) -> "BaseHookOutput":
return MyCustomOutput()Properties:
session_id: str- Unique session identifiertranscript_path: str- Path to transcript filehook_event_name: str- Type of hook eventoutput: BaseHookOutput- Output handler for this context type
Methods:
from_stdin(stdin: TextIO = sys.stdin) -> BaseHookContext- Create context from stdin JSON
Abstract base class for all hook outputs. Provides common output methods and utilities.
Methods:
_continue_flow(suppress_output: bool = False) -> dict- JSON response to continue processing_stop_flow(stop_reason: str, suppress_output: bool = False) -> dict- JSON response to stop processing_with_specific_output(common_output: CommonOutput, hook_event_name: str, **specific_fields: Any)- Add hook-specific outpout to common JSON structure_success(message: Optional[str] = None) -> NoReturn- Exit with success (code 0)_error(message: str, exit_code: int = 1) -> NoReturn- Exit with non-blocking error (code 1)_block(reason: str) -> NoReturn- Exit with blocking error (code 2)
Runs before tool execution with the ability to approve or block tools.
from cchooks import create_context
from cchooks.contexts import PreToolUseContext
context = create_context()
if isinstance(context, PreToolUseContext):
tool_name = context.tool_name
tool_input = context.tool_input
if tool_name == "Write" and "password" in tool_input.get("file_path", ""):
context.output.deny("Refusing to write to password file")
else:
context.output.allow()Properties:
tool_name: ToolName- Name of the tool being executedtool_input: Dict[str, Any]- Parameters being passed to the tool
Output Methods:
allow(reason: str = "", suppress_output: bool = False)- Allow tool execution, show user the reason for allowingdeny(reason: str, suppress_output: bool = False)- Deny tool execution, prompt Claude with the reason for denyingask(suppress_output: bool = False)- Ask user for permissionhalt(reason: str, suppress_output: bool = False)- Stop all processing immediatelyexit_success(message: Optional[str] = None) -> NoReturn- Exit 0 (success)exit_non_block(message: str) -> NoReturn- Exit 1 (non-blocking error)exit_block(reason: str) -> NoReturn- Exit 2 (blocking error)
Runs after tool execution to provide feedback or block processing.
from cchooks.contexts import PostToolUseContext
if isinstance(context, PostToolUseContext):
tool_name = context.tool_name
tool_input = context.tool_input
tool_response = context.tool_response
if tool_response.get("success") == False:
context.output.simple_block("Tool execution failed")Properties:
tool_name: ToolName- Name of the executed tooltool_input: Dict[str, Any]- Parameters that were passed to the tooltool_response: Dict[str, Any]- Response data from the tool execution
Output Methods:
accept(suppress_output: bool = False)- Accept tool resultschallenge(reason: str, suppress_output: bool = False)- Challenge tool resultsignore(suppress_output: bool = False)- Ignore tool resultshalt(reason: str, suppress_output: bool = False)- Stop all processing immediatelyexit_success(message: Optional[str] = None) -> NoReturn- Exit 0 (success)exit_non_block(message: str) -> NoReturn- Exit 1 (non-blocking error)exit_block(reason: str) -> NoReturn- Exit 2 (blocking error)
Processes notifications without decision control capabilities.
from cchooks.contexts import NotificationContext
if isinstance(context, NotificationContext):
message = context.message
log_notification(message)
context.output.acknowledge("Notification processed")Properties:
message: str- Notification message content
Output Methods:
acknowledge(message: Optional[str]) -> NoReturn- Acknowledge and process informationexit_success(message: Optional[str]) -> NoReturn- Exit 0 (success)exit_non_block(message: str) -> NoReturn- Exit 1 (non-blocking error)exit_block(message: str) -> NoReturn- Exit 2 (blocking error)
exit_blockandexit_non_blockbehavior ofNotification HookandPreCompact Hookis actually the same. All of them showreasonormessageto the user and Claude will keep going. Andexit_successwill showmessagein transcript (default hidden to the user). For details see official docs
Runs when the user submits a prompt, before Claude processes it. This allows you to add additional context based on the prompt/conversation, validate prompts, or block certain types of prompts.
from cchooks.contexts import UserPromptSubmitContext
if isinstance(context, UserPromptSubmitContext):
prompt = context.prompt
# Block prompts with sensitive data
if "password" in prompt.lower():
context.output.block("Security: Prompt contains sensitive data")
else:
# Allow prompt to proceed
context.output.allow()Properties:
prompt: str- The user-submitted prompt text
Output Methods:
allow(suppress_output: bool = False)- Allow the prompt to proceed normallyblock(reason: str, suppress_output: bool = False)- Deny the prompt from being processedadd_context(reason: str, context: str, suppress_output: bool = False)- Add additional context to the prompt.halt(reason: str, suppress_output: bool = False)- Stop all processing immediatelyexit_success(message: Optional[str] = None) -> NoReturn- Exit 0 (success)exit_non_block(message: str) -> NoReturn- Exit 1 (non-blocking error)exit_block(reason: str) -> NoReturn- Exit 2 (blocking error)
Controls Claude's stopping behavior when Claude wants to stop.
from cchooks.contexts import StopContext
if isinstance(context, StopContext):
if context.stop_hook_active:
# Already handled by stop hook
context.output.allow()
else:
# Allow Claude to stop
context.output.allow()Properties:
stop_hook_active: bool- Whether stop hook is already active
Output Methods:
allow(suppress_output: bool = False)- Allow Claude to stopprevent(reason: str, suppress_output: bool = False)- Prevent Claude from stoppinghalt(reason: str, suppress_output: bool = False)- Stop all processing immediatelyexit_success(message: Optional[str] = None) -> NoReturn- Exit 0 (success)exit_non_block(message: str) -> NoReturn- Exit 1 (non-blocking error)exit_block(reason: str) -> NoReturn- Exit 2 (blocking error)
Controls subagent stopping behavior when subagent wants to stop.
from cchooks.contexts import SubagentStopContext
if isinstance(context, SubagentStopContext):
# Similar to StopContext but for subagents
context.output.allow()Properties:
stop_hook_active: bool- Whether stop hook is already active
Output Methods:
Same as StopOutput.
Runs before transcript compaction with custom instructions.
from cchooks.contexts import PreCompactContext
if isinstance(context, PreCompactContext):
trigger = context.trigger # "manual" or "auto"
instructions = context.custom_instructions
if trigger == "manual" and instructions:
process_custom_instructions(instructions)
context.output.acknowledge("Compaction ready")Properties:
trigger: PreCompactTrigger- Type of compaction trigger ("manual"or"auto")custom_instructions: str- Custom instructions provided by user
Output Methods:
acknowledge(message: Optional[str]) -> NoReturn- Acknowledge the compactionexit_success(message: Optional[str]) -> NoReturn- Exit 0 (success)exit_non_block(message: str) -> NoReturn- Exit 1 (non-blocking error)exit_block(message: str) -> NoReturn- Exit 2 (blocking error)
Runs when Claude Code starts a new session or resumes an existing session. Useful for loading development context like existing issues or recent changes to your codebase.
from cchooks.contexts import SessionStartContext
if isinstance(context, SessionStartContext):
source = context.source # "startup", "resume", or "clear"
if source == "startup":
# Load recent changes or project context
recent_changes = get_recent_changes()
context.output.additional_context(recent_changes)
else:
context.output.exit_success("Session ready")Properties:
source: SessionStartSource- Session start source ("startup","resume", or"clear")
Output Methods:
additional_context(context: str, suppress_output: bool = False)- Add context to the session via hookSpecificOutputexit_success(message: Optional[str] = None) -> NoReturn- Exit 0 (success) - message added to session contextexit_non_block(message: str) -> NoReturn- Exit 1 (non-blocking error)exit_block(message: str) -> NoReturn- Exit 2 (blocking error) - behaves same as exit_non_block for SessionStart
Note: Unlike most hooks, SessionStart stdout from exit code 0 is added to the session context rather than shown in transcript mode.
from cchooks.types import HookEventType
# Possible values:
HookEventType = Literal[
"PreToolUse", "PostToolUse", "Notification",
"UserPromptSubmit", "Stop", "SubagentStop", "PreCompact", "SessionStart"
]from cchooks.types import ToolName
# Possible values:
ToolName = Literal[
"Task", "Bash", "Glob", "Grep", "Read",
"Edit", "MultiEdit", "Write", "WebFetch", "WebSearch"
]from cchooks.types import PreToolUseDecision, PostToolUseDecision, StopDecision, UserPromptSubmitDecision
# Possible values:
PreToolUseDecision = Literal["allow", "deny", "ask"]
PostToolUseDecision = Literal["block"]
StopDecision = Literal["block"]
UserPromptSubmitDecision = Literal["block"]from cchooks.types import PreCompactTrigger
# Possible values:
PreCompactTrigger = Literal["manual", "auto"]from cchooks.types import SessionStartSource
# Possible values:
SessionStartSource = Literal["startup", "resume", "clear"]Base exception for all cchooks errors.
Raised when hook input validation fails.
try:
context = create_context()
except HookValidationError as e:
print(f"Validation error: {e}")
sys.exit(1)Raised when JSON parsing fails.
Raised when an invalid hook type is encountered.
Direct control over output and exit behavior when context objects are not available:
Exit with success (exit code 0).
from cchooks import exit_success
exit_success("Operation completed successfully")Exit with error (non-blocking).
from cchooks import exit_non_block
exit_non_block("Configuration error", exit_code=1)Exit with blocking error (exit code 2).
from cchooks import exit_block
exit_block("Security violation detected")Output JSON data to the specified file.
from cchooks import output_json
output_json({"status": "success", "message": "Operation completed"})Safe wrapper around create_context() with built-in error handling. Exits gracefully on any error.
from cchooks import safe_create_context, PreToolUseContext
context = safe_create_context()
assert isinstance(context, PreToolUseContext)
# Safe to proceed - any errors would have been handledUnified handler for all context creation errors.
from cchooks import create_context, handle_context_error
try:
context = create_context()
except Exception as e:
handle_context_error(e) # Graceful exit with appropriate messagehandle_parse_error(error: Exception, file: TextIO = sys.stderr) -> NoReturnhandle_validation_error(error: Exception, file: TextIO = sys.stderr) -> NoReturnhandle_invalid_hook_type(error: Exception, file: TextIO = sys.stderr) -> NoReturn
Read and parse JSON from stdin with validation.
from cchooks.utils import read_json_from_stdin
data = read_json_from_stdin()
print(f"Hook type: {data['hook_event_name']}")Validate that required fields are present in the data.
from cchooks.utils import validate_required_fields
data = {"name": "test", "value": 42}
validate_required_fields(data, ["name", "value"]) # OK
validate_required_fields(data, ["name", "missing"]) # Raises KeyErrorsafe_get_str(data: Dict[str, Any], key: str, default: str = "") -> strsafe_get_bool(data: Dict[str, Any], key: str, default: bool = False) -> boolsafe_get_dict(data: Dict[str, Any], key: str, default: Dict[str, Any] | None = None) -> Dict[str, Any]
#!/usr/bin/env python3
"""Example hook for blocking dangerous file writes."""
import sys
from cchooks import create_context
from cchooks.contexts import PreToolUseContext
def main():
try:
context = create_context()
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if isinstance(context, PreToolUseContext):
# Check for dangerous file operations
if (context.tool_name == "Write" and
"config/" in context.tool_input.get("file_path", "")):
context.output.exit_block("Config files are protected")
else:
context.output.exit_success()
if __name__ == "__main__":
main()#!/usr/bin/env python3
"""Example using JSON output for advanced control."""
from cchooks import create_context
from cchooks.contexts import PreToolUseContext
def main():
context = create_context()
if isinstance(context, PreToolUseContext):
tool_name = context.tool_name
tool_input = context.tool_input
if tool_name == "Bash":
command = tool_input.get("command", "")
if "rm -rf /" in command:
context.output.block("Dangerous command detected")
else:
context.output.approve("Command looks safe")
if __name__ == "__main__":
main()#!/usr/bin/env python3
"""Example post-tool use hook for logging."""
import json
from cchooks import create_context
from cchooks.contexts import PostToolUseContext
def main():
context = create_context()
if isinstance(context, PostToolUseContext):
# Log tool usage
log_entry = {
"tool": context.tool_name,
"input": context.tool_input,
"response": context.tool_response
}
with open("tool_usage.log", "a") as f:
json.dump(log_entry, f)
f.write("\n")
context.output.exit_success("Logged successfully")
if __name__ == "__main__":
main()#!/usr/bin/env python3
"""Example notification handler."""
from cchooks import create_context
from cchooks.contexts import NotificationContext
def main():
context = create_context()
if isinstance(context, NotificationContext):
message = context.message
# Some logic to send Desktop Notification
context.output.acknowledge("Desktop Notification Sent!")
if __name__ == "__main__":
main()- Always handle exceptions - Use try-except blocks around
create_context() - Use type checking - Use
isinstance()to determine context type - Choose appropriate output methods - JSON output for complex decisions, simple exit codes for basic operations
- Provide clear messages - Give meaningful reasons for blocking operations
- Test thoroughly - Test with different hook types and edge cases
- Document your hooks - Include clear documentation about what your hook does
cchooks/
├── __init__.py # Main exports
├── types.py # Type definitions
├── exceptions.py # Exception classes
├── utils.py # Utility functions
└── contexts/ # Hook contexts
├── __init__.py
├── base.py # Base classes
├── pre_tool_use.py
├── post_tool_use.py
├── notification.py
├── user_prompt_submit.py
├── stop.py
├── subagent_stop.py
├── pre_compact.py
└── session_start.py
| Hook Type | Can Block? | Decision Control | Key Properties |
|---|---|---|---|
| PreToolUse | ✅ | approve/block | tool_name, tool_input |
| PostToolUse | ✅ | block only | tool_name, tool_input, tool_response |
| Notification | ❌ | none | message |
| UserPromptSubmit | ✅ | block only | prompt |
| Stop | ✅ | block only | stop_hook_active |
| SubagentStop | ✅ | block only | stop_hook_active |
| PreCompact | ❌ | none | trigger, custom_instructions |
| SessionStart | ❌ | none | source |