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.shPostToolUse 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.shCLAUDE_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.shA 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.shTest 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;Stophook as aggregator.permission-and-tool-scoping-primitives.md—PreToolUsehook 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 →