wip: [01-stabilize] paused at task 1/1 - OCR Hallucination Immune logic via Semantic delta window and fret-isolation
This commit is contained in:
128
.agent/services/claude-mem/docs/context/agent-sdk-v2-examples.ts
Normal file
128
.agent/services/claude-mem/docs/context/agent-sdk-v2-examples.ts
Normal 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);
|
||||
390
.agent/services/claude-mem/docs/context/agent-sdk-v2-preview.md
Normal file
390
.agent/services/claude-mem/docs/context/agent-sdk-v2-preview.md
Normal 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
|
||||
@@ -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§ion=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.
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user