Get the FREE Ultimate OpenClaw Setup Guide →

hook-architecture

npx machina-cli add skill zircote/auto-harness/hook-architecture --openclaw
Files (1)
SKILL.md
7.9 KB
<!-- BEGIN MNEMONIC PROTOCOL -->

Memory

Search first: /mnemonic:search {relevant_keywords} Capture after: /mnemonic:capture {namespace} "{title}"

Run /mnemonic:list --namespaces to see available namespaces from loaded ontologies.

<!-- END MNEMONIC PROTOCOL -->

Hook-Driven Test Architecture

Understand the hook system that powers automated testing in Claude Code.

Mnemonic Integration

Before explaining hook architecture, check mnemonic for prior learnings:

# Search for hook-related memories
rg -i "hook|UserPromptSubmit|test-wrapper|prompt-interception" ~/.claude/mnemonic/ ./.claude/mnemonic/ --glob "*.memory.md" -l | head -5

Apply recalled context:

  • Prior hook implementations that worked well
  • Common pitfalls with JSON escaping or state management
  • User's familiarity level with hook concepts

Architecture Overview

The hook-driven test framework transforms Claude Code's conversational interface into an automated test harness by intercepting user prompts and replacing them with test instructions.

┌─────────────────────────────────────────────────────┐
│                 Claude Code Session                  │
├─────────────────────────────────────────────────────┤
│  User: "next"                                       │
│       │                                             │
│       ▼                                             │
│  ┌─────────────────────────────────────────────┐   │
│  │         UserPromptSubmit Hook                │   │
│  │         (test-wrapper.sh)                    │   │
│  │                                              │   │
│  │  1. Check test mode active                   │   │
│  │  2. Intercept command                        │   │
│  │  3. Call runner.sh                           │   │
│  │  4. Return {"replace": "new prompt"}         │   │
│  └─────────────────────────────────────────────┘   │
│       │                                             │
│       ▼                                             │
│  Claude sees: "Execute test: Call tool X..."        │
│       │                                             │
│       ▼                                             │
│  Claude executes the MCP tool call                  │
└─────────────────────────────────────────────────────┘

Core Components

1. UserPromptSubmit Hook

The hook intercepts every user prompt and can:

  • Pass through: Return unchanged for normal operation
  • Replace: Substitute with test instruction
  • Block: Prevent prompt from reaching Claude

Hook registration in hooks/hooks.json:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PLUGIN_ROOT}/hooks/test-wrapper.sh user-prompt-submit"
          }
        ]
      }
    ]
  }
}

2. Test Wrapper Script

The wrapper (hooks/test-wrapper.sh) handles prompt interception:

#!/usr/bin/env bash

handle_user_prompt_submit() {
  local input=$(cat)
  local prompt=$(echo "$input" | python3 -c "
import json, sys
print(json.load(sys.stdin).get('prompt', ''))
")

  # Check if test mode active
  if is_test_mode; then
    case "$prompt" in
      next|n)
        output=$("$RUNNER" next)
        json_replace "Execute the following test:\n\n$output"
        ;;
      validate*)
        response="${prompt#validate }"
        output=$("$RUNNER" validate "$response")
        json_replace "$output"
        ;;
      # ... other commands
    esac
  else
    # Pass through to normal processing
    echo "$input"
  fi
}

3. Runner Script

The runner (tests/functional/runner.sh) orchestrates test execution:

Key functions:

  • cmd_init - Initialize test state
  • cmd_next - Return next test action
  • cmd_validate - Check response against expectations
  • cmd_status - Report progress
  • cmd_report - Generate results

4. State Management

Persistent state in .claude/test-state.json:

{
  "mode": "running",
  "total_tests": 53,
  "current_index": 5,
  "current_test": {
    "id": "test_id",
    "action": "...",
    "expect": [...]
  },
  "results": [
    {"id": "test1", "status": "pass"},
    {"id": "test2", "status": "fail", "failures": ["..."]}
  ],
  "saved_vars": {
    "memory_id": "abc123"
  }
}

Hook Input/Output Format

Input (JSON via stdin)

{
  "session_id": "abc123",
  "prompt": "user's typed command",
  "cwd": "/project/path"
}

Output (JSON to stdout)

Replace prompt:

{
  "replace": "New prompt text for Claude"
}

Pass through:

{}

Block prompt:

{
  "continue": false,
  "systemMessage": "Explanation"
}

Critical Implementation Details

JSON Escaping

Hook output must be valid JSON. Use Python for reliable escaping:

json_replace() {
  local content="$1"
  python3 -c "
import json, sys
content = sys.stdin.read()
print(json.dumps({'replace': content}))
" <<< "$content"
}

Common pitfall: Bash string escaping breaks on newlines and special characters.

State Persistence

Each hook invocation is stateless. State must persist to disk:

update_state() {
  local field="$1"
  local value="$2"
  python3 -c "
import json
with open('$STATE_FILE', 'r+') as f:
    data = json.load(f)
    data['$field'] = $value
    f.seek(0)
    json.dump(data, f, indent=2)
    f.truncate()
"
}

Test Mode Detection

Check state file to determine if tests are running:

is_test_mode() {
  [[ -f "$STATE_FILE" ]] || return 1
  local mode=$(python3 -c "
import json
with open('$STATE_FILE') as f:
    print(json.load(f).get('mode', ''))
")
  [[ "$mode" == "running" ]]
}

Test Execution Flow

1. User: /run-tests
   └─> Initializes state, sets mode=running

2. User: "next"
   └─> Hook intercepts
   └─> Runner returns test action
   └─> Hook replaces prompt with action
   └─> Claude executes test

3. User: "validate <response>"
   └─> Hook intercepts
   └─> Runner validates against expectations
   └─> Records PASS/FAIL
   └─> Advances to next test

4. Repeat steps 2-3 until all tests complete

5. User: "report"
   └─> Runner generates summary

Extension Points

Adding New Commands

Extend the wrapper to handle new commands:

case "$prompt" in
  retry)
    # Re-run current test
    output=$("$RUNNER" retry)
    json_replace "$output"
    ;;
esac

Custom Validation

Add validation types in runner:

if 'json_path' in exp:
    # JSONPath validation
    import jsonpath_ng
    matches = jsonpath_ng.parse(exp['json_path']).find(response)
    if not matches:
        failures.append(f"JSONPath not found: {exp['json_path']}")

Pre/Post Test Hooks

Add lifecycle hooks in runner:

run_pre_test_hook() {
  if [[ -x "$HOOKS_DIR/pre-test.sh" ]]; then
    "$HOOKS_DIR/pre-test.sh" "$current_test_id"
  fi
}

Additional Resources

Reference Files

  • references/state-management.md - Test state file schema and management

Source

git clone https://github.com/zircote/auto-harness/blob/main/skills/hook-architecture/SKILL.mdView on GitHub

Overview

This skill explains how hook-driven automated testing powers Claude Code by intercepting prompts and transforming conversations into test instructions. It covers the hook lifecycle, mnemonic context, and the role of the test wrapper in orchestrating test execution.

How This Skill Works

The UserPromptSubmit hook intercepts every user prompt and can pass through, replace, or block it. Hook registration is defined in hooks/hooks.json, enabling specific interception behavior. The Test Wrapper Script (hooks/test-wrapper.sh) handles the interception logic and delegates test actions to a runner, returning modified prompts to Claude.

When to Use It

  • Explaining the end-to-end flow of hook-driven prompt interception.
  • Debugging test mode behavior with UserPromptSubmit.
  • Designing and registering a new hook in hooks.json.
  • Verifying how prompts are replaced or blocked during tests.
  • Documenting how the wrapper script and runner cooperate.

Quick Start

  1. Step 1: Review the Architecture Overview and identify the hook you’ll modify.
  2. Step 2: Register the hook in hooks/hooks.json with a suitable matcher.
  3. Step 3: Run in test mode and observe hooks/test-wrapper.sh intercept prompts and invoke the runner.

Best Practices

  • Keep hook matchers specific to avoid unintended replacements.
  • Document which prompts are replaced vs passed through.
  • Test in development before enabling in production; clearly toggle test mode.
  • Ensure proper JSON escaping in intercepted prompts.
  • Log hook actions for easier troubleshooting and audits.

Example Use Cases

  • UserPromptSubmit intercepts a 'next' command and routes to the test-runner.
  • The wrapper outputs 'Execute the following test:' along with runner results.
  • Blocking a sensitive prompt in production to prevent unsafe actions.
  • Hook JSON configuration in hooks/hooks.json demonstrating the matcher setup.
  • Mnemonic integration to reuse prior hook learnings for consistency.

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers