wip: [01-stabilize] paused at task 1/1 - OCR Hallucination Immune logic via Semantic delta window and fret-isolation

This commit is contained in:
2026-03-29 22:08:40 +09:00
parent aca7bf592a
commit 2507de45d3
4289 changed files with 732689 additions and 28672 deletions

View File

@@ -0,0 +1,128 @@
/**
* Claude Agent SDK V2 Examples
*
* The V2 API provides a session-based interface with separate send()/receive(),
* ideal for multi-turn conversations. Run with: npx tsx v2-examples.ts
*/
import {
unstable_v2_createSession,
unstable_v2_resumeSession,
unstable_v2_prompt,
} from '@anthropic-ai/claude-agent-sdk';
async function main() {
const example = process.argv[2] || 'basic';
switch (example) {
case 'basic':
await basicSession();
break;
case 'multi-turn':
await multiTurn();
break;
case 'one-shot':
await oneShot();
break;
case 'resume':
await sessionResume();
break;
default:
console.log('Usage: npx tsx v2-examples.ts [basic|multi-turn|one-shot|resume]');
}
}
// Basic session with send/receive pattern
async function basicSession() {
console.log('=== Basic Session ===\n');
await using session = unstable_v2_createSession({ model: 'sonnet' });
await session.send('Hello! Introduce yourself in one sentence.');
for await (const msg of session.receive()) {
if (msg.type === 'assistant') {
const text = msg.message.content.find((c): c is { type: 'text'; text: string } => c.type === 'text');
console.log(`Claude: ${text?.text}`);
}
}
}
// Multi-turn conversation - V2's key advantage
async function multiTurn() {
console.log('=== Multi-Turn Conversation ===\n');
await using session = unstable_v2_createSession({ model: 'sonnet' });
// Turn 1
await session.send('What is 5 + 3? Just the number.');
for await (const msg of session.receive()) {
if (msg.type === 'assistant') {
const text = msg.message.content.find((c): c is { type: 'text'; text: string } => c.type === 'text');
console.log(`Turn 1: ${text?.text}`);
}
}
// Turn 2 - Claude remembers context
await session.send('Multiply that by 2. Just the number.');
for await (const msg of session.receive()) {
if (msg.type === 'assistant') {
const text = msg.message.content.find((c): c is { type: 'text'; text: string } => c.type === 'text');
console.log(`Turn 2: ${text?.text}`);
}
}
}
// One-shot convenience function
async function oneShot() {
console.log('=== One-Shot Prompt ===\n');
const result = await unstable_v2_prompt('What is the capital of France? One word.', { model: 'sonnet' });
if (result.subtype === 'success') {
console.log(`Answer: ${result.result}`);
console.log(`Cost: $${result.total_cost_usd.toFixed(4)}`);
}
}
// Session resume - persist context across sessions
async function sessionResume() {
console.log('=== Session Resume ===\n');
let sessionId: string | undefined;
// First session - establish a memory
{
await using session = unstable_v2_createSession({ model: 'sonnet' });
console.log('[Session 1] Telling Claude my favorite color...');
await session.send('My favorite color is blue. Remember this!');
for await (const msg of session.receive()) {
if (msg.type === 'system' && msg.subtype === 'init') {
sessionId = msg.session_id;
console.log(`[Session 1] ID: ${sessionId}`);
}
if (msg.type === 'assistant') {
const text = msg.message.content.find((c): c is { type: 'text'; text: string } => c.type === 'text');
console.log(`[Session 1] Claude: ${text?.text}\n`);
}
}
}
console.log('--- Session closed. Time passes... ---\n');
// Resume and verify Claude remembers
{
await using session = unstable_v2_resumeSession(sessionId!, { model: 'sonnet' });
console.log('[Session 2] Resuming and asking Claude...');
await session.send('What is my favorite color?');
for await (const msg of session.receive()) {
if (msg.type === 'assistant') {
const text = msg.message.content.find((c): c is { type: 'text'; text: string } => c.type === 'text');
console.log(`[Session 2] Claude: ${text?.text}`);
}
}
}
}
main().catch(console.error);

View File

@@ -0,0 +1,390 @@
# TypeScript SDK V2 interface (preview)
Preview of the simplified V2 TypeScript Agent SDK, with session-based send/receive patterns for multi-turn conversations.
---
<Warning>
The V2 interface is an **unstable preview**. APIs may change based on feedback before becoming stable. Some features like session forking are only available in the [V1 SDK](/docs/en/agent-sdk/typescript).
</Warning>
The V2 Claude Agent TypeScript SDK removes the need for async generators and yield coordination. This makes multi-turn conversations simpler—instead of managing generator state across turns, each turn is a separate `send()`/`receive()` cycle. The API surface reduces to three concepts:
- `createSession()` / `resumeSession()`: Start or continue a conversation
- `session.send()`: Send a message
- `session.receive()`: Get the response
## Installation
The V2 interface is included in the existing SDK package:
```bash
npm install @anthropic-ai/claude-agent-sdk
```
## Quick start
### One-shot prompt
For simple single-turn queries where you don't need to maintain a session, use `unstable_v2_prompt()`. This example sends a math question and logs the answer:
```typescript
import { unstable_v2_prompt } from '@anthropic-ai/claude-agent-sdk'
const result = await unstable_v2_prompt('What is 2 + 2?', {
model: 'claude-sonnet-4-5-20250929'
})
console.log(result.result)
```
<details>
<summary>See the same operation in V1</summary>
```typescript
import { query } from '@anthropic-ai/claude-agent-sdk'
const q = query({
prompt: 'What is 2 + 2?',
options: { model: 'claude-sonnet-4-5-20250929' }
})
for await (const msg of q) {
if (msg.type === 'result') {
console.log(msg.result)
}
}
```
</details>
### Basic session
For interactions beyond a single prompt, create a session. V2 separates sending and receiving into distinct steps:
- `send()` dispatches your message
- `receive()` streams back the response
This explicit separation makes it easier to add logic between turns (like processing responses before sending follow-ups).
The example below creates a session, sends "Hello!" to Claude, and prints the text response. It uses [`await using`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#using-declarations-and-explicit-resource-management) (TypeScript 5.2+) to automatically close the session when the block exits. You can also call `session.close()` manually.
```typescript
import { unstable_v2_createSession } from '@anthropic-ai/claude-agent-sdk'
await using session = unstable_v2_createSession({
model: 'claude-sonnet-4-5-20250929'
})
await session.send('Hello!')
for await (const msg of session.receive()) {
// Filter for assistant messages to get human-readable output
if (msg.type === 'assistant') {
const text = msg.message.content
.filter(block => block.type === 'text')
.map(block => block.text)
.join('')
console.log(text)
}
}
```
<details>
<summary>See the same operation in V1</summary>
In V1, both input and output flow through a single async generator. For a basic prompt this looks similar, but adding multi-turn logic requires restructuring to use an input generator.
```typescript
import { query } from '@anthropic-ai/claude-agent-sdk'
const q = query({
prompt: 'Hello!',
options: { model: 'claude-sonnet-4-5-20250929' }
})
for await (const msg of q) {
if (msg.type === 'assistant') {
const text = msg.message.content
.filter(block => block.type === 'text')
.map(block => block.text)
.join('')
console.log(text)
}
}
```
</details>
### Multi-turn conversation
Sessions persist context across multiple exchanges. To continue a conversation, call `send()` again on the same session. Claude remembers the previous turns.
This example asks a math question, then asks a follow-up that references the previous answer:
```typescript
import { unstable_v2_createSession } from '@anthropic-ai/claude-agent-sdk'
await using session = unstable_v2_createSession({
model: 'claude-sonnet-4-5-20250929'
})
// Turn 1
await session.send('What is 5 + 3?')
for await (const msg of session.receive()) {
// Filter for assistant messages to get human-readable output
if (msg.type === 'assistant') {
const text = msg.message.content
.filter(block => block.type === 'text')
.map(block => block.text)
.join('')
console.log(text)
}
}
// Turn 2
await session.send('Multiply that by 2')
for await (const msg of session.receive()) {
if (msg.type === 'assistant') {
const text = msg.message.content
.filter(block => block.type === 'text')
.map(block => block.text)
.join('')
console.log(text)
}
}
```
<details>
<summary>See the same operation in V1</summary>
```typescript
import { query } from '@anthropic-ai/claude-agent-sdk'
// Must create an async iterable to feed messages
async function* createInputStream() {
yield {
type: 'user',
session_id: '',
message: { role: 'user', content: [{ type: 'text', text: 'What is 5 + 3?' }] },
parent_tool_use_id: null
}
// Must coordinate when to yield next message
yield {
type: 'user',
session_id: '',
message: { role: 'user', content: [{ type: 'text', text: 'Multiply by 2' }] },
parent_tool_use_id: null
}
}
const q = query({
prompt: createInputStream(),
options: { model: 'claude-sonnet-4-5-20250929' }
})
for await (const msg of q) {
if (msg.type === 'assistant') {
const text = msg.message.content
.filter(block => block.type === 'text')
.map(block => block.text)
.join('')
console.log(text)
}
}
```
</details>
### Session resume
If you have a session ID from a previous interaction, you can resume it later. This is useful for long-running workflows or when you need to persist conversations across application restarts.
This example creates a session, stores its ID, closes it, then resumes the conversation:
```typescript
import {
unstable_v2_createSession,
unstable_v2_resumeSession,
type SDKMessage
} from '@anthropic-ai/claude-agent-sdk'
// Helper to extract text from assistant messages
function getAssistantText(msg: SDKMessage): string | null {
if (msg.type !== 'assistant') return null
return msg.message.content
.filter(block => block.type === 'text')
.map(block => block.text)
.join('')
}
// Create initial session and have a conversation
const session = unstable_v2_createSession({
model: 'claude-sonnet-4-5-20250929'
})
await session.send('Remember this number: 42')
// Get the session ID from any received message
let sessionId: string | undefined
for await (const msg of session.receive()) {
sessionId = msg.session_id
const text = getAssistantText(msg)
if (text) console.log('Initial response:', text)
}
console.log('Session ID:', sessionId)
session.close()
// Later: resume the session using the stored ID
await using resumedSession = unstable_v2_resumeSession(sessionId!, {
model: 'claude-sonnet-4-5-20250929'
})
await resumedSession.send('What number did I ask you to remember?')
for await (const msg of resumedSession.receive()) {
const text = getAssistantText(msg)
if (text) console.log('Resumed response:', text)
}
```
<details>
<summary>See the same operation in V1</summary>
```typescript
import { query } from '@anthropic-ai/claude-agent-sdk'
// Create initial session
const initialQuery = query({
prompt: 'Remember this number: 42',
options: { model: 'claude-sonnet-4-5-20250929' }
})
// Get session ID from any message
let sessionId: string | undefined
for await (const msg of initialQuery) {
sessionId = msg.session_id
if (msg.type === 'assistant') {
const text = msg.message.content
.filter(block => block.type === 'text')
.map(block => block.text)
.join('')
console.log('Initial response:', text)
}
}
console.log('Session ID:', sessionId)
// Later: resume the session
const resumedQuery = query({
prompt: 'What number did I ask you to remember?',
options: {
model: 'claude-sonnet-4-5-20250929',
resume: sessionId
}
})
for await (const msg of resumedQuery) {
if (msg.type === 'assistant') {
const text = msg.message.content
.filter(block => block.type === 'text')
.map(block => block.text)
.join('')
console.log('Resumed response:', text)
}
}
```
</details>
### Cleanup
Sessions can be closed manually or automatically using [`await using`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#using-declarations-and-explicit-resource-management), a TypeScript 5.2+ feature for automatic resource cleanup. If you're using an older TypeScript version or encounter compatibility issues, use manual cleanup instead.
**Automatic cleanup (TypeScript 5.2+):**
```typescript
import { unstable_v2_createSession } from '@anthropic-ai/claude-agent-sdk'
await using session = unstable_v2_createSession({
model: 'claude-sonnet-4-5-20250929'
})
// Session closes automatically when the block exits
```
**Manual cleanup:**
```typescript
import { unstable_v2_createSession } from '@anthropic-ai/claude-agent-sdk'
const session = unstable_v2_createSession({
model: 'claude-sonnet-4-5-20250929'
})
// ... use the session ...
session.close()
```
## API reference
### `unstable_v2_createSession()`
Creates a new session for multi-turn conversations.
```typescript
function unstable_v2_createSession(options: {
model: string;
// Additional options supported
}): Session
```
### `unstable_v2_resumeSession()`
Resumes an existing session by ID.
```typescript
function unstable_v2_resumeSession(
sessionId: string,
options: {
model: string;
// Additional options supported
}
): Session
```
### `unstable_v2_prompt()`
One-shot convenience function for single-turn queries.
```typescript
function unstable_v2_prompt(
prompt: string,
options: {
model: string;
// Additional options supported
}
): Promise<Result>
```
### Session interface
```typescript
interface Session {
send(message: string): Promise<void>;
receive(): AsyncGenerator<SDKMessage>;
close(): void;
}
```
## Feature availability
Not all V1 features are available in V2 yet. The following require using the [V1 SDK](/docs/en/agent-sdk/typescript):
- Session forking (`forkSession` option)
- Some advanced streaming input patterns
## Feedback
Share your feedback on the V2 interface before it becomes stable. Report issues and suggestions through [GitHub Issues](https://github.com/anthropics/claude-code/issues).
## See also
- [TypeScript SDK reference (V1)](/docs/en/agent-sdk/typescript) - Full V1 SDK documentation
- [SDK overview](/docs/en/agent-sdk/overview) - General SDK concepts
- [V2 examples on GitHub](https://github.com/anthropics/claude-agent-sdk-demos/tree/main/hello-world-v2) - Working code examples

View File

@@ -0,0 +1,586 @@
# Hooks
Hooks let you observe, control, and extend the agent loop using custom scripts. Hooks are spawned processes that communicate over stdio using JSON in both directions. They run before or after defined stages of the agent loop and can observe, block, or modify behavior.
With hooks, you can:
- Run formatters after edits
- Add analytics for events
- Scan for PII or secrets
- Gate risky operations (e.g., SQL writes)
<Tip>
Looking for ready-to-use integrations? See [Partner Integrations](#partner-integrations) for security, governance, and secrets management solutions from our ecosystem partners.
</Tip>
## Agent and Tab Support
Hooks work with both **Cursor Agent** (Cmd+K/Agent Chat) and **Cursor Tab** (inline completions), but they use different hook events:
**Agent (Cmd+K/Agent Chat)** uses the standard hooks:
- `beforeShellExecution` / `afterShellExecution` - Control shell commands
- `beforeMCPExecution` / `afterMCPExecution` - Control MCP tool usage
- `beforeReadFile` / `afterFileEdit` - Control file access and edits
- `beforeSubmitPrompt` - Validate prompts before submission
- `stop` - Handle agent completion
- `afterAgentResponse` / `afterAgentThought` - Track agent responses
**Tab (inline completions)** uses specialized hooks:
- `beforeTabFileRead` - Control file access for Tab completions
- `afterTabFileEdit` - Post-process Tab edits
These separate hooks allow different policies for autonomous Tab operations versus user-directed Agent operations.
## Quickstart
Create a `hooks.json` file. You can create it at the project level (`<project>/.cursor/hooks.json`) or in your home directory (`~/.cursor/hooks.json`). Project-level hooks apply only to that specific project, while home directory hooks apply globally.
```json
{
"version": 1,
"hooks": {
"afterFileEdit": [{ "command": "./hooks/format.sh" }]
}
}
```
Create your hook script at `~/.cursor/hooks/format.sh`:
```bash
#!/bin/bash
# Read input, do something, exit 0
cat > /dev/null
exit 0
```
Make it executable:
```bash
chmod +x ~/.cursor/hooks/format.sh
```
Restart Cursor. Your hook now runs after every file edit.
## Examples
<CodeGroup>
```json title="hooks.json"
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{
"command": "./hooks/audit.sh"
},
{
"command": "./hooks/block-git.sh"
}
],
"beforeMCPExecution": [
{
"command": "./hooks/audit.sh"
}
],
"afterShellExecution": [
{
"command": "./hooks/audit.sh"
}
],
"afterMCPExecution": [
{
"command": "./hooks/audit.sh"
}
],
"afterFileEdit": [
{
"command": "./hooks/audit.sh"
}
],
"beforeSubmitPrompt": [
{
"command": "./hooks/audit.sh"
}
],
"stop": [
{
"command": "./hooks/audit.sh"
}
],
"beforeTabFileRead": [
{
"command": "./hooks/redact-secrets-tab.sh"
}
],
"afterTabFileEdit": [
{
"command": "./hooks/format-tab.sh"
}
]
}
}
```
```sh title="audit.sh"
#!/bin/bash
# audit.sh - Hook script that writes all JSON input to /tmp/agent-audit.log
# This script is designed to be called by Cursor's hooks system for auditing purposes
# Read JSON input from stdin
json_input=$(cat)
# Create timestamp for the log entry
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# Create the log directory if it doesn't exist
mkdir -p "$(dirname /tmp/agent-audit.log)"
# Write the timestamped JSON entry to the audit log
echo "[$timestamp] $json_input" >> /tmp/agent-audit.log
# Exit successfully
exit 0
```
```sh title="block-git.sh"
#!/bin/bash
# Hook to block git commands and redirect to gh tool usage
# This hook implements the beforeShellExecution hook from the Cursor Hooks Spec
# Initialize debug logging
echo "Hook execution started" >> /tmp/hooks.log
# Read JSON input from stdin
input=$(cat)
echo "Received input: $input" >> /tmp/hooks.log
# Parse the command from the JSON input
command=$(echo "$input" | jq -r '.command // empty')
echo "Parsed command: '$command'" >> /tmp/hooks.log
# Check if the command contains 'git' or 'gh'
if [[ "$command" =~ git[[:space:]] ]] || [[ "$command" == "git" ]]; then
echo "Git command detected - blocking: '$command'" >> /tmp/hooks.log
# Block the git command and provide guidance to use gh tool instead
cat << EOF
{
"continue": true,
"permission": "deny",
"user_message": "Git command blocked. Please use the GitHub CLI (gh) tool instead.",
"agent_message": "The git command '$command' has been blocked by a hook. Instead of using raw git commands, please use the 'gh' tool which provides better integration with GitHub and follows best practices. For example:\n- Instead of 'git clone', use 'gh repo clone'\n- Instead of 'git push', use 'gh repo sync' or the appropriate gh command\n- For other git operations, check if there's an equivalent gh command or use the GitHub web interface\n\nThis helps maintain consistency and leverages GitHub's enhanced tooling."
}
EOF
elif [[ "$command" =~ gh[[:space:]] ]] || [[ "$command" == "gh" ]]; then
echo "GitHub CLI command detected - asking for permission: '$command'" >> /tmp/hooks.log
# Ask for permission for gh commands
cat << EOF
{
"continue": true,
"permission": "ask",
"user_message": "GitHub CLI command requires permission: $command",
"agent_message": "The command '$command' uses the GitHub CLI (gh) which can interact with your GitHub repositories and account. Please review and approve this command if you want to proceed."
}
EOF
else
echo "Non-git/non-gh command detected - allowing: '$command'" >> /tmp/hooks.log
# Allow non-git/non-gh commands
cat << EOF
{
"continue": true,
"permission": "allow"
}
EOF
fi
```
</CodeGroup>
## Partner Integrations
We partner with ecosystem vendors who have built hooks support with Cursor. These integrations cover security scanning, governance, secrets management, and more.
### MCP governance and visibility
| Partner | Description |
|---------|-------------|
| [MintMCP](https://www.mintmcp.com/blog/mcp-governance-cursor-hooks) | Build a complete inventory of MCP servers, monitor tool usage patterns, and scan responses for sensitive data before it reaches the AI model. |
| [Oasis Security](https://www.oasis.security/blog/cursor-oasis-governing-agentic-access) | Enforce least-privilege policies on AI agent actions and maintain full audit trails across enterprise systems. |
| [Runlayer](https://www.runlayer.com/blog/cursor-hooks) | Wrap MCP tools and integrate with their MCP broker for centralized control and visibility over agent-to-tool interactions. |
### Code security and best practices
| Partner | Description |
|---------|-------------|
| [Corridor](https://corridor.dev/blog/corridor-cursor-hooks/) | Get real-time feedback on code implementation and security design decisions as code is being written. |
| [Semgrep](https://semgrep.dev/blog/2025/cursor-hooks-mcp-server) | Automatically scan AI-generated code for vulnerabilities with real-time feedback to regenerate code until security issues are resolved. |
### Dependency security
| Partner | Description |
|---------|-------------|
| [Endor Labs](https://www.endorlabs.com/learn/bringing-malware-detection-into-ai-coding-workflows-with-cursor-hooks) | Intercept package installations and scan for malicious dependencies, preventing supply chain attacks before they enter your codebase. |
### Agent security and safety
| Partner | Description |
|---------|-------------|
| [Snyk](https://snyk.io/blog/evo-agent-guard-cursor-integration/) | Review agent actions in real-time with Evo Agent Guard, detecting and preventing issues like prompt injection and dangerous tool calls. |
### Secrets management
| Partner | Description |
|---------|-------------|
| [1Password](https://marketplace.1password.com/integration/cursor-hooks) | Validate that environment files from 1Password Environments are properly mounted before shell commands execute, enabling just-in-time secrets access without writing credentials to disk. |
For more details about our hooks partners, see the [Hooks for security and platform teams](/blog/hooks-partners) blog post.
## Configuration
Define hooks in a `hooks.json` file. Configuration can exist at multiple levels; higher-priority sources override lower ones:
```sh
~/.cursor/
├── hooks.json
└── hooks/
├── audit.sh
└── block-git.sh
```
- **Global** (Enterprise-managed):
- macOS: `/Library/Application Support/Cursor/hooks.json`
- Linux/WSL: `/etc/cursor/hooks.json`
- Windows: `C:\\ProgramData\\Cursor\\hooks.json`
- **Project Directory** (Project-specific):
- `<project-root>/.cursor/hooks.json`
- Project hooks run in any trusted workspace and are checked into version control with your project
- **Home Directory** (User-specific):
- `~/.cursor/hooks.json`
Priority order (highest to lowest): Enterprise → Project → User
The `hooks` object maps hook names to arrays of hook definitions. Each definition currently supports a `command` property that can be a shell string, an absolute path, or a path relative to the `hooks.json` file.
### Configuration file
```json
{
"version": 1,
"hooks": {
"beforeShellExecution": [{ "command": "./script.sh" }],
"afterShellExecution": [{ "command": "./script.sh" }],
"afterMCPExecution": [{ "command": "./script.sh" }],
"afterFileEdit": [{ "command": "./format.sh" }],
"beforeTabFileRead": [{ "command": "./redact-secrets-tab.sh" }],
"afterTabFileEdit": [{ "command": "./format-tab.sh" }]
}
}
```
The Agent hooks (`beforeShellExecution`, `afterShellExecution`, `beforeMCPExecution`, `afterMCPExecution`, `beforeReadFile`, `afterFileEdit`, `beforeSubmitPrompt`, `stop`, `afterAgentResponse`, `afterAgentThought`) apply to Cmd+K and Agent Chat operations. The Tab hooks (`beforeTabFileRead`, `afterTabFileEdit`) apply specifically to inline Tab completions.
## Team Distribution
Hooks can be distributed to team members using project hooks (via version control), MDM tools, or Cursor's cloud distribution system.
### Project Hooks (Version Control)
Project hooks are the simplest way to share hooks with your team. Place a `hooks.json` file at `<project-root>/.cursor/hooks.json` and commit it to your repository. When team members open the project in a trusted workspace, Cursor automatically loads and runs the project hooks.
Project hooks:
- Are stored in version control alongside your code
- Automatically load for all team members in trusted workspaces
- Can be project-specific (e.g., enforce formatting standards for a particular codebase)
- Require the workspace to be trusted to run (for security)
### MDM Distribution
Distribute hooks across your organization using Mobile Device Management (MDM) tools. Place the `hooks.json` file and hook scripts in the target directories on each machine.
**User home directory** (per-user distribution):
- `~/.cursor/hooks.json`
- `~/.cursor/hooks/` (for hook scripts)
**Global directories** (system-wide distribution):
- macOS: `/Library/Application Support/Cursor/hooks.json`
- Linux/WSL: `/etc/cursor/hooks.json`
- Windows: `C:\\ProgramData\\Cursor\\hooks.json`
Note: MDM-based distribution is fully managed by your organization. Cursor does not deploy or manage files through your MDM solution. Ensure your internal IT or security team handles configuration, deployment, and updates in accordance with your organization's policies.
### Cloud Distribution (Enterprise Only)
Enterprise teams can use Cursor's native cloud distribution to automatically sync hooks to all team members. Configure hooks in the [web dashboard](https://cursor.com/dashboard?tab=team-content&section=hooks). Cursor automatically delivers configured hooks to all client machines when team members log in.
Cloud distribution provides:
- Automatic synchronization to all team members (every thirty minutes)
- Operating system targeting for platform-specific hooks
- Centralized management through the dashboard
Enterprise administrators can create, edit, and manage team hooks from the dashboard without requiring access to individual machines.
## Reference
### Common schema
#### Input (all hooks)
All hooks receive a base set of fields in addition to their hook-specific fields:
```json
{
"conversation_id": "string",
"generation_id": "string",
"model": "string",
"hook_event_name": "string",
"cursor_version": "string",
"workspace_roots": ["<path>"],
"user_email": "string | null"
}
```
| Field | Type | Description |
|-------|------|-------------|
| `conversation_id` | string | Stable ID of the conversation across many turns |
| `generation_id` | string | The current generation that changes with every user message |
| `model` | string | The model configured for the composer that triggered the hook |
| `hook_event_name` | string | Which hook is being run |
| `cursor_version` | string | Cursor application version (e.g. "1.7.2") |
| `workspace_roots` | string[] | The list of root folders in the workspace (normally just one, but multiroot workspaces can have multiple) |
| `user_email` | string \| null | Email address of the authenticated user, if available |
### Hook events
#### beforeShellExecution / beforeMCPExecution
Called before any shell command or MCP tool is executed. Return a permission decision.
```json
// beforeShellExecution input
{
"command": "<full terminal command>",
"cwd": "<current working directory>"
}
// beforeMCPExecution input
{
"tool_name": "<tool name>",
"tool_input": "<json params>"
}
// Plus either:
{ "url": "<server url>" }
// Or:
{ "command": "<command string>" }
// Output
{
"permission": "allow" | "deny" | "ask",
"user_message": "<message shown in client>",
"agent_message": "<message sent to agent>"
}
```
#### afterShellExecution
Fires after a shell command executes; useful for auditing or collecting metrics from command output.
```json
// Input
{
"command": "<full terminal command>",
"output": "<full terminal output>",
"duration": 1234
}
```
| Field | Type | Description |
|-------|------|-------------|
| `command` | string | The full terminal command that was executed |
| `output` | string | Full output captured from the terminal |
| `duration` | number | Duration in milliseconds spent executing the shell command (excludes approval wait time) |
#### afterMCPExecution
Fires after an MCP tool executes; includes the tool's input parameters and full JSON result.
```json
// Input
{
"tool_name": "<tool name>",
"tool_input": "<json params>",
"result_json": "<tool result json>",
"duration": 1234
}
```
| Field | Type | Description |
|-------|------|-------------|
| `tool_name` | string | Name of the MCP tool that was executed |
| `tool_input` | string | JSON params string passed to the tool |
| `result_json` | string | JSON string of the tool response |
| `duration` | number | Duration in milliseconds spent executing the MCP tool (excludes approval wait time) |
#### afterFileEdit
Fires after the Agent edits a file; useful for formatters or accounting of agent-written code.
```json
// Input
{
"file_path": "<absolute path>",
"edits": [{ "old_string": "<search>", "new_string": "<replace>" }]
}
```
#### beforeTabFileRead
Called before Tab (inline completions) reads a file. Enable redaction or access control before Tab accesses file contents.
**Key differences from `beforeReadFile`:**
- Only triggered by Tab, not Agent
- Does not include `attachments` field (Tab doesn't use prompt attachments)
- Useful for applying different policies to autonomous Tab operations
```json
// Input
{
"file_path": "<absolute path>",
"content": "<file contents>"
}
// Output
{
"permission": "allow" | "deny"
}
```
#### afterTabFileEdit
Called after Tab (inline completions) edits a file. Useful for formatters or auditing of Tab-written code.
**Key differences from `afterFileEdit`:**
- Only triggered by Tab, not Agent
- Includes detailed edit information: `range`, `old_line`, and `new_line` for precise edit tracking
- Useful for fine-grained formatting or analysis of Tab edits
```json
// Input
{
"file_path": "<absolute path>",
"edits": [
{
"old_string": "<search>",
"new_string": "<replace>",
"range": {
"start_line_number": 10,
"start_column": 5,
"end_line_number": 10,
"end_column": 20
},
"old_line": "<line before edit>",
"new_line": "<line after edit>"
}
]
}
// Output
{
// No output fields currently supported
}
```
#### beforeSubmitPrompt
Called right after user hits send but before backend request. Can prevent submission.
```json
// Input
{
"prompt": "<user prompt text>",
"attachments": [
{
"type": "file" | "rule",
"filePath": "<absolute path>"
}
]
}
// Output
{
"continue": true | false,
"user_message": "<message shown to user when blocked>"
}
```
| Output Field | Type | Description |
|--------------|------|-------------|
| `continue` | boolean | Whether to allow the prompt submission to proceed |
| `user_message` | string (optional) | Message shown to the user when the prompt is blocked |
#### afterAgentResponse
Called after the agent has completed an assistant message.
```json
// Input
{
"text": "<assistant final text>"
}
```
#### afterAgentThought
Called after the agent completes a thinking block. Useful for observing the agent's reasoning process.
```json
// Input
{
"text": "<fully aggregated thinking text>",
"duration_ms": 5000
}
// Output
{
// No output fields currently supported
}
```
| Field | Type | Description |
|-------|------|-------------|
| `text` | string | Fully aggregated thinking text for the completed block |
| `duration_ms` | number (optional) | Duration in milliseconds for the thinking block |
#### stop
Called when the agent loop ends. Can optionally auto-submit a follow-up user message to keep iterating.
```json
// Input
{
"status": "completed" | "aborted" | "error",
"loop_count": 0
}
```
```json
// Output
{
"followup_message": "<message text>"
}
```
- The optional `followup_message` is a string. When provided and non-empty, Cursor will automatically submit it as the next user message. This enables loop-style flows (e.g., iterate until a goal is met).
- The `loop_count` field indicates how many times the stop hook has already triggered an automatic follow-up for this conversation (starts at 0). To prevent infinite loops, a maximum of 5 auto follow-ups is enforced.
## Troubleshooting
**How to confirm hooks are active**
There is a Hooks tab in Cursor Settings to debug configured and executed hooks, as well as a Hooks output channel to see errors.
**If hooks are not working**
- Restart Cursor to ensure the hooks service is running.
- Ensure hook script paths are relative to `hooks.json` when using relative paths.

View File

@@ -0,0 +1,338 @@
# Get started with Claude Code hooks
> Learn how to customize and extend Claude Code's behavior by registering shell commands
Claude Code hooks are user-defined shell commands that execute at various points
in Claude Code's lifecycle. Hooks provide deterministic control over Claude
Code's behavior, ensuring certain actions always happen rather than relying on
the LLM to choose to run them.
<Tip>
For reference documentation on hooks, see [Hooks reference](/en/hooks).
</Tip>
Example use cases for hooks include:
* **Notifications**: Customize how you get notified when Claude Code is awaiting
your input or permission to run something.
* **Automatic formatting**: Run `prettier` on .ts files, `gofmt` on .go files,
etc. after every file edit.
* **Logging**: Track and count all executed commands for compliance or
debugging.
* **Feedback**: Provide automated feedback when Claude Code produces code that
does not follow your codebase conventions.
* **Custom permissions**: Block modifications to production files or sensitive
directories.
By encoding these rules as hooks rather than prompting instructions, you turn
suggestions into app-level code that executes every time it is expected to run.
<Warning>
You must consider the security implication of hooks as you add them, because hooks run automatically during the agent loop with your current environment's credentials.
For example, malicious hooks code can exfiltrate your data. Always review your hooks implementation before registering them.
For full security best practices, see [Security Considerations](/en/hooks#security-considerations) in the hooks reference documentation.
</Warning>
## Hook Events Overview
Claude Code provides several hook events that run at different points in the
workflow:
* **PreToolUse**: Runs before tool calls (can block them)
* **PermissionRequest**: Runs when a permission dialog is shown (can allow or deny)
* **PostToolUse**: Runs after tool calls complete
* **UserPromptSubmit**: Runs when the user submits a prompt, before Claude processes it
* **Notification**: Runs when Claude Code sends notifications
* **Stop**: Runs when Claude Code finishes responding
* **SubagentStop**: Runs when subagent tasks complete
* **PreCompact**: Runs before Claude Code is about to run a compact operation
* **SessionStart**: Runs when Claude Code starts a new session or resumes an existing session
* **SessionEnd**: Runs when Claude Code session ends
Each event receives different data and can control Claude's behavior in
different ways.
## Quickstart
In this quickstart, you'll add a hook that logs the shell commands that Claude
Code runs.
### Prerequisites
Install `jq` for JSON processing in the command line.
### Step 1: Open hooks configuration
Run the `/hooks` [slash command](/en/slash-commands) and select
the `PreToolUse` hook event.
`PreToolUse` hooks run before tool calls and can block them while providing
Claude feedback on what to do differently.
### Step 2: Add a matcher
Select `+ Add new matcher…` to run your hook only on Bash tool calls.
Type `Bash` for the matcher.
<Note>You can use `*` to match all tools.</Note>
### Step 3: Add the hook
Select `+ Add new hook…` and enter this command:
```bash theme={null}
jq -r '"\(.tool_input.command) - \(.tool_input.description // "No description")"' >> ~/.claude/bash-command-log.txt
```
### Step 4: Save your configuration
For storage location, select `User settings` since you're logging to your home
directory. This hook will then apply to all projects, not just your current
project.
Then press `Esc` until you return to the REPL. Your hook is now registered.
### Step 5: Verify your hook
Run `/hooks` again or check `~/.claude/settings.json` to see your configuration:
```json theme={null}
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '\"\\(.tool_input.command) - \\(.tool_input.description // \"No description\")\"' >> ~/.claude/bash-command-log.txt"
}
]
}
]
}
}
```
### Step 6: Test your hook
Ask Claude to run a simple command like `ls` and check your log file:
```bash theme={null}
cat ~/.claude/bash-command-log.txt
```
You should see entries like:
```
ls - Lists files and directories
```
## More Examples
<Note>
For a complete example implementation, see the [bash command validator example](https://github.com/anthropics/claude-code/blob/main/examples/hooks/bash_command_validator_example.py) in our public codebase.
</Note>
### Code Formatting Hook
Automatically format TypeScript files after editing:
```json theme={null}
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | { read file_path; if echo \"$file_path\" | grep -q '\\.ts$'; then npx prettier --write \"$file_path\"; fi; }"
}
]
}
]
}
}
```
### Markdown Formatting Hook
Automatically fix missing language tags and formatting issues in markdown files:
```json theme={null}
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/markdown_formatter.py"
}
]
}
]
}
}
```
Create `.claude/hooks/markdown_formatter.py` with this content:
````python theme={null}
#!/usr/bin/env python3
"""
Markdown formatter for Claude Code output.
Fixes missing language tags and spacing issues while preserving code content.
"""
import json
import sys
import re
import os
def detect_language(code):
"""Best-effort language detection from code content."""
s = code.strip()
# JSON detection
if re.search(r'^\s*[{\[]', s):
try:
json.loads(s)
return 'json'
except:
pass
# Python detection
if re.search(r'^\s*def\s+\w+\s*\(', s, re.M) or \
re.search(r'^\s*(import|from)\s+\w+', s, re.M):
return 'python'
# JavaScript detection
if re.search(r'\b(function\s+\w+\s*\(|const\s+\w+\s*=)', s) or \
re.search(r'=>|console\.(log|error)', s):
return 'javascript'
# Bash detection
if re.search(r'^#!.*\b(bash|sh)\b', s, re.M) or \
re.search(r'\b(if|then|fi|for|in|do|done)\b', s):
return 'bash'
# SQL detection
if re.search(r'\b(SELECT|INSERT|UPDATE|DELETE|CREATE)\s+', s, re.I):
return 'sql'
return 'text'
def format_markdown(content):
"""Format markdown content with language detection."""
# Fix unlabeled code fences
def add_lang_to_fence(match):
indent, info, body, closing = match.groups()
if not info.strip():
lang = detect_language(body)
return f"{indent}```{lang}\n{body}{closing}\n"
return match.group(0)
fence_pattern = r'(?ms)^([ \t]{0,3})```([^\n]*)\n(.*?)(\n\1```)\s*$'
content = re.sub(fence_pattern, add_lang_to_fence, content)
# Fix excessive blank lines (only outside code fences)
content = re.sub(r'\n{3,}', '\n\n', content)
return content.rstrip() + '\n'
# Main execution
try:
input_data = json.load(sys.stdin)
file_path = input_data.get('tool_input', {}).get('file_path', '')
if not file_path.endswith(('.md', '.mdx')):
sys.exit(0) # Not a markdown file
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
formatted = format_markdown(content)
if formatted != content:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(formatted)
print(f"✓ Fixed markdown formatting in {file_path}")
except Exception as e:
print(f"Error formatting markdown: {e}", file=sys.stderr)
sys.exit(1)
````
Make the script executable:
```bash theme={null}
chmod +x .claude/hooks/markdown_formatter.py
```
This hook automatically:
* Detects programming languages in unlabeled code blocks
* Adds appropriate language tags for syntax highlighting
* Fixes excessive blank lines while preserving code content
* Only processes markdown files (`.md`, `.mdx`)
### Custom Notification Hook
Get desktop notifications when Claude needs input:
```json theme={null}
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code' 'Awaiting your input'"
}
]
}
]
}
}
```
### File Protection Hook
Block edits to sensitive files:
```json theme={null}
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "python3 -c \"import json, sys; data=json.load(sys.stdin); path=data.get('tool_input',{}).get('file_path',''); sys.exit(2 if any(p in path for p in ['.env', 'package-lock.json', '.git/']) else 0)\""
}
]
}
]
}
}
```
## Learn more
* For reference documentation on hooks, see [Hooks reference](/en/hooks).
* For comprehensive security best practices and safety guidelines, see [Security Considerations](/en/hooks#security-considerations) in the hooks reference documentation.
* For troubleshooting steps and debugging techniques, see [Debugging](/en/hooks#debugging) in the hooks reference
documentation.
---
> To find navigation and other pages in this documentation, fetch the llms.txt file at: https://code.claude.com/docs/llms.txt