BD Brain Drip
Build a Claude Code Harness Extension

Step 4: Write the Orchestrator with Hooks

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:

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 →