Learn Claude Code: Hooks Deep Dive Input Gates, Feedback Loops, and Turn Gates

Input Gates, Feedback Loops, and Turn Gates

Advanced 🕐 14 min Lesson 5 of 12
What you'll learn
  • Write a UserPromptSubmit hook that validates or blocks user messages before Claude processes them
  • Configure PostToolUse hooks to log, lint, or react to tool results after execution
  • Build a Stop hook that runs tests and injects pass/fail results as context for the next turn

The Prompt-Tool-Stop Lifecycle

Every conversation turn in Claude Code follows the same sequence: you send a message, Claude processes it, Claude calls tools, Claude responds. Hooks give you interception points at each phase of this sequence.

UserPromptSubmit fires before Claude reads your message. It is the only hook that can block a turn before any tool calls happen. PostToolUse fires after each tool call succeeds, with the tool output available for inspection. Stop fires when Claude finishes its response — after all tool calls are done and the turn is about to close.

Together these three events create a pipeline: gate the input, monitor the middle, gate the output. Each hook in this pipeline can inject context that shapes the next phase, building a feedback loop that keeps Claude Code behavior consistent without requiring you to be present.

UserPromptSubmit: Gating the Input

UserPromptSubmit fires before Claude processes each user message. The hook receives the raw prompt content in prompt_content and can block the turn by exiting with code 2. This is the earliest possible interception point — before Claude has read the message, before any tool calls have been planned.

Common uses include prompt injection filtering (blocking messages that contain attempts to override system instructions), content policy enforcement (blocking requests that violate project rules), and input validation (ensuring messages meet a required format before Claude spends tokens on them).

A minimal prompt injection guard:

#!/bin/bash
content=$(cat | jq -r .prompt_content)
if echo "$content" | grep -qi "ignore previous instructions|disregard your system prompt"; then
  echo "Message blocked: potential prompt injection detected." >&2
  exit 2
fi
exit 0

UserPromptSubmit can also return a JSON response on stdout to provide context or a system message without blocking. Set systemMessage to inject a warning that Claude will see alongside the prompt.

PostToolUse: Reacting to Tool Results

PostToolUse fires after every successful tool execution. The hook receives tool_name, tool_input, and tool_output — the complete picture of what just happened. Unlike PreToolUse, PostToolUse cannot block the action that just ran. The tool has already executed. But it can react, observe, and provide feedback.

Logging is the simplest use case. A PostToolUse hook that matches Bash can append every command to an audit log with its output and timestamp:

#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | jq -r .tool_input.command)
out=$(echo "$input" | jq -r .tool_output // empty)
echo "[$(date -Iseconds)] CMD: $cmd" >> ~/.claude/audit.log
echo "OUT: ${out:0:200}" >> ~/.claude/audit.log
exit 0

Style enforcement is more sophisticated. A PostToolUse hook on Edit can send the modified file through a linter and return the lint output as a systemMessage that Claude sees in the next turn, prompting it to fix style violations automatically.

Secret scanning is another common pattern. A PostToolUse hook on Write or Edit can scan the file content for patterns matching API keys, passwords, or tokens, and surface a warning if it finds any before they end up in git.

PostToolUseFailure is the companion event that fires when a tool fails. It receives the error field instead of tool_output. Use it for error logging, alerting, or providing Claude with recovery suggestions when a tool fails.

Stop: Gating the Output

Stop fires when Claude finishes a response and the turn is about to close. Unlike PostToolUse, Stop can block the turn from completing — if a Stop hook exits with code 2, Claude Code treats the turn as incomplete and continues the conversation with the hook's stderr as context.

This is the basis for the "test gate" pattern. A Stop hook runs your test suite and inspects the result. If tests pass, the hook exits 0 and the turn closes normally. If tests fail, the hook exits 2 with the failure summary in stderr, keeping the turn open so Claude can see the failures and attempt a fix.

The JSON output from Stop can also inject context without blocking. The hookSpecificOutput.additionalContext field adds text that Claude can see in the next turn. Use this to deliver test results, build status, deployment outputs, or environment state automatically after every response — without requiring Claude to run a verification step explicitly.

The Test Gate Pattern

The test gate is one of the most powerful hook patterns in practice. It combines Stop with additionalContext to create a self-correcting loop where Claude sees its own test results after every turn and continues working until they pass.

A complete Stop hook for a Node.js project:

#!/bin/bash
result=$(npm test 2>&1)
exit_code=$?
passed=$(echo "$result" | grep -oP "d+ passed" | head -1)
failed=$(echo "$result" | grep -oP "d+ failed" | head -1)
if [ $exit_code -ne 0 ]; then
  echo "Tests failed: $failed. Fix the failing tests before completing this turn." >&2
  exit 2
fi
cat <

With this hook active, every time Claude finishes a response, it either sees "All tests green" as context for the next turn, or the turn does not close until the tests pass. This transforms Claude Code from a command executor into a self-verifying development loop.

StopFailure fires when Claude cannot complete a response due to an API error (rate limit, authentication failure, network issue). Use it to log the error, send an alert, or take a recovery action when Claude Code encounters infrastructure problems.

Key takeaways
  • UserPromptSubmit fires before Claude reads the message — it is the only hook that can block a turn before any tool calls happen, making it the right place for prompt injection filtering and content policy enforcement
  • PostToolUse cannot block the tool that already ran — it fires after success, so use it for logging, linting, secret scanning, and feedback injection, not for stopping already-executed actions
  • Stop fires when Claude finishes responding and can prevent turn completion — exit 2 from Stop keeps the turn open until the condition passes, enabling the test gate pattern
  • additionalContext in Stop's hookSpecificOutput injects text Claude sees in the next turn — use it to deliver test results, build output, and environment state automatically after every response
  • Chaining UserPromptSubmit and Stop creates a full conversation gate — input filtering on the way in, validation on the way out, with PostToolUse feedback in between