> ## Documentation Index
> Fetch the complete documentation index at: https://www.truefoundry.com/llms.txt
> Use this file to discover all available pages before exploring further.

# AI Coding Assistant Hooks API

> Engineering design for Cursor and Claude Code hooks with TrueFoundry AI Gateway: protocol comparison, adapters, guardrail mapping, and implementation phases.

<Note>
  **Draft / RFC** — 2026-04-14 · TrueFoundry Engineering. Internal design reference; endpoints and behavior may change before general availability.
</Note>

## 1. Executive Summary

AI coding assistants (Cursor, Claude Code, and others emerging) now support **hooks** -- lifecycle callbacks that let external systems observe, control, and extend the agent loop. TrueFoundry's AI Gateway already has a working Cursor hooks endpoint (`/cursor/hooks/validate`). This document maps the exact API surface of each client, identifies gaps, and proposes a unified architecture to support all clients through the gateway's guardrail engine.

***

## 2. Current State (What We Have)

### Existing Endpoint

```
POST /cursor/hooks/validate
POST /api/llm/cursor/hooks/validate
```

**Implementation Files:**

* Router: `src/routers/cursorHooks.ts`
* Handler: `src/handlers/cursorHooksHandler.ts`

**Current Request Body:**

```json theme={"dark"}
{
  "hook_event_name": "beforeSubmitPrompt | beforeShellExecution | beforeReadFile | afterFileEdit | afterAgentResponse | beforeMCPExecution | afterMCPExecution",
  "text": "content to validate",
  "conversation_id": "string",
  "generation_id": "string",
  "workspace_roots": ["string"],
  "metadata": { "server": "mcp-server-name", "tool_name": "tool-name" }
}
```

**Current Response Body:**

```json theme={"dark"}
{
  "continue": true | false,
  "permission": "allow" | "deny",
  "userMessage": "string",
  "agentMessage": "string",
  "guardrail_checks": [
    { "id": "string", "verdict": true|false, "error": "string", "data": {}, "execution_time": 123 }
  ]
}
```

**What it does:** Maps hook events to guardrail types (input/output/mcp-pre/mcp-post), resolves applicable guardrails for the user/tenant, executes them, and returns allow/deny.

***

## 3. Cursor Hooks API -- Complete Reference

### 3.1 Protocol

| Aspect            | Detail                                                                                                  |
| ----------------- | ------------------------------------------------------------------------------------------------------- |
| **Transport**     | Hooks are local **child processes** communicating via **stdin/stdout JSON**. No HTTP involved natively. |
| **Registration**  | File-based: `.cursor/hooks.json` (project), `~/.cursor/hooks.json` (user), MDM-managed (enterprise)     |
| **Exit codes**    | `0` = success (parse stdout), `2` = block/deny, other = fail-open                                       |
| **Fail behavior** | Default: fail-open. Set `failClosed: true` to block on hook errors                                      |

### 3.2 Manifest Format (`hooks.json`)

```json theme={"dark"}
{
  "version": 1,
  "hooks": {
    "<eventName>": [
      {
        "command": "./path/to/script.sh",
        "type": "command" | "prompt",
        "timeout": 30,
        "loop_limit": 5,
        "failClosed": false,
        "matcher": "regex-pattern"
      }
    ]
  }
}
```

### 3.3 All Hook Events

| Event                  | Category          | Matcher Target                            |
| ---------------------- | ----------------- | ----------------------------------------- |
| `sessionStart`         | Session           | --                                        |
| `sessionEnd`           | Session           | --                                        |
| `beforeSubmitPrompt`   | Pre-input         | --                                        |
| `preToolUse`           | Pre-tool          | Tool name (Shell\|Read\|Write\|Task\|MCP) |
| `postToolUse`          | Post-tool         | Tool name                                 |
| `postToolUseFailure`   | Post-tool         | Tool name                                 |
| `beforeShellExecution` | Pre-tool (shell)  | --                                        |
| `afterShellExecution`  | Post-tool (shell) | --                                        |
| `beforeMCPExecution`   | Pre-MCP           | --                                        |
| `afterMCPExecution`    | Post-MCP          | --                                        |
| `beforeReadFile`       | Pre-file          | --                                        |
| `afterFileEdit`        | Post-file         | --                                        |
| `afterAgentResponse`   | Post-response     | --                                        |
| `afterAgentThought`    | Post-thought      | --                                        |
| `subagentStart`        | Subagent          | --                                        |
| `subagentStop`         | Subagent          | --                                        |
| `stop`                 | Lifecycle         | --                                        |
| `preCompact`           | Compaction        | --                                        |
| `beforeTabFileRead`    | Tab (inline)      | --                                        |
| `afterTabFileEdit`     | Tab (inline)      | --                                        |

### 3.4 Key Input/Output Schemas

**Common base (all events):**

```json theme={"dark"}
{
  "conversation_id": "string",
  "generation_id": "string",
  "model": "string",
  "hook_event_name": "string",
  "cursor_version": "string",
  "workspace_roots": ["string"],
  "user_email": "string | null",
  "transcript_path": "string | null"
}
```

**preToolUse input:**

```json theme={"dark"}
{
  "tool_name": "Shell|Read|Write|Task|MCP",
  "tool_input": { "command": "..." },
  "tool_use_id": "string",
  "cwd": "string",
  "agent_message": "string"
}
```

**preToolUse output:**

```json theme={"dark"}
{
  "permission": "allow|deny",
  "user_message": "string",
  "agent_message": "string",
  "updated_input": {}
}
```

**beforeShellExecution input:**

```json theme={"dark"}
{ "command": "string", "cwd": "string", "sandbox": false }
```

**beforeMCPExecution input:**

```json theme={"dark"}
{ "tool_name": "string", "tool_input": "string", "url": "string" | "command": "string" }
```

**beforeSubmitPrompt input:**

```json theme={"dark"}
{ "prompt": "string", "attachments": [{ "type": "file|rule", "file_path": "string" }] }
```

**Permission output (shared across pre-hooks):**

```json theme={"dark"}
{ "permission": "allow|deny|ask", "user_message": "string", "agent_message": "string" }
```

### 3.5 How Cursor Calls External Hooks (Gateway Integration Pattern)

Cursor hooks are **local processes**. To route through the gateway, teams deploy a **thin CLI script** as the hook command that:

1. Reads JSON from stdin
2. POSTs it to `https://gateway.example.com/cursor/hooks/validate`
3. Reads the gateway response
4. Exits with code `0` (allow) or `2` (deny)

This is the bridge pattern our current implementation relies on.

***

## 4. Claude Code Hooks API -- Complete Reference

### 4.1 Protocol

| Aspect             | Detail                                                                                                                                                             |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Transport**      | Four handler types: **command** (stdin/stdout), **http** (POST), **prompt** (LLM eval), **agent** (sub-agent)                                                      |
| **Registration**   | `~/.claude/settings.json` (user), `.claude/settings.json` (project), `.claude/settings.local.json` (local), managed policy (enterprise), plugin `hooks/hooks.json` |
| **Exit codes**     | `0` = success, `2` = block, other = non-blocking error                                                                                                             |
| **Fail behavior**  | Default: fail-open.                                                                                                                                                |
| **Key difference** | Native HTTP hook support -- can POST directly to gateway without a CLI bridge                                                                                      |

### 4.2 Manifest Format (`settings.json`)

```json theme={"dark"}
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Edit|Write",
        "hooks": [
          {
            "type": "command" | "http" | "prompt" | "agent",
            "command": "./script.sh",
            "url": "https://gateway.example.com/hooks/validate",
            "headers": { "Authorization": "Bearer $TFY_API_KEY" },
            "allowedEnvVars": ["TFY_API_KEY"],
            "timeout": 30,
            "if": "Bash(rm *)",
            "statusMessage": "Checking guardrails...",
            "async": false,
            "once": false
          }
        ]
      }
    ]
  }
}
```

### 4.3 All Hook Events

| Event                               | Category   | Matcher Target                                                              |
| ----------------------------------- | ---------- | --------------------------------------------------------------------------- |
| `SessionStart`                      | Session    | Source: startup\|resume\|clear\|compact                                     |
| `SessionEnd`                        | Session    | Reason: clear\|resume\|logout\|...                                          |
| `UserPromptSubmit`                  | Pre-input  | --                                                                          |
| `PreToolUse`                        | Pre-tool   | Tool name (Bash\|Edit\|Write\|Read\|Glob\|Grep\|WebFetch\|Agent\|mcp\_\_\*) |
| `PostToolUse`                       | Post-tool  | Tool name                                                                   |
| `PostToolUseFailure`                | Post-tool  | Tool name                                                                   |
| `PermissionRequest`                 | Permission | Tool name                                                                   |
| `PermissionDenied`                  | Permission | Tool name                                                                   |
| `Stop`                              | Lifecycle  | --                                                                          |
| `StopFailure`                       | Lifecycle  | Error type: rate\_limit\|authentication\_failed                             |
| `SubagentStart`                     | Subagent   | Agent type: Explore\|Bash\|Plan                                             |
| `SubagentStop`                      | Subagent   | Agent type                                                                  |
| `PreCompact` / `PostCompact`        | Compaction | manual\|auto                                                                |
| `Notification`                      | Async      | notification\_type                                                          |
| `WorktreeCreate` / `WorktreeRemove` | Async      | --                                                                          |
| `InstructionsLoaded`                | Async      | File path                                                                   |
| `ConfigChange`                      | Async      | Config source                                                               |
| `CwdChanged`                        | Async      | --                                                                          |
| `FileChanged`                       | Async      | Filename pattern                                                            |
| `TaskCreated` / `TaskCompleted`     | Async      | --                                                                          |
| `TeammateIdle`                      | Async      | --                                                                          |
| `Elicitation` / `ElicitationResult` | MCP        | Server name                                                                 |

### 4.4 Key Input/Output Schemas

**Common base (all events):**

```json theme={"dark"}
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "default|plan|acceptEdits|auto|dontAsk|bypassPermissions",
  "hook_event_name": "string",
  "agent_id": "string",
  "agent_type": "string"
}
```

**PreToolUse input:**

```json theme={"dark"}
{
  "tool_name": "Bash|Edit|Write|Read|Glob|Grep|WebFetch|Agent|mcp__server__tool",
  "tool_input": {
    "command": "npm test",
    "description": "Run tests",
    "timeout": 120000
  },
  "tool_use_id": "toolu_01ABC..."
}
```

**PreToolUse output:**

```json theme={"dark"}
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow|deny|ask|defer",
    "permissionDecisionReason": "string",
    "updatedInput": {},
    "additionalContext": "string"
  }
}
```

**UserPromptSubmit input:**

```json theme={"dark"}
{ "prompt": "user's text" }
```

**UserPromptSubmit output:**

```json theme={"dark"}
{
  "decision": "block|none",
  "reason": "string",
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "string"
  }
}
```

**Universal output fields (all events):**

```json theme={"dark"}
{
  "continue": true,
  "stopReason": "string",
  "suppressOutput": false,
  "systemMessage": "Warning shown to user",
  "decision": "block|none",
  "reason": "string",
  "hookSpecificOutput": { "hookEventName": "...", "...": "..." }
}
```

**Permission decision precedence:** `deny` > `defer` > `ask` > `allow`

### 4.5 HTTP Hook Type (Direct Gateway Integration)

This is the **critical difference** from Cursor. Claude Code can POST directly to our gateway:

```json theme={"dark"}
{
  "type": "http",
  "url": "https://llm-gateway.truefoundry.com/api/llm/hooks/validate",
  "headers": {
    "Authorization": "Bearer $TFY_API_KEY"
  },
  "allowedEnvVars": ["TFY_API_KEY"],
  "timeout": 30
}
```

* Request body = full event JSON sent as POST body
* Response = JSON parsed for decision/output
* Non-2xx = non-blocking error (execution continues)
* Must return 2xx with JSON body containing deny decision to actually block

***

## 5. Side-by-Side Comparison

| Dimension                  | Cursor                                                                                                                                                                          | Claude Code                                                                                                                                                                                                           |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Protocol**               | stdin/stdout (child process only)                                                                                                                                               | stdin/stdout, **HTTP**, prompt, agent                                                                                                                                                                                 |
| **Direct HTTP to gateway** | No (needs CLI bridge script)                                                                                                                                                    | **Yes (native)**                                                                                                                                                                                                      |
| **Config location**        | `.cursor/hooks.json`                                                                                                                                                            | `.claude/settings.json`                                                                                                                                                                                               |
| **Config schema version**  | `{ "version": 1, "hooks": {...} }`                                                                                                                                              | `{ "hooks": {...} }`                                                                                                                                                                                                  |
| **Event naming**           | camelCase (`beforeShellExecution`)                                                                                                                                              | PascalCase (`PreToolUse`)                                                                                                                                                                                             |
| **Permission values**      | `allow\|deny\|ask`                                                                                                                                                              | `allow\|deny\|ask\|defer`                                                                                                                                                                                             |
| **Output structure**       | Flat `{ permission, user_message }`                                                                                                                                             | Nested `{ hookSpecificOutput: { permissionDecision } }`                                                                                                                                                               |
| **Input mutation**         | `updated_input` in preToolUse                                                                                                                                                   | `updatedInput` in hookSpecificOutput                                                                                                                                                                                  |
| **Fail behavior**          | `failClosed` per-hook                                                                                                                                                           | Default fail-open (no per-hook override)                                                                                                                                                                              |
| **Auth in hooks**          | `user_email` env var                                                                                                                                                            | `headers` with env var interpolation                                                                                                                                                                                  |
| **MCP tool naming**        | `metadata.server` + `metadata.tool_name`                                                                                                                                        | `mcp__<server>__<tool>` in tool\_name                                                                                                                                                                                 |
| **Total event types**      | \~20                                                                                                                                                                            | \~30+                                                                                                                                                                                                                 |
| **Shared events**          | preToolUse, postToolUse, beforeShellExecution, afterFileEdit, beforeSubmitPrompt, beforeMCPExecution, afterMCPExecution, sessionStart/End, subagentStart/Stop, preCompact, stop | All of Cursor's + PermissionRequest, PermissionDenied, StopFailure, Notification, WorktreeCreate/Remove, InstructionsLoaded, ConfigChange, FileChanged, TaskCreated/Completed, TeammateIdle, Elicitation, PostCompact |

***

## 6. Engineering Design Decisions

<Info>
  **Decision summary:** We will expose a single **`/guardrail` API** on the AI Gateway that any AI coding assistant hook can invoke. Cursor reaches it via a thin CLI bridge (because Cursor hooks are local processes), and Claude Code reaches it directly using its native HTTP hook type. The endpoint normalizes client-specific event schemas into a shared internal format, runs the existing guardrail engine, and serializes the response back into the format the calling client expects.
</Info>

### Decision 1: Unified `/guardrail` Endpoint (DECIDED)

**Decision: Single unified endpoint** — `POST /api/llm/guardrail` (with the existing `/cursor/hooks/validate` kept as a backwards-compatible alias).

The endpoint accepts events from any supported coding assistant. A thin per-client adapter normalizes the incoming schema, the shared guardrail engine evaluates the event, and a per-client serializer formats the response.

```
┌─────────────┐    ┌──────────────┐    ┌─────────────────────┐
│ Cursor       │───>│ Cursor       │──┐ │                     │
│ CLI bridge   │    │ Adapter      │  │ │  Guardrail Engine   │
└─────────────┘    └──────────────┘  │ │  (HooksManager +    │
                                     ├>│  Plugin System)      │
┌─────────────┐    ┌──────────────┐  │ │                     │
│ Claude Code  │───>│ Claude Code  │──┘ │                     │
│ HTTP hook    │    │ Adapter      │    └──────────┬──────────┘
└─────────────┘    └──────────────┘               │
                                                  v
                                       ┌──────────────────────┐
                                       │  Response Serializer  │
                                       │  (per-client format)  │
                                       └──────────────────────┘
```

**Why a single endpoint:**

* One auth/rate-limit/observability surface to operate.
* Guardrail resolution rules are defined once and apply to every client.
* New clients (Windsurf, Copilot, JetBrains AI, etc.) plug in by adding an adapter — no new public route.
* Cursor and Claude Code teams point their hook config at the same URL; only the request body differs.

**Still open / ambiguous:**

* Do we keep `/cursor/hooks/validate` mounted long-term as an alias, or deprecate it after a migration window?
* Should `/api/llm/guardrail` accept a `client` query/header for unambiguous adapter selection, or rely purely on payload shape sniffing?
* Is the same endpoint also the right place for non-blocking analytics events (`SessionStart`, `Notification`, etc.), or do those go to a separate `/guardrail/events` ingest path?

***

### Decision 2: Claude Code Response Format

The current Cursor response format is flat:

```json theme={"dark"}
{ "continue": true, "permission": "allow", "userMessage": "", "agentMessage": "" }
```

Claude Code expects nested output:

```json theme={"dark"}
{
  "continue": true,
  "decision": "none",
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "additionalContext": ""
  }
}
```

**QUESTION:** Should the Claude Code adapter return the exact Claude Code schema, or can we get away with just the common fields (`continue`, `decision`, `reason`)? Need to test what Claude Code actually requires vs. what it tolerates.

***

### Decision 3: Event Mapping to Guardrail Types

**Current mapping (Cursor):**

| Hook Event                                                     | Guardrail Type                |
| -------------------------------------------------------------- | ----------------------------- |
| `beforeSubmitPrompt`, `beforeShellExecution`, `beforeReadFile` | `llmInputGuardrails`          |
| `afterFileEdit`, `afterAgentResponse`                          | `llmOutputGuardrails`         |
| `beforeMCPExecution`                                           | `mcpToolPreInvokeGuardrails`  |
| `afterMCPExecution`                                            | `mcpToolPostInvokeGuardrails` |

**New events from Claude Code that need mapping:**

| Claude Code Event         | Proposed Guardrail Type       | Notes                                        |
| ------------------------- | ----------------------------- | -------------------------------------------- |
| `UserPromptSubmit`        | `llmInputGuardrails`          | Same as `beforeSubmitPrompt`                 |
| `PreToolUse` (Bash)       | `llmInputGuardrails`          | Same as `beforeShellExecution`               |
| `PreToolUse` (Edit/Write) | `llmInputGuardrails`          | Same as `beforeReadFile`                     |
| `PreToolUse` (mcp\_\_\*)  | `mcpToolPreInvokeGuardrails`  | MCP tool call                                |
| `PostToolUse`             | `llmOutputGuardrails`         | Same as `afterFileEdit`/`afterAgentResponse` |
| `PostToolUse` (mcp\_\_\*) | `mcpToolPostInvokeGuardrails` | MCP tool result                              |
| `Stop`                    | New: `agentStopGuardrails`?   | No current equivalent                        |
| `PermissionRequest`       | New: `permissionGuardrails`?  | Could auto-approve/deny                      |
| `SessionStart`            | Not a guardrail               | Observability/analytics only                 |
| `Notification`            | Not a guardrail               | Observability/analytics only                 |

**QUESTION:** Which Claude Code events do we actually want to run guardrails on? The full list has 30+ events -- most are observability. Should we start with just the blocking events (`UserPromptSubmit`, `PreToolUse`, `PostToolUse`) and treat the rest as analytics passthrough?

***

### Decision 4: Input Extraction from Claude Code Events

The current handler extracts `text` from the request body and wraps it as:

```ts theme={"dark"}
const requestParams = { messages: [{ role: 'user', content: text }], model: cacheIdentifier };
```

Claude Code sends structured tool input, not just text:

```json theme={"dark"}
{
  "tool_name": "Bash",
  "tool_input": { "command": "rm -rf /", "description": "Clean up" }
}
```

**QUESTION:** How should we extract the "text to validate" from Claude Code's structured input?

* Option A: `JSON.stringify(tool_input)` -- simple but loses semantic meaning
* Option B: Extract the primary field per tool type (`command` for Bash, `content` for Write, `file_path` for Read)
* Option C: Send the full structured input to guardrails and let each plugin decide what to inspect

***

### Decision 5: Authentication for Claude Code HTTP Hooks

Claude Code supports header interpolation from env vars:

```json theme={"dark"}
{ "headers": { "Authorization": "Bearer $TFY_API_KEY" }, "allowedEnvVars": ["TFY_API_KEY"] }
```

**QUESTION:** Do we use the existing JWT auth flow (user must have a TFY API key)? Or do we create a lighter-weight hook-specific API key? Considerations:

* JWT flow requires the user to have a TrueFoundry account
* Hook-specific key could be scoped to just guardrail validation
* Enterprise deployment: how does the admin distribute credentials?

***

### Decision 6: Input Mutation Support

Both Cursor and Claude Code support **mutating** the tool input via hook response:

* Cursor: `{ "updated_input": { "command": "safe-version" } }`
* Claude Code: `{ "hookSpecificOutput": { "updatedInput": { "command": "safe-version" } } }`

**QUESTION:** Should our guardrails be able to mutate inputs (e.g., sanitize a shell command, redact PII from a prompt)? This is powerful but risky. Current implementation only does allow/deny.

***

### Decision 7: Streaming / Latency Requirements

Hooks are in the **critical path** of every tool call. If the gateway is slow:

* Cursor: User sees a hang (script blocked on HTTP POST)
* Claude Code: User sees custom `statusMessage` spinner

**QUESTION:** What is the acceptable p99 latency for hook validation?

* Current guardrail execution times?
* Should we implement a fast-path for common cases (e.g., cache allow decisions for repeated tool types)?
* Should we support async guardrails that don't block the agent?

***

### Decision 8: Additional Context Injection

Claude Code hooks can inject `additionalContext` -- text that gets added to the conversation for the LLM to see. This is a powerful feature for:

* Adding security policies inline
* Warning about sensitive operations
* Providing organization-specific instructions

**QUESTION:** Should our gateway be able to inject context (not just allow/deny)? What would the guardrail plugin interface look like for this?

***

### Decision 9: `defer` Permission (Claude Code Only)

Claude Code supports a 4th permission level: `defer` (let Claude decide, but show the user). This is between `allow` and `ask`.

**QUESTION:** Should we support `defer` in our response? This would require guardrails to have a "soft warning" mode in addition to hard block.

***

### Decision 10: Other Clients (Future-Proofing)

Emerging AI coding assistants likely to adopt hooks:

* **Windsurf** (Codeium) -- likely similar to Cursor's local-process model
* **GitHub Copilot** -- may adopt HTTP hooks
* **JetBrains AI** -- may adopt HTTP hooks
* **Zed AI** -- early stage
* **Aider**, **Continue.dev** -- OSS tools

**QUESTION:** How much should we invest in a generic adapter pattern now vs. adding clients one at a time?

***

## 7. Proposed Architecture

All clients call the single decided endpoint — `POST /api/llm/guardrail` — and the gateway picks the right adapter for each.

```
                    ┌─────────────────────────────────────────────┐
                    │           TFY LLM Gateway                   │
                    │                                             │
  Cursor            │                                             │
  (CLI bridge) ────>│ ┌──────────────────────┐                    │
                    │ │                      │                    │
  Claude Code       │ │  POST                │  ┌──────────────┐  │
  (HTTP hook) ─────>│ │  /api/llm/guardrail  │─>│ Hooks Adapter │  │
                    │ │                      │  │ Layer         │  │
  Future Client ───>│ │                      │  │              │  │
                    │ └──────────────────────┘  │ - Normalize   │  │
                    │                           │ - Map events  │  │
                    │                           │ - Extract text│  │
                    │                           └──────┬────────┘  │
                    │                                  v           │
                    │                    ┌──────────────────────┐  │
                    │                    │  Guardrail Engine    │  │
                    │                    │  (HooksManager)      │  │
                    │                    │                      │  │
                    │                    │  - Resolve rules     │  │
                    │                    │  - Execute plugins   │  │
                    │                    │  - Collect verdicts  │  │
                    │                    └────────┬─────────────┘  │
                    │                             │                │
                    │                    ┌────────v─────────────┐  │
                    │                    │  Response Adapter    │  │
                    │                    │  (per-client format) │  │
                    │                    └──────────────────────┘  │
                    └─────────────────────────────────────────────┘
```

### Internal Normalized Event Format

```typescript theme={"dark"}
interface NormalizedHookEvent {
  // Source client
  client: 'cursor' | 'claude-code' | 'generic';

  // Normalized event category
  category: 'input' | 'output' | 'mcp-pre' | 'mcp-post' | 'lifecycle' | 'analytics';

  // Original event name (preserved for logging/debugging)
  originalEventName: string;

  // Content to validate
  content: string;

  // Structured input (for guardrails that need it)
  structuredInput?: Record<string, unknown>;

  // Tool context
  toolName?: string;
  toolType?: 'shell' | 'file-read' | 'file-write' | 'file-edit' | 'mcp' | 'web' | 'agent' | 'other';

  // MCP context
  mcpServer?: string;
  mcpTool?: string;

  // Session context
  sessionId?: string;
  conversationId?: string;

  // User context (from auth middleware)
  user: Session;
  metadata: Record<string, unknown>;
}
```

***

## 8. Implementation Plan

### Phase 1: Claude Code HTTP Hook Endpoint (Week 1-2)

1. Create `src/routers/claudeCodeHooks.ts` and `src/handlers/claudeCodeHooksHandler.ts`
2. Mount at `/claude-code/hooks/validate` and `/api/llm/claude-code/hooks/validate`
3. Handle `PreToolUse`, `PostToolUse`, `UserPromptSubmit` events
4. Parse Claude Code input format, map to guardrail types
5. Return Claude Code response format (`hookSpecificOutput`)
6. Auth via Bearer token header (existing JWT flow)

### Phase 2: Adapter Layer Refactor (Week 2-3)

1. Extract common guardrail resolution logic from `cursorHooksHandler.ts`
2. Create `src/hooks/adapters/cursor.ts` and `src/hooks/adapters/claudeCode.ts`
3. Create `src/hooks/normalizedEvent.ts` for internal format
4. Refactor both handlers to use shared core

### Phase 3: Advanced Features (Week 3-4)

1. Input mutation support (return modified tool inputs)
2. Additional context injection
3. `defer` permission support
4. Analytics passthrough for non-blocking events (SessionStart, Stop, etc.)

### Phase 4: Distribution & Onboarding (Week 4-5)

1. CLI bridge script for Cursor (npm package or downloadable script)
2. Claude Code `.claude/settings.json` template generator
3. TrueFoundry dashboard UI for hooks configuration
4. Documentation and onboarding guide

***

## 9. Open Questions (Sorted by Priority)

### P0 -- Must answer before implementation

| # | Question                                                              | Context                                                                                                                                         |
| - | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | **Which Claude Code events should we run guardrails on?**             | 30+ events exist. Recommend starting with `UserPromptSubmit`, `PreToolUse`, `PostToolUse` only. The rest are analytics.                         |
| 2 | **What is the exact response format Claude Code requires?**           | Need to test: does Claude Code work with just `{ "decision": "block", "reason": "..." }` or does it need the full `hookSpecificOutput` nesting? |
| 3 | **How do we extract "text to validate" from structured tool inputs?** | Bash sends `{ command }`, Write sends `{ content }`, Edit sends `{ old_string, new_string }`. Each tool has different primary content.          |
| 4 | **Do we use existing JWT auth or create hook-specific API keys?**     | Affects onboarding friction. JWT requires TFY account.                                                                                          |

### P1 -- Should answer before Phase 2

| # | Question                                                | Context                                                        |
| - | ------------------------------------------------------- | -------------------------------------------------------------- |
| 5 | **Should we support input mutation?**                   | Powerful but risky. E.g., rewriting a dangerous shell command. |
| 6 | **Should we support `additionalContext` injection?**    | Can inject security policies into the LLM's context window.    |
| 7 | **What is acceptable p99 latency for hook validation?** | Hooks are in the critical path. Every ms matters.              |
| 8 | **Should we support the `defer` permission level?**     | Only Claude Code supports it. Adds complexity.                 |
| 9 | **Unified endpoint or keep client-specific?**           | Affects API surface, routing, and documentation.               |

### P2 -- Can defer to Phase 3+

| #  | Question                                                                                    | Context                                                                                       |
| -- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| 10 | **How do we handle analytics-only events?**                                                 | SessionStart/End, Stop, Notification etc. Log them? Forward to OTEL?                          |
| 11 | **Should we generate `.cursor/hooks.json` and `.claude/settings.json` from the dashboard?** | Better DX but more surface area.                                                              |
| 12 | **How do we distribute the Cursor CLI bridge script?**                                      | npm package? curl-installable? Bundled in TFY CLI?                                            |
| 13 | **Do we need to handle `PermissionRequest` to auto-configure Claude Code permissions?**     | This would let the gateway auto-allow/deny permission prompts.                                |
| 14 | **How do we handle `failClosed` behavior differences?**                                     | Cursor has per-hook failClosed. Claude Code is always fail-open. Should gateway control this? |
| 15 | **Future clients: invest in generic adapter now or add ad-hoc?**                            | Windsurf, GitHub Copilot, JetBrains AI are likely next.                                       |

***

## 10. Appendix: Client Configuration Examples

### A. Cursor Setup (with CLI bridge)

**.cursor/hooks.json:**

```json theme={"dark"}
{
  "version": 1,
  "hooks": {
    "beforeSubmitPrompt": [
      {
        "command": "tfy-hook-bridge",
        "timeout": 10,
        "failClosed": true
      }
    ],
    "beforeShellExecution": [
      {
        "command": "tfy-hook-bridge",
        "timeout": 10,
        "failClosed": true
      }
    ],
    "afterFileEdit": [
      {
        "command": "tfy-hook-bridge",
        "timeout": 10
      }
    ],
    "beforeMCPExecution": [
      {
        "command": "tfy-hook-bridge",
        "timeout": 10,
        "failClosed": true,
        "matcher": ".*"
      }
    ]
  }
}
```

**tfy-hook-bridge script (conceptual):**

```bash theme={"dark"}
#!/bin/bash
INPUT=$(cat)
RESPONSE=$(echo "$INPUT" | curl -s -X POST \
  -H "Authorization: Bearer $TFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- \
  "https://llm-gateway.truefoundry.com/cursor/hooks/validate")

PERMISSION=$(echo "$RESPONSE" | jq -r '.permission')
if [ "$PERMISSION" = "deny" ]; then
  echo "$RESPONSE" >&2
  exit 2
fi
echo "$RESPONSE"
exit 0
```

### B. Claude Code Setup (native HTTP)

**.claude/settings.json:**

```json theme={"dark"}
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "http",
            "url": "https://llm-gateway.truefoundry.com/api/llm/claude-code/hooks/validate",
            "headers": {
              "Authorization": "Bearer ${TFY_API_KEY}"
            },
            "allowedEnvVars": ["TFY_API_KEY"],
            "timeout": 10,
            "statusMessage": "Checking guardrails..."
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash|Edit|Write|mcp__.*",
        "hooks": [
          {
            "type": "http",
            "url": "https://llm-gateway.truefoundry.com/api/llm/claude-code/hooks/validate",
            "headers": {
              "Authorization": "Bearer ${TFY_API_KEY}"
            },
            "allowedEnvVars": ["TFY_API_KEY"],
            "timeout": 10,
            "statusMessage": "Validating tool use..."
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Bash|Edit|Write|mcp__.*",
        "hooks": [
          {
            "type": "http",
            "url": "https://llm-gateway.truefoundry.com/api/llm/claude-code/hooks/validate",
            "headers": {
              "Authorization": "Bearer ${TFY_API_KEY}"
            },
            "allowedEnvVars": ["TFY_API_KEY"],
            "timeout": 10
          }
        ]
      }
    ]
  }
}
```

***

## 11. References

* Cursor Hooks Docs: [https://cursor.com/docs/hooks](https://cursor.com/docs/hooks)
* Claude Code Hooks: [https://docs.anthropic.com/en/docs/claude-code/hooks](https://docs.anthropic.com/en/docs/claude-code/hooks)
* Existing implementation: `src/handlers/cursorHooksHandler.ts`
* Guardrail engine: `src/middlewares/hooks/`
* Plugin system: `src/plugins/`
