CC-301c · Module 1
Console.log Communication Protocol
3 min read
Hooks communicate with Claude through a deliberately constrained protocol: the first console.log output from your hook script is treated as the instruction to Claude. Everything else — console.error, subsequent console.log calls, stderr output — is debug logging that Claude never sees. This design is intentional. It forces hooks to produce a single, clean instruction rather than a stream of noisy output.
The instruction format is a JSON object with two fields: decision (either "approve," "block," or for stop hooks, the action to take) and reason (the human-readable explanation that Claude uses to understand what happened and what to do next). The reason field is critical — it is not just logging. It is the instruction that drives Claude's next action. "Please fix TypeScript errors: src/App.tsx line 42: Type string is not assignable to type number" gives Claude a clear, actionable directive.
The single-instruction constraint has a practical consequence that trips up every new hook author: any tool you invoke from your hook script must run quietly. If you run tsc inside a stop hook and tsc produces output on stdout, that output becomes the instruction to Claude — not the JSON object you intended. Claude receives a wall of TypeScript compiler output instead of a structured instruction and has no idea what to do with it.
The solution is the --quiet flag or output redirection. Run tsc --noEmit 2>&1 and capture the output in a variable. Inspect the variable. Construct your JSON instruction based on what you found. Then — and only then — console.log the JSON. Everything between "hook starts" and "console.log the JSON" must be silent on stdout. Use console.error for any debug output you need during development. This is not a suggestion. It is the rule that makes hooks work.