# Phase 6: Per-Agent Hooks System — Specification

**Priority:** P5 · **Effort:** 2 days · **Dependencies:** Phase 4 (nice-to-have)  
**Reference:** `claude-code-main/src/utils/hooks/registerFrontmatterHooks.ts`, `sessionHooks.ts`

---

## 1. Problem Statement

Claude Code agents can declare lifecycle hooks in frontmatter (`hooks` field). These hooks fire at various points in the agent's lifecycle — when the agent starts, when the user submits a prompt, when a tool use is about to execute (Stop/SubagentStop), and after sampling. This enables plugins and extensions to react to agent activity at a granular level.

pi-subagents has `pi.events`-based lifecycle events (`subagents:created`, `started`, `completed`, etc.) but no per-agent hook mechanism. Extensions can observe agent lifecycle globally but cannot register agent-specific hooks that are scoped to a particular agent's lifetime.

---

## 2. Design

### 2.1. Hook Types

Support these hook types (matching Claude Code's subset):

| Hook Type | When It Fires | Signature |
|---|---|---|
| `SubagentStart` | When the agent begins execution | `(agentId, agentType, abortSignal) => AsyncIterable<{ additionalContexts: string[] }>` |
| `UserPromptSubmit` | When user sends a message to the agent | `(message, agentContext) => { content?: string; stop?: boolean }` |
| `Stop` / `SubagentStop` | Before a tool use executes | `(toolUse, agentContext) => { deny?: boolean; reason?: string }` |
| `PostSampling` | After the LLM responds | `(message, agentContext) => { modifications?: ContentBlock[] }` |

### 2.2. Frontmatter Schema

```yaml
---
name: my-agent
description: My custom agent
hooks:
  SubagentStart:
    - command: "echo 'Agent started: {{agentType}}'"
      type: "command"
  UserPromptSubmit:
    - command: "node .pi/hooks/validate-input.js"
      type: "command"
  SubagentStop:
    - pattern: "edit"
      type: "deny"
      reason: "My agent cannot edit files"
---
```

### 2.3. Hook Registration Lifecycle

1. When an agent **starts**, parse `hooks` from the agent definition
2. Register each hook function with the session's hook registry
3. When the agent **completes** (or errors), unregister all hooks
4. Hooks are scoped to the agent's `agentId` — concurrent agents don't interfere

### 2.4. Execution Model

Hooks are async functions. The agent's execution loop calls them at the appropriate lifecycle point:
- `SubagentStart`: called before the first LLM query. Returned `additionalContexts` are appended as a user message.
- `UserPromptSubmit`: called when the user submits a message. Can modify content or stop processing.
- `SubagentStop`: called before each tool execution. Return `deny: true` to block the tool call (with a reason).
- `PostSampling`: called after each LLM response. Can modify content blocks.

---

## 3. API Changes

### 3.1. `AgentConfig` — New Field

```typescript
export interface AgentConfig {
  // ...existing fields...
  /** Lifecycle hooks for this agent. */
  hooks?: HooksSettings;
}
```

### 3.2. Hook Types

```typescript
export interface HooksSettings {
  SubagentStart?: HookCommand[];
  UserPromptSubmit?: HookCommand[];
  SubagentStop?: HookCommand[];
  PostSampling?: HookCommand[];
}

interface HookCommand {
  command: string;
  type: "command" | "deny" | "prompt";
  pattern?: string;    // For deny hooks — tool name pattern
  reason?: string;     // For deny hooks — displayed reason
}
```

### 3.3. Hook Registry

```typescript
class AgentHookRegistry {
  register(agentId: string, hooks: HooksSettings): void;
  unregister(agentId: string): void;
  getHooks(agentId: string, hookType: string): HookCommand[];
  clearAll(): void;
}
```

---

## 4. Integration Points

| File | Change |
|---|---|
| `src/types.ts` | Add `HooksSettings`, `HookCommand` types, `hooks` on `AgentConfig` |
| `src/agent-hooks.ts` | New module: hook registry, execution helpers |
| `src/agent-runner.ts` | Call hooks at lifecycle points during agent execution |
| `src/agent-manager.ts` | Hook cleanup on agent completion |
| `src/custom-agents.ts` | Parse `hooks` from frontmatter |
| `src/index.ts` | Thread hook results into agent context |

---

## 5. Edge Cases

| Case | Behavior |
|---|---|
| No hooks defined | No-op — existing behavior unchanged |
| Hook command fails | Log warning, don't fail agent |
| Agent aborted mid-execution | Hooks cleaned up in `finally` block |
| Concurrent agents with hooks | Each agent has its own hook scope (keyed by agentId) |
| Invalid hook syntax in frontmatter | Parse error emitted once, hooks skipped |
| Hook returns deny on every tool | Agent loops until max turns, no tools execute |

---

## 6. Testing Plan

| Test | What it verifies |
|---|---|
| Hook registration | Hooks stored and retrievable by agentId |
| Hook cleanup | `unregister()` removes hooks for an agent |
| Hook isolation | Agent A's hooks don't interfere with Agent B's |
| Frontmatter parsing | YAML `hooks` field parsed correctly |
| SubagentStart context | `additionalContexts` from hook appended to agent messages |
| SubagentStop deny | Hook returning `deny: true` blocks the tool call |
| Hook command failure | Error logged, agent continues |
