훅을 사용하면 에이전트 실행의 주요 지점에서 검증, 로깅, 보안 제어 또는 사용자 정의 로직을 추가하기 위해 에이전트 실행을 가로챌 수 있습니다. 훅을 사용하면 다음을 수행할 수 있습니다:
훅은 두 부분으로 구성됩니다:
PreToolUse 같은) 그리고 어떤 도구를 일치시킬지 알려줍니다다음 예제는 에이전트가 .env 파일을 수정하는 것을 차단합니다. 먼저 파일 경로를 확인하는 콜백을 정의한 다음, query()에 전달하여 Write 또는 Edit 도구 호출 전에 실행합니다:
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, HookMatcher
# 도구 호출 세부 정보를 받는 훅 콜백 정의
async def protect_env_files(input_data, tool_use_id, context):
# 도구의 입력 인수에서 파일 경로 추출
file_path = input_data['tool_input'].get('file_path', '')
file_name = file_path.split('/')[-1]
# .env 파일을 대상으로 하는 경우 작업 차단
if file_name == '.env':
return {
'hookSpecificOutput': {
'hookEventName': input_data['hook_event_name'],
'permissionDecision': 'deny',
'permissionDecisionReason': 'Cannot modify .env files'
}
}
# 작업을 허용하려면 빈 객체 반환
return {}
async def main():
async for message in query(
prompt="Update the database configuration",
options=ClaudeAgentOptions(
hooks={
# PreToolUse 이벤트에 대한 훅 등록
# 매처는 Write 및 Edit 도구 호출만 필터링합니다
'PreToolUse': [HookMatcher(matcher='Write|Edit', hooks=[protect_env_files])]
}
)
):
print(message)
asyncio.run(main())이것은 PreToolUse 훅입니다. 도구 실행 전에 실행되며 로직에 따라 작업을 차단하거나 허용할 수 있습니다. 이 가이드의 나머지 부분에서는 사용 가능한 모든 훅, 구성 옵션 및 일반적인 사용 사례의 패턴을 다룹니다.
SDK는 에이전트 실행의 다양한 단계에 대한 훅을 제공합니다. 일부 훅은 두 SDK 모두에서 사용 가능하지만, Python SDK가 지원하지 않기 때문에 다른 훅은 TypeScript 전용입니다.
| 훅 이벤트 | Python SDK | TypeScript SDK | 트리거 | 사용 사례 예 |
|---|---|---|---|---|
PreToolUse | 예 | 예 | 도구 호출 요청 (차단 또는 수정 가능) | 위험한 셸 명령어 차단 |
PostToolUse | 예 | 예 | 도구 실행 결과 | 모든 파일 변경 사항을 감사 추적에 로깅 |
PostToolUseFailure | 아니오 | 예 | 도구 실행 실패 | 도구 오류 처리 또는 로깅 |
UserPromptSubmit | 예 | 예 | 사용자 프롬프트 제출 | 프롬프트에 추가 컨텍스트 주입 |
Stop |
훅은 많은 다양한 시나리오를 처리할 수 있을 만큼 유연합니다. 다음은 카테고리별로 정렬된 가장 일반적인 패턴입니다.
에이전트에 대한 훅을 구성하려면 query()를 호출할 때 options.hooks 매개변수에 훅을 전달합니다:
async for message in query(
prompt="Your prompt",
options=ClaudeAgentOptions(
hooks={
'PreToolUse': [HookMatcher(matcher='Bash', hooks=[my_callback])]
}
)
):
print(message)hooks 옵션은 다음과 같은 딕셔너리(Python) 또는 객체(TypeScript)입니다:
훅 콜백 함수는 이벤트에 대한 입력 데이터를 받고 에이전트가 작업을 허용, 차단 또는 수정할지 알 수 있도록 응답을 반환합니다.
매처를 사용하여 콜백을 트리거하는 도구를 필터링합니다:
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
matcher | string | undefined | 도구 이름을 일치시키는 정규식 패턴. 기본 제공 도구에는 Bash, Read, Write, Edit, Glob, Grep, WebFetch, Task 등이 포함됩니다. MCP 도구는 mcp__<server>__<action> 패턴을 사용합니다. |
hooks | HookCallback[] | - | 필수. 패턴이 일치할 때 실행할 콜백 함수 배열 |
가능할 때마다 matcher 패턴을 사용하여 특정 도구를 대상으로 합니다. 'Bash' 매처는 Bash 명령어에만 실행되지만, 패턴을 생략하면 모든 도구 호출에 대해 콜백이 실행됩니다. 매처는 도구 이름으로만 필터링하며, 파일 경로나 다른 인수로는 필터링하지 않습니다. 파일 경로로 필터링하려면 콜백 내에서 tool_input.file_path를 확인합니다.
매처는 도구 기반 훅(PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest)에만 적용됩니다. Stop, SessionStart, Notification 같은 수명 주기 훅의 경우 매처는 무시되고 해당 유형의 모든 이벤트에 대해 훅이 발동합니다.
도구 이름 발견: 세션이 시작될 때 초기 시스템 메시지의 tools 배열을 확인하거나, 매처 없이 훅을 추가하여 모든 도구 호출을 로깅합니다.
MCP 도구 이름 지정: MCP 도구는 항상 mcp__로 시작하고 그 뒤에 서버 이름과 작업이 옵니다: mcp__<server>__<action>. 예를 들어, playwright라는 서버를 구성하면 해당 도구는 mcp__playwright__browser_screenshot, mcp__playwright__browser_click 등으로 이름이 지정됩니다. 서버 이름은 mcpServers 구성에서 사용하는 키에서 나옵니다.
이 예제는 매처를 사용하여 PreToolUse 이벤트가 발동할 때 파일 수정 도구에만 대해 훅을 실행합니다:
options = ClaudeAgentOptions(
hooks={
'PreToolUse': [
HookMatcher(matcher='Write|Edit', hooks=[validate_file_path])
]
}
)모든 훅 콜백은 세 개의 인수를 받습니다:
dict / HookInput): 이벤트 세부 정보. 입력 데이터에서 필드를 참조하세요str | None / string | null): PreToolUse 및 PostToolUse 이벤트를 상호 연관시킵니다HookContext): TypeScript에서는 취소를 위한 signal 속성(AbortSignal)을 포함합니다. 훅이 시간 초과되면 자동으로 취소되도록 fetch() 같은 비동기 작업에 이를 전달합니다. Python에서는 이 인수는 향후 사용을 위해 예약되어 있습니다.훅 콜백의 첫 번째 인수에는 이벤트에 대한 정보가 포함됩니다. 필드 이름은 SDK 간에 동일합니다(둘 다 snake_case 사용).
모든 훅 유형에 있는 공통 필드:
| 필드 | 타입 | 설명 |
|---|---|---|
hook_event_name | string | 훅 유형 (PreToolUse, PostToolUse 등) |
session_id | string | 현재 세션 식별자 |
transcript_path | string | 대화 기록 경로 |
cwd | string | 현재 작업 디렉토리 |
훅별 필드는 훅 유형에 따라 다릅니다. TS로 표시된 항목은 TypeScript SDK에서만 사용 가능합니다:
| 필드 | 타입 | 설명 | 훅 |
|---|---|---|---|
tool_name | string | 호출되는 도구의 이름 | PreToolUse, PostToolUse, PostToolUseFailureTS, PermissionRequestTS |
tool_input | object | 도구에 전달된 인수 | PreToolUse, PostToolUse, PostToolUseFailureTS, PermissionRequestTS |
tool_response | any | 도구 실행에서 반환된 결과 | PostToolUse |
error | string | 도구 실행 실패의 오류 메시지 |
아래 코드는 tool_name 및 tool_input을 사용하여 각 도구 호출에 대한 세부 정보를 로깅하는 훅 콜백을 정의합니다:
콜백 함수는 SDK에 진행 방법을 알려주는 객체를 반환합니다. 변경 없이 작업을 허용하려면 빈 객체 {}를 반환합니다. 작업을 차단, 수정 또는 컨텍스트를 추가하려면 hookSpecificOutput 필드를 포함하는 객체를 반환합니다.
최상위 필드 (hookSpecificOutput 외부):
| 필드 | 타입 | 설명 |
|---|---|---|
continue | boolean | 이 훅 후 에이전트가 계속해야 하는지 여부 (기본값: true) |
stopReason | string | continue가 false일 때 표시되는 메시지 |
suppressOutput | boolean | 기록에서 stdout 숨기기 (기본값: false) |
systemMessage | string | Claude가 볼 수 있도록 대화에 주입된 메시지 |
hookSpecificOutput 내부의 필드:
| 필드 | 타입 | 훅 | 설명 |
|---|---|---|---|
hookEventName | string | 모두 | 필수. 현재 이벤트를 일치시키려면 input.hook_event_name을 사용합니다 |
permissionDecision | 'allow' | 'deny' | 'ask' | PreToolUse | 도구 실행 여부를 제어합니다 |
permissionDecisionReason | string | PreToolUse | Claude에 표시되는 결정에 대한 설명 |
updatedInput |
이 예제는 /etc 디렉토리에 대한 쓰기 작업을 차단하면서 Claude에게 안전한 파일 관행을 상기시키는 시스템 메시지를 주입합니다:
여러 훅 또는 권한 규칙이 적용되면 SDK는 다음 순서로 평가합니다:
훅이 deny를 반환하면 작업이 차단되며, allow를 반환하는 다른 훅은 이를 재정의하지 않습니다.
도구 실행을 방지하려면 거부 결정을 반환합니다:
도구가 받는 것을 변경하려면 업데이트된 입력을 반환합니다:
updatedInput을 사용할 때는 permissionDecision도 포함해야 합니다. 원본 tool_input을 변경하는 대신 항상 새 객체를 반환합니다.
대화에 컨텍스트를 주입합니다:
async def add_security_reminder(input_data, tool_use_id, context):
return {
'systemMessage': 'Remember to follow security best practices.'
}신뢰할 수 있는 도구에 대한 권한 프롬프트를 우회합니다. 이는 특정 작업이 사용자 확인 없이 실행되도록 하려는 경우에 유용합니다:
permissionDecision 필드는 세 가지 값을 허용합니다: 'allow' (자동 승인), 'deny' (차단) 또는 'ask' (확인 프롬프트).
이러한 패턴은 복잡한 사용 사례를 위해 더 정교한 훅 시스템을 구축하는 데 도움이 됩니다.
훅은 배열에 나타나는 순서대로 실행됩니다. 각 훅을 단일 책임에 집중하고 복잡한 로직을 위해 여러 훅을 연결합니다. 이 예제는 모든 도구 호출에 대해 4개의 훅을 모두 실행합니다 (매처 지정 안 함):
정규식 패턴을 사용하여 여러 도구를 일치시킵니다:
매처는 도구 이름만 일치시키며, 파일 경로나 다른 인수는 일치시키지 않습니다. 파일 경로로 필터링하려면 훅 콜백 내에서 tool_input.file_path를 확인합니다.
SubagentStop 훅을 사용하여 서브에이전트 완료를 모니터링합니다. tool_use_id는 부모 에이전트 호출을 서브에이전트와 상호 연관시키는 데 도움이 됩니다:
훅은 HTTP 요청과 같은 비동기 작업을 수행할 수 있습니다. 예외를 던지는 대신 예외를 포착하여 오류를 정상적으로 처리합니다. TypeScript에서는 훅이 시간 초과되면 요청이 취소되도록 signal을 fetch()에 전달합니다:
Notification 훅을 사용하여 에이전트에서 상태 업데이트를 받고 Slack 또는 모니터링 대시보드와 같은 외부 서비스로 전달합니다:
import { query, HookCallback, NotificationHookInput } from "@anthropic-ai/claude-agent-sdk";
const notificationHandler: HookCallback = async (input, toolUseID, { signal }) => {
const notification = input as NotificationHookInput;
await fetch('https://hooks.slack.com/services/YOUR/WEBHOOK/URL', {
method: 'POST',
body: JSON.stringify({
text: `Agent status: ${notification.message}`
}),
signal
});
return {};
};
for await (const message of query({
prompt: "Analyze this codebase",
options: {
hooks: {
Notification: [{ hooks: [notificationHandler] }]
}
}
})) {
console.log(message);
}이 섹션에서는 일반적인 문제와 해결 방법을 다룹니다.
preToolUse가 아닌 PreToolUse)options.hooks의 올바른 이벤트 유형 아래에 있는지 확인합니다SubagentStop, Stop, SessionStart, SessionEnd 및 Notification 훅의 경우 매처는 무시됩니다. 이러한 훅은 해당 유형의 모든 이벤트에 대해 발동합니다.max_turns 제한에 도달하면 훅이 발동하지 않을 수 있습니다. 세션이 훅을 실행하기 전에 종료되기 때문입니다.매처는 도구 이름만 일치시키며, 파일 경로나 다른 인수는 일치시키지 않습니다. 파일 경로로 필터링하려면 훅 내에서 tool_input.file_path를 확인합니다:
const myHook: HookCallback = async (input, toolUseID, { signal }) => {
const preInput = input as PreToolUseHookInput;
const filePath = preInput.tool_input?.file_path as string;
if (!filePath?.endsWith('.md')) return {}; // 마크다운이 아닌 파일 건너뛰기
// 마크다운 파일 처리...
};HookMatcher 구성에서 timeout 값을 증가시킵니다AbortSignal을 사용하여 TypeScript에서 취소를 정상적으로 처리합니다PreToolUse 훅에서 permissionDecision: 'deny' 반환을 확인합니다permissionDecisionReason을 확인합니다updatedInput이 최상위 수준이 아닌 hookSpecificOutput 내부에 있는지 확인합니다:
return {
hookSpecificOutput: {
hookEventName: input.hook_event_name,
permissionDecision: 'allow',
updatedInput: { command: 'new command' }
}
};입력 수정이 적용되려면 permissionDecision: 'allow'도 반환해야 합니다
SessionStart, SessionEnd 및 Notification 훅은 TypeScript SDK에서만 사용 가능합니다. Python SDK는 설정 제한으로 인해 이러한 이벤트를 지원하지 않습니다.
여러 서브에이전트를 생성할 때 각 서브에이전트는 별도로 권한을 요청할 수 있습니다. 서브에이전트는 부모 에이전트 권한을 자동으로 상속하지 않습니다. 반복되는 프롬프트를 피하려면 PreToolUse 훅을 사용하여 특정 도구를 자동 승인하거나 서브에이전트 세션에 적용되는 권한 규칙을 구성합니다.
서브에이전트를 생성하는 UserPromptSubmit 훅은 해당 서브에이전트가 동일한 훅을 트리거하면 무한 루프를 만들 수 있습니다. 이를 방지하려면:
parent_tool_use_id 필드를 사용하여 이미 서브에이전트 컨텍스트에 있는지 감지합니다systemMessage 필드는 모델이 볼 수 있도록 대화에 컨텍스트를 추가하지만 모든 SDK 출력 모드에 나타나지 않을 수 있습니다. 훅 결정을 애플리케이션에 표시해야 하면 별도로 로깅하거나 전용 출력 채널을 사용합니다.
| 예 |
| 예 |
| 에이전트 실행 중지 |
| 종료 전 세션 상태 저장 |
SubagentStart | 아니오 | 예 | 서브에이전트 초기화 | 병렬 작업 생성 추적 |
SubagentStop | 예 | 예 | 서브에이전트 완료 | 병렬 작업의 결과 집계 |
PreCompact | 예 | 예 | 대화 압축 요청 | 요약 전 전체 기록 보관 |
PermissionRequest | 아니오 | 예 | 권한 대화 표시 | 사용자 정의 권한 처리 |
SessionStart | 아니오 | 예 | 세션 초기화 | 로깅 및 원격 측정 초기화 |
SessionEnd | 아니오 | 예 | 세션 종료 | 임시 리소스 정리 |
Notification | 아니오 | 예 | 에이전트 상태 메시지 | Slack 또는 PagerDuty로 에이전트 상태 업데이트 전송 |
timeout | number | 60 | 시간 초과(초); 외부 API 호출을 수행하는 훅의 경우 증가시킵니다 |
| PostToolUseFailureTS |
is_interrupt | boolean | 실패가 인터럽트로 인한 것인지 여부 | PostToolUseFailureTS |
prompt | string | 사용자의 프롬프트 텍스트 | UserPromptSubmit |
stop_hook_active | boolean | 중지 훅이 현재 처리 중인지 여부 | Stop, SubagentStop |
agent_id | string | 서브에이전트의 고유 식별자 | SubagentStartTS, SubagentStopTS |
agent_type | string | 서브에이전트의 유형/역할 | SubagentStartTS |
agent_transcript_path | string | 서브에이전트의 대화 기록 경로 | SubagentStopTS |
trigger | string | 압축을 트리거한 것: manual 또는 auto | PreCompact |
custom_instructions | string | 압축을 위해 제공된 사용자 정의 지침 | PreCompact |
permission_suggestions | array | 도구에 대한 제안된 권한 업데이트 | PermissionRequestTS |
source | string | 세션이 시작된 방식: startup, resume, clear 또는 compact | SessionStartTS |
reason | string | 세션이 종료된 이유: clear, logout, prompt_input_exit, bypass_permissions_disabled 또는 other | SessionEndTS |
message | string | 에이전트의 상태 메시지 | NotificationTS |
notification_type | string | 알림 유형: permission_prompt, idle_prompt, auth_success 또는 elicitation_dialog | NotificationTS |
title | string | 에이전트가 설정한 선택적 제목 | NotificationTS |
async def log_tool_calls(input_data, tool_use_id, context):
if input_data['hook_event_name'] == 'PreToolUse':
print(f"Tool: {input_data['tool_name']}")
print(f"Input: {input_data['tool_input']}")
return {}object |
| PreToolUse |
수정된 도구 입력 (permissionDecision: 'allow' 필요) |
additionalContext | string | PostToolUse, UserPromptSubmit, SessionStartTS, SubagentStartTS | 대화에 추가된 컨텍스트 |
async def block_etc_writes(input_data, tool_use_id, context):
file_path = input_data['tool_input'].get('file_path', '')
if file_path.startswith('/etc'):
return {
# 최상위 필드: 대화에 지침 주입
'systemMessage': 'Remember: system directories like /etc are protected.',
# hookSpecificOutput: 작업 차단
'hookSpecificOutput': {
'hookEventName': input_data['hook_event_name'],
'permissionDecision': 'deny',
'permissionDecisionReason': 'Writing to /etc is not allowed'
}
}
return {}async def block_dangerous_commands(input_data, tool_use_id, context):
if input_data['hook_event_name'] != 'PreToolUse':
return {}
command = input_data['tool_input'].get('command', '')
if 'rm -rf /' in command:
return {
'hookSpecificOutput': {
'hookEventName': input_data['hook_event_name'],
'permissionDecision': 'deny',
'permissionDecisionReason': 'Dangerous command blocked: rm -rf /'
}
}
return {}async def redirect_to_sandbox(input_data, tool_use_id, context):
if input_data['hook_event_name'] != 'PreToolUse':
return {}
if input_data['tool_name'] == 'Write':
original_path = input_data['tool_input'].get('file_path', '')
return {
'hookSpecificOutput': {
'hookEventName': input_data['hook_event_name'],
'permissionDecision': 'allow',
'updatedInput': {
**input_data['tool_input'],
'file_path': f'/sandbox{original_path}'
}
}
}
return {}async def auto_approve_read_only(input_data, tool_use_id, context):
if input_data['hook_event_name'] != 'PreToolUse':
return {}
read_only_tools = ['Read', 'Glob', 'Grep', 'LS']
if input_data['tool_name'] in read_only_tools:
return {
'hookSpecificOutput': {
'hookEventName': input_data['hook_event_name'],
'permissionDecision': 'allow',
'permissionDecisionReason': 'Read-only tool auto-approved'
}
}
return {}options = ClaudeAgentOptions(
hooks={
'PreToolUse': [
HookMatcher(hooks=[rate_limiter]), # 첫 번째: 속도 제한 확인
HookMatcher(hooks=[authorization_check]), # 두 번째: 권한 확인
HookMatcher(hooks=[input_sanitizer]), # 세 번째: 입력 정제
HookMatcher(hooks=[audit_logger]) # 마지막: 작업 로깅
]
}
)options = ClaudeAgentOptions(
hooks={
'PreToolUse': [
# 파일 수정 도구 일치
HookMatcher(matcher='Write|Edit|Delete', hooks=[file_security_hook]),
# 모든 MCP 도구 일치
HookMatcher(matcher='^mcp__', hooks=[mcp_audit_hook]),
# 모든 것 일치 (매처 없음)
HookMatcher(hooks=[global_logger])
]
}
)async def subagent_tracker(input_data, tool_use_id, context):
if input_data['hook_event_name'] == 'SubagentStop':
print(f"[SUBAGENT] Completed")
print(f" Tool use ID: {tool_use_id}")
print(f" Stop hook active: {input_data.get('stop_hook_active')}")
return {}
options = ClaudeAgentOptions(
hooks={
'SubagentStop': [HookMatcher(hooks=[subagent_tracker])]
}
)import aiohttp
from datetime import datetime
async def webhook_notifier(input_data, tool_use_id, context):
if input_data['hook_event_name'] != 'PostToolUse':
return {}
try:
async with aiohttp.ClientSession() as session:
await session.post(
'https://api.example.com/webhook',
json={
'tool': input_data['tool_name'],
'timestamp': datetime.now().isoformat()
}
)
except Exception as e:
print(f'Webhook request failed: {e}')
return {}hookSpecificOutput에 hookEventName을 포함하여 출력이 어떤 훅 유형에 대한 것인지 식별합니다