Skip to content
Back to skills

Hook Architecture

ASecurity

This skill should be used when the user asks about "how hooks work",

  • 3 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 26, 2026
testing-securitypythonbashtestingapi

Works with

  • claude code
  • api
  • mcp

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned May 27, 2026

npx -y skills add zircote/auto-harness --skill hook-architecture --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Hook Architecture?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Hook Architecture
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/zircote-hook-architecture/badge)](https://www.skillsdirectory.com/skills/zircote-hook-architecture)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
allowed-tools:
- Bash
- Glob
- Grep
- Read
- Write
description: This skill should be used when the user asks about "how hooks work",
  "UserPromptSubmit", "hook-driven testing", "test wrapper", "prompt interception",
  "hook JSON format", "state management", or needs to understand the architecture
  of hook-driven automated testing.
name: hook-architecture
version: 1.0.0
---
<!-- 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:

```bash
# 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`:

```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:

```bash
#!/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`:

```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)

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

### Output (JSON to stdout)

**Replace prompt:**
```json
{
  "replace": "New prompt text for Claude"
}
```

**Pass through:**
```json
{}
```

**Block prompt:**
```json
{
  "continue": false,
  "systemMessage": "Explanation"
}
```

## Critical Implementation Details

### JSON Escaping

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

```bash
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:

```bash
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:

```bash
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:

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

### Custom Validation

Add validation types in runner:

```python
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:

```bash
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

Files in this skill

  • SKILL.md7.9 KB
  • references/state-management.md4.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.