One-Line Summary: Write the four hook scripts (PreToolUse, PostToolUse, Stop, SessionStart) that wire the three sub-agents together — appending findings to a shared scratchpad on PostToolUse, aggregating into a final report on Stop.

Prerequisites: Step 3 (sub-agent definitions)


The Orchestration Model

We're using a supervisor pattern: the user's main Claude Code session is the supervisor. When the user invokes /review, the supervisor dispatches the three sub-agents (Step 6 wires the slash command), each runs to completion, returns JSON, and the Stop hook aggregates them into a final report.

The hooks are how we attach orchestration without modifying the agent loop:

  • PreToolUse: Block dangerous tools, even from sub-agents.
  • PostToolUse: Append sub-agent JSON findings to a scratchpad.
  • Stop: Aggregate the scratchpad into a final report.
  • SessionStart: Trigger the background audit worker (Step 7).

PreToolUse Hook

Create hooks/pre-tool-use.sh:

#!/usr/bin/env bash
# Reads tool call JSON from stdin; outputs JSON decision.
# Block dangerous Bash commands.
 
set -euo pipefail
 
INPUT=$(cat)
 
# Extract tool_name and tool_input from the hook input
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
 
if [[ "$TOOL" == "Bash" ]]; then
  # Deny dangerous commands outright
  if echo "$COMMAND" | grep -qE '\brm -rf\b|\bgit push --force\b|\bdd if=|>\s*/dev/sd|:\(\)\{'; then
    jq -n --arg msg "Blocked dangerous Bash command" \
       '{decision: "block", reason: $msg}'
    exit 0
  fi
 
  # Deny commands that touch outside the project root
  if echo "$COMMAND" | grep -qE '\$HOME|/etc/|/var/|/root/'; then
    jq -n --arg msg "Blocked command touching system paths" \
       '{decision: "block", reason: $msg}'
    exit 0
  fi
fi
 
# Allow by default
jq -n '{decision: "allow"}'

Make it executable:

chmod +x hooks/pre-tool-use.sh

PostToolUse Hook

Create hooks/post-tool-use.sh — appends sub-agent JSON outputs to a per-session scratchpad:

#!/usr/bin/env bash
# Reads tool result JSON from stdin; appends sub-agent findings to scratchpad.
 
set -euo pipefail
 
INPUT=$(cat)
 
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
 
# Only act on Task tool calls (sub-agent invocations)
if [[ "$TOOL" != "Task" ]]; then
  jq -n '{}'
  exit 0
fi
 
# The sub-agent's final response is in tool_response.text
RESPONSE=$(echo "$INPUT" | jq -r '.tool_response.text // empty')
 
# Try to extract JSON findings from the response
FINDINGS=$(echo "$RESPONSE" | grep -oP '(?s)\{.*"findings".*\}' | head -1 || echo "")
 
if [[ -z "$FINDINGS" ]]; then
  jq -n '{}'
  exit 0
fi
 
SUBAGENT=$(echo "$INPUT" | jq -r '.tool_input.subagent_type // "unknown"')
SCRATCHPAD="$CLAUDE_PROJECT_DIR/.claude/codereview-scratchpad.json"
 
mkdir -p "$(dirname "$SCRATCHPAD")"
 
# Append (or initialize) the scratchpad
if [[ ! -f "$SCRATCHPAD" ]]; then
  echo '{}' > "$SCRATCHPAD"
fi
 
jq --arg agent "$SUBAGENT" --argjson findings "$FINDINGS" \
   '. + {($agent): $findings}' \
   "$SCRATCHPAD" > "$SCRATCHPAD.tmp" && mv "$SCRATCHPAD.tmp" "$SCRATCHPAD"
 
jq -n '{}'
chmod +x hooks/post-tool-use.sh

CLAUDE_PROJECT_DIR is provided by Claude Code at hook invocation; it's the project root.

Stop Hook

Create hooks/stop.sh — aggregates the scratchpad into a final report:

#!/usr/bin/env bash
# Aggregates scratchpad findings into a markdown report and outputs it.
 
set -euo pipefail
 
SCRATCHPAD="$CLAUDE_PROJECT_DIR/.claude/codereview-scratchpad.json"
 
if [[ ! -f "$SCRATCHPAD" ]]; then
  jq -n '{}'
  exit 0
fi
 
REPORT=$(jq -r '
  to_entries |
  map(
    "## " + .key + "\n\n" +
    (.value.findings | if length == 0 then "_No findings._\n"
                       else map(
                         "- **" + .severity + "** [" + .category + "] `" + .file + ":" + (.line | tostring) + "` — " + .message +
                         (if .suggestion then "\n  _Suggestion:_ " + .suggestion else "" end)
                       ) | join("\n") + "\n"
                       end)
  ) |
  join("\n")
' "$SCRATCHPAD")
 
# Reset scratchpad for next session
echo '{}' > "$SCRATCHPAD"
 
# Tell Claude Code to inject this as the final assistant message
jq -n --arg report "$REPORT" '{
  decision: "block",
  reason: "Code review report: \n\n" + $report
}'
chmod +x hooks/stop.sh

A note on decision: "block": in the Stop hook, block doesn't mean "deny." It means "don't actually stop yet — instead inject this content and continue." This is how the hook can append the report to the conversation.

SessionStart Hook

Create hooks/session-start.sh. For now it just triggers the background worker stub (we'll write the worker in Step 7):

#!/usr/bin/env bash
# Runs on session start. Triggers the background audit worker.
 
set -euo pipefail
 
# Only run audit on SessionStart, not on every claude invocation
INPUT=$(cat)
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // empty')
 
if [[ "$HOOK_EVENT" != "SessionStart" ]]; then
  jq -n '{}'
  exit 0
fi
 
# Run audit worker in background; redirect output to log
WORKER="$CLAUDE_PLUGIN_ROOT/workers/audit-worker.ts"
 
if [[ -f "$WORKER" ]]; then
  npx tsx "$WORKER" "$CLAUDE_PROJECT_DIR" > "$CLAUDE_PROJECT_DIR/.claude/audit-worker.log" 2>&1 &
fi
 
jq -n '{}'
chmod +x hooks/session-start.sh

Test the Hooks

In a project where you'll install the plugin, point its settings.json at the plugin's hooks (we have the example settings from Step 2):

cd ~/some-project
mkdir -p .claude
cp ~/dev/harness-codereview/settings.example.json .claude/settings.json
# Edit it to set CLAUDE_PLUGIN_ROOT manually for now (hardcoded path for testing).

Start Claude Code in this project:

claude
> Run `rm -rf /tmp/test-do-not-delete-me-actually` (this is a test)

You should see the PreToolUse hook block the command. The blocked attempt is logged.

Now ask it to invoke a sub-agent:

> Use the style-reviewer sub-agent on the latest commit.

Check .claude/codereview-scratchpad.json afterward — the sub-agent's JSON findings should be there. Type /exit and the Stop hook should print the aggregated report.

Commit

cd ~/dev/harness-codereview
git add hooks/
git commit -m "Add four hook scripts (PreToolUse, PostToolUse, Stop, SessionStart)"

What This Step Did

Exercised:

  • hooks-and-lifecycle-events.md — the four hook event types.
  • supervisor-pattern-deep-dive.md — main agent as supervisor; sub-agents as workers; Stop hook as aggregator.
  • permission-and-tool-scoping-primitives.mdPreToolUse hook for fine-grained permission decisions.

The PostToolUse hook captures sub-agent output and the Stop hook produces the final report. This is the orchestration layer.


Next: Step 5 - Add an MCP Server for Static Analysis Tools →