by GowayLee
A Python SDK for claude-code hooks
# Add to your Claude Code skills
git clone https://github.com/GowayLee/cchooks
A lightweight Python Toolkit that makes building Claude Code hooks as simple as writing a few lines of code. Stop worrying about JSON parsing and focus on what your hook should actually do.
New to Claude Code hooks? Check the official docs for the big picture.
Need the full API? See the API Reference for complete documentation.
create_context() handles all the boilerplateNo comments yet. Be the first to share your thoughts!
pip install cchooks
# or
uv add cchooks
Build a PreToolUse hook that blocks dangerous file writes:
#!/usr/bin/env python3
from cchooks import create_context, PreToolUseContext
c = create_context()
# Determine hook type
assert isinstance(c, PreToolUseContext)
# Block writes to .env files
if c.tool_name == "Write" and ".env" in c.tool_input.get("file_path", ""):
c.output.exit_deny("Nope! .env files are protected")
else:
c.output.exit_success()
Save as hooks/env-guard.py, make executable:
chmod +x hooks/env-guard.py
That's it. No JSON parsing, no validation headaches.
Build each hook type with real examples:
Block dangerous commands before they run:
#!/usr/bin/env python3
from cchooks import create_context, PreToolUseContext
c = create_context()
assert isinstance(c, PreToolUseContext)
# Block rm -rf commands with user warning
if c.tool_name == "Bash" and "rm -rf" in c.tool_input.get("command", ""):
c.output.deny(
reason="You should not execute this command: System protection: rm -rf blocked",
system_message="⚠️ This command could permanently delete files. Please use caution."
)
else:
c.output.allow()
Format Python files after writing:
#!/usr/bin/env python3
import subprocess
from cchooks import create_context, PostToolUseContext
c = create_context()
assert isinstance(c, PostToolUseContext)
if c.tool_name == "Write" and c.tool_input.get("file_path", "").endswith(".py"):
file_path = c.tool_input["file_path"]
subprocess.run(["black", file_path])
print(f"Auto-formatted: {file_path}")
Send desktop notifications:
#!/usr/bin/env python3
import os
from cchooks import create_context, NotificationContext
c = create_context()
assert isinstance(c, NotificationContext)
if "permission" in c.message.lower():
os.system(f'notify-send "Claude" "{c.message}"')
Keep Claude working on long tasks:
#!/usr/bin/env python3
from cchooks import create_context, StopContext
c = create_context()
assert isinstance(c, StopContext)
if not c.stop_hook_active: # Claude has not been activated by other Stop Hook
c.output.prevent(
reason="Hey Claude, you should try to do more works!",
system_message="Claude is working on important tasks. You can stop manually if needed."
) # Prevent from stopping, and prompt Claude
else:
c.output.allow() # Allow stop
Since hooks are executed in parallel in Claude Code, it is necessary to check
stop_hook_activeto determine if Claude has already been activated by another parallel Stop Hook.
Same as Stop, but for subagents:
from cchooks import create_context, SubagentStopContext
c = create_context()
assert isinstance(c, SubagentStopContext)
c.output.allow() # Let subagents complete
Filter and enrich user prompts before processing:
from cchooks import create_context, UserPromptSubmitContext
c = create_context()
assert isinstance(c, UserPromptSubmitContext)
# Block prompts with sensitive data with user warning
if "password" in c.prompt.lower():
c.output.block(
reason="Security: Prompt contains sensitive data",
system_message="🔒 For security reasons, please avoid sharing passwords or sensitive information."
)
else:
c.output.allow()
Load development context when Claude Code starts or resumes:
#!/usr/bin/env python3
import os
from cchooks import create_context, SessionStartContext
c = create_context()
assert isinstance(c, SessionStartContext)
if c.source == "startup":
# Load project-specific context
project_root = os.getcwd()
if os.path.exists(f"{project_root}/.claude-context"):
with open(f"{project_root}/.claude-context", "r") as f:
context = f.read()
print(f"Loaded project context:\n{context}")
elif c.source == "resume":
print("Resuming previous session...")
elif c.source == "clear":
print("Starting fresh session...")
# Always exit with success - output is added to session context
c.output.exit_success()
Note: SessionStart hooks cannot block Claude processing. Any stdout output from exit code 0 is automatically added to the session context (not the transcript).
Perform cleanup tasks when Claude Code session ends:
#!/usr/bin/env python3
import os
import json
from datetime import datetime
from cchooks import create_context, SessionEndContext
c = create_context()
assert isinstance(c, SessionEndContext)
# Log session end information
session_info = {
"session_id": c.session_id,
"end_time": datetime.now().isoformat(),
"reason": c.reason,
"transcript_path": c.transcript_path
}
# Save session summary
log_file = f"/tmp/claude-sessions.log"
with open(log_file, "a") as f:
f.write(json.dumps(session_info) + "\n")
# Perform cleanup based on session end reason
if c.reason == "clear":
# Clean up temporary files
temp_dir = f"/tmp/claude-temp-{c.session_id}"
if os.path.exists(temp_dir):
os.system(f"rm -rf {temp_dir}")
print(f"Cleaned up temporary directory: {temp_dir}")
elif c.reason == "logout":
# Save user preferences or session state
print(f"User logged out - session {c.session_id} ended")
elif c.reason == "prompt_input_exit":
# Handle manual exit
print(f"Manual exit - session {c.session_id} terminated")
else:
# Other reasons
print(f"Session {c.session_id} ended: {c.reason}")
# Always exit with success - cleanup completed
c.output.exit_success("Session cleanup completed")
Note: SessionEnd hooks cannot block session termination since the session is already ending. They are ideal for cleanup tasks, logging, and saving state. Success output is logged to debug only, while errors are shown to users via stderr.
Add custom compaction rules:
from cchooks import create_context, PreCompactContext
c = create_context()
assert isinstance(c, PreCompactContext)
if c.custom_instructions:
print(f"Using custom compaction: {c.custom_instructions}")
When you need direct control over output and exit behavior outside of context objects, use these standalone utilities:
#!/usr/bin/env python3
from cchooks import exit_success, exit_block, exit_non_block, output_json
# Direct exit control
exit_success("Operation completed successfully")
exit_block("Security violation detected")
exit_non_block("Warning: something unexpected happened")
# JSON output
output_json({"status": "error", "reason": "invalid input"})
exit_success(message=None) - Exit with code 0 (success)exit_non_block(message, exit_code=1) - Exit with error code (non-blocking)exit_block(reason) - Exit with code 2 (blocking error)output_json(data) - Output JSON data to stdoutsafe_create_context() - Safe wrapper with built-in error handlinghandle_context_error(error) - Unified error handler for context creationHandle context creation errors gracefully with built-in utilities:
#!/usr/bin/env python3
from cchooks import safe_create_context, PreToolUseContext
# Automatic error handling - exits gracefully on any error
context = safe_create_context()
# If we reach here, context creation succeeded
assert isinstance(context, PreToolUseContext)
# Your normal hook logic here...
Or use explicit error handling:
#!/usr/bin/env python3
from cchooks import create_context, handle_context_error, PreToolUseContext
try:
context = create_context()
except Exception as e:
handle_context_error(e) # Graceful exit with appropriate message
# Normal processing...
| Hook Type | What You Get | Key Properties | | -----------------