@@ -216,6 +216,119 @@ If hooks fail with Python errors:
2162162 . Check for syntax errors: ` python3 -m py_compile .claude/hooks/<hook>.py `
2172173 . Review hook output in Claude Code for error messages
218218
219+ ### Hook Response Schema Errors
220+
221+ ** CRITICAL** : Different hook event types require DIFFERENT response schemas!
222+
223+ #### Stop Hook Schema
224+
225+ ``` json
226+ {
227+ "continue" : true ,
228+ "stopReason" : " Why stopping/continuing" ,
229+ "decision" : " approve" | "block"
230+ }
231+ ```
232+
233+ - ` continue: true ` = block the stop, keep running
234+ - ` continue: false ` = approve the stop, let Claude stop
235+ - ` decision: "block" ` = block the stop
236+ - ` decision: "approve" ` = approve the stop
237+
238+ #### PreToolUse/PostToolUse Schema
239+
240+ ``` json
241+ {
242+ "hookSpecificOutput" : {
243+ "hookEventName" : " PreToolUse" ,
244+ "permissionDecision" : " allow" | "deny",
245+ "permissionDecisionReason" : " Why"
246+ }
247+ }
248+ ```
249+
250+ ** Common Error** : Using ` hookSpecificOutput ` for Stop events causes:
251+ ```
252+ Stop hook error: JSON validation failed: Hook JSON output validation failed
253+ ```
254+
255+ ## Dedicated Entry Points Pattern (CRITICAL)
256+
257+ When a hook may be called from multiple event types (Stop, PreToolUse, PostToolUse), it MUST have dedicated entry points for each. DO NOT try to infer the event type from input - it's unreliable.
258+
259+ ### Pattern
260+
261+ ``` python
262+ def make_response (event_name : str , decision : str , reason : str ) -> dict :
263+ """ Create response with correct schema for event type."""
264+ if event_name == " Stop" :
265+ # Stop uses completely different schema!
266+ return {
267+ " continue" : decision == " deny" , # deny stop = continue
268+ " stopReason" : reason,
269+ " decision" : " block" if decision == " deny" else " approve"
270+ }
271+ else :
272+ # PreToolUse/PostToolUse use hookSpecificOutput
273+ return {
274+ " hookSpecificOutput" : {
275+ " hookEventName" : event_name,
276+ " permissionDecision" : decision,
277+ " permissionDecisionReason" : reason
278+ }
279+ }
280+
281+ def process_hook_logic (event_name : str ) -> None :
282+ """ Core logic - event_name passed explicitly, NOT inferred."""
283+ # ... your logic here ...
284+ output_and_exit(make_response(event_name, " allow" , " Reason" ))
285+
286+ # DEDICATED ENTRY POINTS - one per event type
287+ def main_stop () -> None :
288+ process_hook_logic(" Stop" )
289+
290+ def main_pre_tool_use () -> None :
291+ process_hook_logic(" PreToolUse" )
292+
293+ def main () -> None :
294+ """ Default entry - uses CLAUDE_HOOK_EVENT env var."""
295+ event = os.environ.get(" CLAUDE_HOOK_EVENT" )
296+ if event:
297+ process_hook_logic(event)
298+ return
299+ main_stop() # Default to this hook's primary purpose
300+ ```
301+
302+ ### Registering for Multiple Events
303+
304+ Use ` CLAUDE_HOOK_EVENT ` environment variable in settings.json:
305+
306+ ``` json
307+ {
308+ "hooks" : {
309+ "Stop" : [{
310+ "hooks" : [{
311+ "type" : " command" ,
312+ "command" : " CLAUDE_HOOK_EVENT=Stop .claude/hooks/my-hook.py"
313+ }]
314+ }],
315+ "PreToolUse" : [{
316+ "hooks" : [{
317+ "type" : " command" ,
318+ "command" : " CLAUDE_HOOK_EVENT=PreToolUse .claude/hooks/my-hook.py"
319+ }]
320+ }]
321+ }
322+ }
323+ ```
324+
325+ ### Why This Pattern?
326+
327+ 1 . ** Correct Schema** - Each event type gets proper response format
328+ 2 . ** Reliable** - Event name explicit, not inferred
329+ 3 . ** Testable** - Each entry point tested independently
330+ 4 . ** DRY** - Shared logic with event-specific response formatting
331+
219332## Related Documentation
220333
221334- ** README.md** - Comprehensive hook documentation and usage guide
0 commit comments