Was this page helpful?
훅을 사용하면 주요 지점에서 에이전트 실행을 가로채 검증, 로깅, 보안 제어 또는 커스텀 로직을 추가할 수 있습니다. 훅을 통해 다음을 수행할 수 있습니다:
훅은 두 부분으로 구성됩니다:
PreToolUse) 그리고 어떤 도구와 매칭할지 알려줌다음 예제는 에이전트가 .env 파일을 수정하는 것을 차단합니다. 먼저 파일 경로를 확인하는 콜백을 정의한 다음, Write 또는 Edit 도구 호출 전에 실행되도록 query()에 전달합니다:
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 | 예 | 예 | 사용자 프롬프트 제출 | 프롬프트에 추가 컨텍스트 주입 |
훅은 다양한 시나리오를 처리할 수 있을 만큼 유연합니다. 다음은 카테고리별로 정리된 가장 일반적인 패턴입니다.
에이전트에 훅을 구성하려면 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 |
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' (확인 요청).
이러한 패턴은 복잡한 사용 사례를 위한 더 정교한 훅 시스템을 구축하는 데 도움이 됩니다.
훅은 배열에 나타나는 순서대로 실행됩니다. 각 훅을 단일 책임에 집중시키고 복잡한 로직을 위해 여러 훅을 체이닝하세요. 이 예제는 모든 도구 호출에 대해 네 개의 훅을 모두 실행합니다(매처 미지정):
정규식 패턴을 사용하여 여러 도구를 매칭합니다:
매처는 도구 이름만 매칭하며, 파일 경로나 다른 인수는 매칭하지 않습니다. 파일 경로로 필터링하려면 훅 콜백 내부에서 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을 사용하여 취소를 우아하게 처리하세요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 출력 모드에서 나타나지 않을 수 있습니다. 훅 결정을 애플리케이션에 표시해야 하는 경우 별도로 로깅하거나 전용 출력 채널을 사용하세요.
Stop| 예 |
| 예 |
| 에이전트 실행 중지 |
| 종료 전 세션 상태 저장 |
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 {}| Claude가 볼 수 있도록 대화에 주입되는 메시지 |
object |
| PreToolUse |
수정된 도구 입력 (permissionDecision: 'allow' 필요) |
additionalContext | string | PreToolUse, 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을 포함하세요