Claude-starter Codex-hook-builder
Interactive hook creator for Codex. Triggers when user mentions creating hooks, PreToolUse, PostToolUse, hook validation, hook configuration, settings.json hooks, or wants to automate tool execution workflows.
git clone https://github.com/raintree-technology/claude-starter
.agents/skills/anthropic/claude-hook-builder/skill.mdCodex Hook Builder
Purpose
Guide users through creating effective Codex hooks for tool validation, automation, and workflow enhancement. Auto-invokes when users want to create or configure hooks.
When to Use
Auto-invoke when users mention:
- Creating hooks - "create hook", "make hook", "new hook", "add hook"
- Hook events - "PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop", "SessionStart"
- Validation - "validate", "check", "prevent", "block", "approve"
- Automation - "auto-format", "auto-lint", "automatic", "trigger"
- Hook configuration - "settings.json hooks", "hook matcher", "hook command"
Knowledge Base
- Official docs:
.Codex/skills/ai/Codex/docs/code_claude_com/docs_en_hooks.md - Hook guide:
.Codex/skills/ai/Codex/docs/code_claude_com/docs_en_hooks-guide.md - Project guide:
.Codex/docs/creating-components.md
Process
1. Gather Requirements
Ask the user:
Let me help you create a Codex hook! I need some details: 1. **What should this hook do?** Examples: - Auto-format code after editing files - Validate bash commands before execution - Add context when user submits prompts - Prevent access to sensitive files - Run tests after file changes 2. **When should it trigger?** - PreToolUse (before tool execution) - PostToolUse (after tool execution) - UserPromptSubmit (when user sends message) - Stop (when Codex finishes responding) - SubagentStop (when subagent finishes) - SessionStart (when session begins) - SessionEnd (when session ends) - Notification (when notification sent) - PermissionRequest (when permission requested) 3. **Which tools should it match?** - Specific tool (Write, Edit, Bash, Read, etc.) - Multiple tools (Write|Edit) - All tools (*) - MCP tools (mcp__server__tool) 4. **What should it return?** - Simple exit code (0 = success, 2 = block) - JSON with decision control - Additional context for Codex - Modified tool inputs 5. **Scope:** - User-level (`~/.Codex/settings.json`) - Project-level (`.Codex/settings.json`) - Local project (`.Codex/settings.local.json`)
2. Determine Hook Type
Bash Command Hook:
{ "type": "command", "command": "/path/to/script.sh" }
- Runs a shell command
- Fast, deterministic
- Good for validation, formatting
Prompt-based Hook:
{ "type": "prompt", "prompt": "Evaluate if Codex should stop: $ARGUMENTS" }
- Uses LLM for decision
- Context-aware, intelligent
- Good for complex decisions (Stop, SubagentStop)
3. Choose Hook Event
PreToolUse
Runs before tool executes.
Use for:
- Validate inputs
- Auto-approve safe operations
- Block dangerous commands
- Modify tool parameters
JSON Output:
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow" | "deny" | "ask", "permissionDecisionReason": "Why this decision", "updatedInput": { "field": "new value" } } }
PostToolUse
Runs after tool completes.
Use for:
- Auto-format code
- Run linters
- Validate outputs
- Log operations
JSON Output:
{ "decision": "block" | undefined, "reason": "Why blocking", "hookSpecificOutput": { "hookEventName": "PostToolUse", "additionalContext": "Extra info for Codex" } }
UserPromptSubmit
Runs when user submits prompt.
Use for:
- Add context automatically
- Validate prompts
- Block sensitive prompts
- Inject current time/date
JSON Output:
{ "decision": "block" | undefined, "reason": "Why blocking", "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": "Extra context" } }
Stop / SubagentStop
Runs when Codex/subagent finishes.
Use for:
- Verify tasks completed
- Continue if work remains
- Intelligent stoppage control
JSON Output:
{ "decision": "block" | undefined, "reason": "Why must continue" }
SessionStart
Runs when session starts.
Use for:
- Load environment variables
- Set up development context
- Install dependencies
- Inject initial context
JSON Output:
{ "hookSpecificOutput": { "hookEventName": "SessionStart", "additionalContext": "Initial context" } }
Special: Can persist environment variables:
#!/bin/bash if [ -n "$CLAUDE_ENV_FILE" ]; then echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE" fi
SessionEnd
Runs when session ends.
Use for:
- Cleanup tasks
- Save session stats
- Log session data
4. Create Hook Script
For bash command hooks, create a script:
Template:
#!/usr/bin/env bash # Read JSON input from stdin INPUT=$(cat) # Parse JSON (requires jq) TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty') FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') # Your validation logic here if [[ condition ]]; then echo "Error message" >&2 exit 2 # Block operation fi # Success exit 0
Python Template:
#!/usr/bin/env python3 import json import sys # Read JSON input try: input_data = json.load(sys.stdin) except json.JSONDecodeError as e: print(f"Error: Invalid JSON: {e}", file=sys.stderr) sys.exit(1) tool_name = input_data.get("tool_name", "") tool_input = input_data.get("tool_input", {}) # Your logic here if condition: # Block with error print("Error message", file=sys.stderr) sys.exit(2) # Or return JSON for control output = { "decision": "approve", "reason": "Auto-approved" } print(json.dumps(output)) sys.exit(0)
5. Configure in settings.json
Add hook configuration:
Basic Hook:
{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.Codex/hooks/format.sh" } ] } ] } }
Multiple Hooks:
{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.Codex/hooks/format.sh", "timeout": 30 }, { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.Codex/hooks/lint.sh", "timeout": 60 } ] } ], "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.Codex/hooks/validate-bash.py" } ] } ] } }
No Matcher (events without tools):
{ "hooks": { "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.Codex/hooks/add-context.sh" } ] } ] } }
6. Hook Input Reference
Each event receives JSON on stdin:
Common fields:
{ "session_id": "abc123", "transcript_path": "/path/to/transcript.jsonl", "cwd": "/current/working/dir", "permission_mode": "default", "hook_event_name": "PostToolUse" }
PreToolUse/PostToolUse:
{ "tool_name": "Write", "tool_input": { "file_path": "/path/to/file.txt", "content": "file content" }, "tool_response": { /* PostToolUse only */ "success": true } }
UserPromptSubmit:
{ "prompt": "User's submitted message" }
Stop/SubagentStop:
{ "stop_hook_active": false }
7. Exit Codes
-
0: Success
- stdout shown in verbose mode (Ctrl+O)
- For UserPromptSubmit/SessionStart: stdout added to context
- JSON parsed if present
-
2: Blocking error
- stderr shown to Codex
- Operation blocked (behavior varies by event)
- JSON in stdout ignored
-
Other: Non-blocking warning
- stderr shown in verbose mode
- Execution continues
8. Test the Hook
Test script directly:
# Create test input echo '{ "tool_name": "Write", "tool_input": { "file_path": "test.txt", "content": "hello" } }' | .Codex/hooks/your-hook.sh # Check exit code echo $?
Test in Codex:
1. Add hook to settings.json 2. Restart Codex 3. Run /hooks to verify it's loaded 4. Trigger the hook (e.g., write a file) 5. Check verbose mode (Ctrl+O) for output
Debug mode:
Codex --debug # Shows hook execution details
9. Provide Configuration
Show the complete configuration:
{ "hooks": { "EventName": [ { "matcher": "ToolPattern", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.Codex/hooks/script.sh", "timeout": 60 } ] } ] } }
Hook Examples
Example 1: Auto-Format Python Files
Hook script (
.Codex/hooks/format-python.sh):
#!/usr/bin/env bash INPUT=$(cat) # Get file path FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') # Only process .py files if [[ "$FILE_PATH" == *.py ]]; then # Run black formatter python -m black "$FILE_PATH" 2>&1 if [[ $? -eq 0 ]]; then echo "Formatted: $FILE_PATH" >&2 fi fi exit 0
Configuration:
{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.Codex/hooks/format-python.sh" } ] } ] } }
Example 2: Validate Bash Commands
Hook script (
.Codex/hooks/validate-bash.py):
#!/usr/bin/env python3 import json import sys import re # Dangerous patterns DANGEROUS = [ (r'\brm\s+-rf\s+/', 'Dangerous: rm -rf on root'), (r'>\s*/dev/sd[a-z]', 'Dangerous: writing to block device'), (r'\bcurl\s+.*\|\s*bash', 'Dangerous: piping curl to bash'), ] try: data = json.load(sys.stdin) except: sys.exit(1) if data.get('tool_name') != 'Bash': sys.exit(0) command = data.get('tool_input', {}).get('command', '') # Check for dangerous patterns for pattern, message in DANGEROUS: if re.search(pattern, command): print(f"⚠️ {message}", file=sys.stderr) print(f"Command: {command}", file=sys.stderr) sys.exit(2) # Block sys.exit(0) # Allow
Configuration:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.Codex/hooks/validate-bash.py" } ] } ] } }
Example 3: Add Timestamp to Prompts
Hook script (
.Codex/hooks/add-timestamp.sh):
#!/usr/bin/env bash # Output current timestamp echo "Current time: $(date '+%Y-%m-%d %H:%M:%S %Z')" exit 0
Configuration:
{ "hooks": { "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.Codex/hooks/add-timestamp.sh" } ] } ] } }
Example 4: Auto-Approve Documentation Reads
Hook script (
.Codex/hooks/auto-approve-docs.py):
#!/usr/bin/env python3 import json import sys data = json.load(sys.stdin) if data.get('tool_name') != 'Read': sys.exit(0) file_path = data.get('tool_input', {}).get('file_path', '') # Auto-approve docs if any(file_path.endswith(ext) for ext in ['.md', '.txt', '.json', '.yaml']): output = { "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow", "permissionDecisionReason": "Documentation file auto-approved" }, "suppressOutput": True } print(json.dumps(output)) sys.exit(0) sys.exit(0)
Configuration:
{ "hooks": { "PreToolUse": [ { "matcher": "Read", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.Codex/hooks/auto-approve-docs.py" } ] } ] } }
Example 5: Prevent Sensitive File Access
Hook script (
.Codex/hooks/block-secrets.sh):
#!/usr/bin/env bash INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') # Block sensitive files if [[ "$FILE_PATH" =~ \.env || "$FILE_PATH" =~ secrets/ || "$FILE_PATH" =~ \.aws/ ]]; then echo "⛔ Access to sensitive file blocked: $FILE_PATH" >&2 exit 2 fi exit 0
Configuration:
{ "hooks": { "PreToolUse": [ { "matcher": "Read|Write|Edit", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.Codex/hooks/block-secrets.sh" } ] } ] } }
Example 6: Intelligent Stop Hook (Prompt-based)
Configuration:
{ "hooks": { "Stop": [ { "hooks": [ { "type": "prompt", "prompt": "Evaluate whether Codex should stop. Context: $ARGUMENTS\n\nCheck if:\n1. All tasks are complete\n2. Tests are passing\n3. No errors need addressing\n\nRespond with JSON: {\"decision\": \"approve\" or \"block\", \"reason\": \"explanation\"}", "timeout": 30 } ] } ] } }
Example 7: Session Setup Hook
Hook script (
.Codex/hooks/session-setup.sh):
#!/usr/bin/env bash # Set up environment for session if [ -n "$CLAUDE_ENV_FILE" ]; then # Load nvm source ~/.nvm/nvm.sh nvm use 20 # Capture environment changes export -p >> "$CLAUDE_ENV_FILE" # Add custom variables echo 'export NODE_ENV=development' >> "$CLAUDE_ENV_FILE" fi # Add context echo "Development environment initialized" echo "Node version: $(node --version)" exit 0
Configuration:
{ "hooks": { "SessionStart": [ { "matcher": "startup", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.Codex/hooks/session-setup.sh" } ] } ] } }
Matcher Patterns
Exact match:
"matcher": "Write"
Multiple tools (regex):
"matcher": "Write|Edit|NotebookEdit"
All tools:
"matcher": "*"
Or:
"matcher": ""
MCP tools:
"matcher": "mcp__github__.*" "matcher": "mcp__.*__write.*"
Event-specific matchers:
Notification:
"matcher": "permission_prompt" "matcher": "idle_prompt"
PreCompact:
"matcher": "manual" "matcher": "auto"
SessionStart:
"matcher": "startup" "matcher": "resume" "matcher": "clear"
Environment Variables
Available in hook scripts:
- Absolute path to project root$CLAUDE_PROJECT_DIR
- "true" if remote/web, empty if local$CLAUDE_CODE_REMOTE
- (SessionStart only) File to persist env vars$CLAUDE_ENV_FILE- Standard environment variables
Best Practices
DO:
✅ Keep hooks fast (<100ms recommended) ✅ Provide clear error messages ✅ Use appropriate exit codes ✅ Quote variables in bash:
"$VAR"
✅ Validate inputs before processing
✅ Test thoroughly before deploying
✅ Use $CLAUDE_PROJECT_DIR for portability
✅ Document what your hook does
DON'T:
❌ Run slow operations (full test suites) ❌ Block legitimate operations unnecessarily ❌ Use hooks for everything (be selective) ❌ Forget to handle errors ❌ Skip input validation ❌ Hardcode absolute paths ❌ Leave debug output in production
Security Considerations
⚠️ USE AT YOUR OWN RISK
Hooks execute arbitrary commands:
- Can modify/delete any files
- Can access sensitive data
- Can cause data loss
- Anthropic provides no warranty
Best practices:
- Validate and sanitize inputs
- Quote all variables
- Block path traversal (
).. - Use absolute paths
- Skip sensitive files
- Test in safe environment first
Troubleshooting
Hook Not Running
Check:
- Hook is in
correctlysettings.json - Matcher pattern is correct (case-sensitive)
- Script has execute permissions:
chmod +x script.sh - Script shebang is correct:
#!/usr/bin/env bash - Restart Codex after config changes
Debug:
# Run with debug mode Codex --debug # Check hook execution in output # Shows: "Executing hooks for PostToolUse:Write"
Hook Errors
Check:
- Script runs standalone:
echo '{}' | ./script.sh - Exit code is correct:
echo $? - JSON output is valid:
./script.sh | jq . - Timeout is sufficient (default: 60s)
View errors:
- Verbose mode: Ctrl+O
- Debug mode:
Codex --debug - Check stderr output
Permissions Issues
Check:
# Make script executable chmod +x .Codex/hooks/script.sh # Verify permissions ls -la .Codex/hooks/
JSON Parse Errors
Validate JSON:
# Test JSON output echo '{}' | ./script.sh | jq . # Common issues: # - Missing quotes # - Trailing commas # - Single quotes instead of double
Resources
- Official Hook Docs:
.Codex/skills/ai/Codex/docs/code_claude_com/docs_en_hooks.md - Hook Guide:
.Codex/skills/ai/Codex/docs/code_claude_com/docs_en_hooks-guide.md - Settings Reference:
.Codex/skills/ai/Codex/docs/code_claude_com/docs_en_settings.md - Project Guide:
.Codex/docs/creating-components.md