343 lines
14 KiB
Markdown
343 lines
14 KiB
Markdown
# Hook System Architecture
|
|
|
|
The Turboshovel hook system is a **self-referential TypeScript application** that uses its own configuration format to define default behaviors. The plugin is built on itself.
|
|
|
|
## Core Concept
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ Claude Code Hook Event │
|
|
│ (PostToolUse, SubagentStop, UserPromptSubmit, etc.) │
|
|
└─────────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ hooks.json Registration │
|
|
│ Routes ALL hook events to TypeScript CLI │
|
|
│ node ${CLAUDE_PLUGIN_ROOT}/hooks/hooks-app/dist/cli.js │
|
|
└─────────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ TypeScript CLI │
|
|
│ plugin/hooks/hooks-app/src/cli.ts │
|
|
└─────────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
┌───────────────┴───────────────┐
|
|
▼ ▼
|
|
┌───────────────────────────────┐ ┌───────────────────────────────────┐
|
|
│ Context Injection │ │ Config Loading │
|
|
│ (PRIMARY - always runs) │ │ (loads + merges gates.json) │
|
|
│ │ │ │
|
|
│ 1. Project .claude/context/ │ │ 1. Plugin gates.json (defaults) │
|
|
│ 2. Plugin context/ (fallback)│ │ 2. Project gates.json (override) │
|
|
└───────────────────────────────┘ └───────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌───────────────────────────────────┐
|
|
│ Gate Execution │
|
|
│ │
|
|
│ Shell command gates (command:) │
|
|
│ TypeScript gates (no command:) │
|
|
└───────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌───────────────────────────────────┐
|
|
│ Action Handling │
|
|
│ │
|
|
│ CONTINUE → proceed │
|
|
│ BLOCK → prevent agent action │
|
|
│ STOP → halt Claude entirely │
|
|
│ {gate} → chain to another gate │
|
|
└───────────────────────────────────┘
|
|
```
|
|
|
|
## Self-Referential Design
|
|
|
|
The hook system uses **its own gates.json** to configure default behaviors:
|
|
|
|
```
|
|
plugin/hooks/gates.json ← Plugin defaults (TypeScript gates)
|
|
↓ merged with
|
|
.claude/gates.json ← Project overrides (user configuration)
|
|
↓
|
|
Merged Configuration ← Project takes precedence
|
|
```
|
|
|
|
### Plugin gates.json
|
|
|
|
```json
|
|
{
|
|
"gates": {
|
|
"commands": {
|
|
"description": "Context-aware command injection",
|
|
"on_pass": "CONTINUE",
|
|
"on_fail": "CONTINUE"
|
|
},
|
|
"plugin-path": { ... }
|
|
},
|
|
"hooks": {
|
|
"UserPromptSubmit": {
|
|
"gates": ["commands"]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**TypeScript gates** have no `command` field - they're implemented in `src/gates/`.
|
|
|
|
**Shell command gates** have a `command` field - they execute shell commands.
|
|
|
|
## Directory Structure
|
|
|
|
```
|
|
plugin/hooks/
|
|
├── hooks.json # Hook registration (routes to CLI)
|
|
├── gates.json # Plugin default gates configuration
|
|
├── ARCHITECTURE.md # This file
|
|
├── CONVENTIONS.md # Context file naming conventions
|
|
├── README.md # Quick start guide
|
|
├── SETUP.md # Detailed setup instructions
|
|
├── TYPESCRIPT.md # TypeScript gate development
|
|
│
|
|
├── context/ # Plugin-level context files (NEW)
|
|
│ └── session-start.md # Injects on SessionStart
|
|
│
|
|
├── hooks-app/ # TypeScript application
|
|
│ ├── src/
|
|
│ │ ├── cli.ts # Entry point
|
|
│ │ ├── dispatcher.ts # Main dispatch logic
|
|
│ │ ├── context.ts # Context file discovery/injection
|
|
│ │ ├── config.ts # Config loading/merging
|
|
│ │ ├── gate-loader.ts # Gate execution
|
|
│ │ ├── action-handler.ts # Action processing
|
|
│ │ ├── session.ts # Session state management
|
|
│ │ ├── logger.ts # Debug logging
|
|
│ │ ├── types.ts # TypeScript interfaces
|
|
│ │ ├── utils.ts # Utility functions
|
|
│ │ └── gates/ # Built-in TypeScript gates
|
|
│ │ ├── index.ts # Gate registry
|
|
│ │ └── plugin-path.ts
|
|
│ └── dist/ # Compiled JavaScript
|
|
│
|
|
└── examples/
|
|
├── context/ # Example context files
|
|
├── strict.json # Example: strict mode
|
|
├── permissive.json # Example: warn only
|
|
└── pipeline.json # Example: gate chaining
|
|
```
|
|
|
|
## Execution Flow
|
|
|
|
### 1. Hook Event Received
|
|
|
|
Claude Code fires a hook event (e.g., `UserPromptSubmit`). The `hooks.json` routes it to the TypeScript CLI:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"UserPromptSubmit": [{
|
|
"matcher": ".*",
|
|
"hooks": [{
|
|
"type": "command",
|
|
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/hooks-app/dist/cli.js"
|
|
}]
|
|
}]
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Context Injection (Primary Behavior)
|
|
|
|
**Always runs first.** Discovers and injects markdown content from:
|
|
|
|
1. **Project context** (highest priority):
|
|
- `.claude/context/{name}-{stage}.md`
|
|
- `.claude/context/slash-command/{name}-{stage}.md`
|
|
- `.claude/context/skill/{name}-{stage}.md`
|
|
|
|
2. **Plugin context** (fallback):
|
|
- `${CLAUDE_PLUGIN_ROOT}/context/{name}-{stage}.md`
|
|
- (same variations as project)
|
|
|
|
### 3. Config Loading and Merging
|
|
|
|
Loads both configs and merges them:
|
|
|
|
```typescript
|
|
// Load plugin defaults first
|
|
const pluginConfig = await loadConfigFile(`${CLAUDE_PLUGIN_ROOT}/hooks/gates.json`);
|
|
|
|
// Load project overrides
|
|
const projectConfig = await loadConfigFile('.claude/gates.json');
|
|
|
|
// Merge: project overrides plugin
|
|
const mergedConfig = {
|
|
hooks: { ...pluginConfig.hooks, ...projectConfig.hooks },
|
|
gates: { ...pluginConfig.gates, ...projectConfig.gates }
|
|
};
|
|
```
|
|
|
|
### 4. Gate Execution
|
|
|
|
For each gate in the hook's `gates` array:
|
|
|
|
**TypeScript Gate** (no `command` field):
|
|
```typescript
|
|
// Gate name maps to module: "commands" → gates/commands.ts
|
|
const gates = await import('./gates');
|
|
const result = await gates.commands.execute(input);
|
|
```
|
|
|
|
**Shell Command Gate** (has `command` field):
|
|
```typescript
|
|
const { stdout, stderr } = await exec(gateConfig.command, { cwd });
|
|
```
|
|
|
|
### 5. Action Handling
|
|
|
|
Based on gate result and `on_pass`/`on_fail` configuration:
|
|
|
|
- **CONTINUE**: Proceed to next gate or complete
|
|
- **BLOCK**: Return block decision to Claude Code
|
|
- **STOP**: Return stop signal to halt Claude
|
|
- **{gate_name}**: Chain to another gate
|
|
|
|
## Supported Hook Events
|
|
|
|
All 12 Claude Code hook types are supported:
|
|
|
|
| Event | Context Pattern | Description |
|
|
|-------|----------------|-------------|
|
|
| `SessionStart` | `session-start.md` | Beginning of Claude session |
|
|
| `SessionEnd` | `session-end.md` | End of Claude session |
|
|
| `UserPromptSubmit` | `prompt-submit.md` | User submits prompt |
|
|
| `SlashCommandStart` | `{command}-start.md` | Command begins |
|
|
| `SlashCommandEnd` | `{command}-end.md` | Command completes |
|
|
| `SkillStart` | `{skill}-start.md` | Skill loads |
|
|
| `SkillEnd` | `{skill}-end.md` | Skill completes |
|
|
| `SubagentStop` | `{agent}-end.md` | Agent completes |
|
|
| `PreToolUse` | `{tool}-pre.md` | Before tool executes |
|
|
| `PostToolUse` | `{tool}-post.md` | After tool executes |
|
|
| `Stop` | `agent-stop.md` | Agent stops |
|
|
| `Notification` | `notification-receive.md` | Notification received |
|
|
|
|
## TypeScript Gates
|
|
|
|
Gates without a `command` field are TypeScript gates. They're implemented in `src/gates/` and export an `execute` function:
|
|
|
|
```typescript
|
|
// src/gates/commands.ts
|
|
import { HookInput, GateResult } from '../types';
|
|
|
|
export async function execute(input: HookInput): Promise<GateResult> {
|
|
// Gate logic here
|
|
return {
|
|
additionalContext: '...' // Inject content
|
|
// or
|
|
decision: 'block',
|
|
reason: '...' // Block execution
|
|
// or
|
|
continue: false,
|
|
message: '...' // Stop Claude
|
|
};
|
|
}
|
|
```
|
|
|
|
Register in `src/gates/index.ts`:
|
|
```typescript
|
|
export * as pluginPath from './plugin-path';
|
|
```
|
|
|
|
Gate name maps to export: `"commands"` → `gates.commands.execute()`
|
|
|
|
## Configuration Merging
|
|
|
|
**Project configuration overrides plugin configuration at the key level:**
|
|
|
|
```json
|
|
// Plugin gates.json (defaults)
|
|
{
|
|
"hooks": {
|
|
"UserPromptSubmit": { "gates": ["commands"] },
|
|
"PostToolUse": { "gates": ["check"] }
|
|
},
|
|
"gates": {
|
|
"commands": { "on_pass": "CONTINUE" },
|
|
"check": { "command": "echo placeholder" }
|
|
}
|
|
}
|
|
|
|
// Project .claude/gates.json (overrides)
|
|
{
|
|
"hooks": {
|
|
"PostToolUse": { "gates": ["lint", "test"] } // Replaces plugin's PostToolUse
|
|
},
|
|
"gates": {
|
|
"check": { "command": "npm run lint" }, // Replaces plugin's check
|
|
"lint": { "command": "eslint ." }, // New gate
|
|
"test": { "command": "npm test" } // New gate
|
|
}
|
|
}
|
|
|
|
// Merged result
|
|
{
|
|
"hooks": {
|
|
"UserPromptSubmit": { "gates": ["commands"] }, // From plugin
|
|
"PostToolUse": { "gates": ["lint", "test"] } // From project (replaced)
|
|
},
|
|
"gates": {
|
|
"commands": { "on_pass": "CONTINUE" }, // From plugin
|
|
"check": { "command": "npm run lint" }, // From project (replaced)
|
|
"lint": { "command": "eslint ." }, // From project (new)
|
|
"test": { "command": "npm test" } // From project (new)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Session State
|
|
|
|
The hook system maintains session state for cross-hook coordination:
|
|
|
|
```typescript
|
|
interface SessionState {
|
|
session_id: string; // Unique session ID
|
|
started_at: string; // ISO timestamp
|
|
active_command: string | null; // Current slash command
|
|
active_skill: string | null; // Current skill
|
|
edited_files: string[]; // Files modified this session
|
|
file_extensions: string[]; // Extensions edited
|
|
metadata: Record<string, any>; // Custom data
|
|
}
|
|
```
|
|
|
|
State persists in `$TMPDIR/turboshovel/session-{cwd-hash}.json`.
|
|
|
|
## Logging
|
|
|
|
All hook invocations are logged to `$TMPDIR/turboshovel/hooks-YYYY-MM-DD.log`:
|
|
|
|
```bash
|
|
# View logs
|
|
tail -f $(node plugin/hooks/hooks-app/dist/cli.js log-path)
|
|
|
|
# Or use mise task
|
|
mise run logs
|
|
```
|
|
|
|
Log entries include:
|
|
- Hook event type
|
|
- Config file paths loaded
|
|
- Gate execution results
|
|
- Action handling decisions
|
|
- Timing information
|
|
|
|
## Benefits of Self-Referential Design
|
|
|
|
1. **Dogfooding**: Plugin uses its own infrastructure
|
|
2. **Consistent Patterns**: Same config format for plugin and projects
|
|
3. **Testable**: TypeScript gates are unit-testable
|
|
4. **Debuggable**: Standard TypeScript tooling works
|
|
5. **Extensible**: Add new TypeScript gates, projects can override
|
|
6. **Type-Safe**: Full TypeScript type checking
|