Hooks là các shell command, HTTP endpoint hoặc LLM prompt do bạn định nghĩa, chạy tự động tại những điểm cụ thể trong vòng đời của Claude Code. Chúng cho bạn quyền kiểm soát mang tính deterministic (chắc chắn xảy ra) thay vì phụ thuộc vào việc model có "chịu" chạy một hành động nào đó hay không — dùng để enforce project rules, tự động hóa việc lặp lại (format, lint) và tích hợp Claude Code với công cụ sẵn có.
Hooks là gì và khi nào dùng
Khi một hook event fire và matcher khớp, Claude Code truyền JSON mô tả context về hook handler. Với command hook, input đến qua stdin; với HTTP hook, nó là body của POST request. Handler đọc input, hành động, và tùy chọn trả về một quyết định qua exit code hoặc JSON trên stdout.
Dùng hook khi bạn cần một hành động luôn xảy ra: chạy formatter sau mỗi lần edit, block một câu lệnh nguy hiểm trước khi nó chạy, gửi notification khi Claude cần input, hay inject context lúc session bắt đầu. Với quyết định cần "phán đoán" thay vì quy tắc cứng, dùng prompt-based hoặc agent-based hook (xem bên dưới).
Hook lifecycle
Hook fire tại các điểm lifecycle cụ thể. Các event chia thành ba nhịp (cadence):
- Một lần mỗi session:
SessionStart,SessionEnd - Một lần mỗi turn:
UserPromptSubmit,Stop,StopFailure - Mỗi tool call trong agentic loop:
PreToolUse,PostToolUse
Khi một event fire, tất cả hook khớp chạy song song, và các handler trùng lặp được dedupe tự động (command hook dedupe theo command string + args; HTTP hook theo URL).
Bảng hook events
Các event thường dùng nhất:
| Event | Fire khi |
|---|---|
SessionStart | Khi session bắt đầu hoặc resume |
Setup | Khi khởi chạy với --init-only, hoặc --init/--maintenance trong chế độ -p |
UserPromptSubmit | Khi bạn submit một prompt, trước khi Claude xử lý |
UserPromptExpansion | Khi một command người dùng gõ expand thành prompt. Có thể block |
PreToolUse | Trước khi một tool call chạy. Có thể block |
PermissionRequest | Khi một permission dialog xuất hiện |
PermissionDenied | Khi tool call bị auto mode classifier từ chối. Trả {retry: true} để cho model retry |
PostToolUse | Sau khi một tool call thành công |
PostToolUseFailure | Sau khi một tool call thất bại |
PostToolBatch | Sau khi một batch tool call song song hoàn tất, trước model call tiếp theo |
Notification | Khi Claude Code gửi notification |
SubagentStart | Khi một subagent được spawn |
SubagentStop | Khi một subagent kết thúc |
TaskCreated | Khi một task được tạo qua TaskCreate |
TaskCompleted | Khi một task được đánh dấu hoàn thành |
Stop | Khi Claude hoàn tất phản hồi |
StopFailure | Khi turn kết thúc do API error. Output và exit code bị bỏ qua |
InstructionsLoaded | Khi một CLAUDE.md hoặc .claude/rules/*.md được load vào context |
ConfigChange | Khi một file cấu hình thay đổi trong session |
CwdChanged | Khi thư mục làm việc thay đổi (ví dụ khi Claude chạy cd) |
FileChanged | Khi một file được watch thay đổi trên disk. matcher chỉ định filename cần watch |
WorktreeCreate | Khi một worktree đang được tạo qua --worktree hoặc isolation: "worktree" |
WorktreeRemove | Khi một worktree đang bị xóa |
PreCompact | Trước khi context compaction |
PostCompact | Sau khi context compaction hoàn tất |
Elicitation | Khi một MCP server yêu cầu user input trong một tool call |
ElicitationResult | Sau khi user phản hồi một MCP elicitation |
SessionEnd | Khi một session kết thúc |
Còn có
TeammateIdle,MessageDisplay, và một số event khác cho các tính năng nâng cao (agent teams, streaming display). Xem Hooks reference để có danh sách đầy đủ.
Cấu hình trong settings.json
Hooks được định nghĩa trong file settings JSON, với ba tầng nesting:
- Chọn một hook event (như
PreToolUsehoặcStop) - Thêm một matcher group để lọc khi nào fire
- Định nghĩa một hoặc nhiều hook handler để chạy khi khớp
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
Mỗi tên event là một key bên trong đối tượng hooks duy nhất. Nếu file settings đã có key hooks, thêm event mới như một sibling thay vì thay thế cả object.
Hook locations (scope)
Nơi bạn định nghĩa hook quyết định phạm vi của nó:
| Location | Scope | Share được? |
|---|---|---|
~/.claude/settings.json | Tất cả project của bạn | Không, local trên máy bạn |
.claude/settings.json | Một project | Có, commit vào repo được |
.claude/settings.local.json | Một project | Không, gitignored khi Claude tạo |
| Managed policy settings | Toàn organization | Có, admin kiểm soát |
Plugin hooks/hooks.json | Khi plugin được bật | Có, đóng gói cùng plugin |
| Skill / agent frontmatter | Khi component đang active | Có, định nghĩa trong file component |
Enterprise admin có thể dùng allowManagedHooksOnly để chặn user/project/plugin hook.
Matcher patterns
Field matcher lọc khi nào hook fire. Cách nó được đánh giá phụ thuộc vào ký tự nó chứa:
| Giá trị matcher | Đánh giá như | Ví dụ |
|---|---|---|
"*", "", hoặc bỏ trống | Match tất cả | fire mọi lần event xảy ra |
Chỉ chữ, số, _, -, khoảng trắng, ,, | | Chuỗi khớp chính xác, hoặc danh sách phân tách bởi | / , | Bash; Edit|Write; Edit, Write |
| Chứa bất kỳ ký tự nào khác | JavaScript regular expression, unanchored | ^Notebook; mcp__memory__.* |
Đường regex được test bằng RegExp.prototype.test, khớp bất kỳ đâu trong chuỗi. Edit.* khớp cả Edit và NotebookEdit; bọc bằng ^ và $ (^Edit$) khi cần khớp toàn chuỗi. Matcher case-sensitive.
Mỗi loại event match trên một field khác nhau:
| Event | Matcher lọc theo | Ví dụ giá trị |
|---|---|---|
PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied | tool name | Bash, Edit|Write, mcp__.* |
SessionStart | cách session bắt đầu | startup, resume, clear, compact |
SessionEnd | lý do session kết thúc | clear, resume, logout, other, ... |
Notification | loại notification | permission_prompt, idle_prompt, ... |
SubagentStart, SubagentStop | agent type | general-purpose, Explore, Plan, ... |
PreCompact, PostCompact | tác nhân trigger compaction | manual, auto |
ConfigChange | nguồn cấu hình | user_settings, project_settings, skills, ... |
FileChanged | filename cần watch | .envrc|.env |
UserPromptSubmit, Stop, PostToolBatch, CwdChanged, ... | không hỗ trợ matcher | luôn fire mọi lần |
Các event không hỗ trợ matcher sẽ âm thầm bỏ qua field matcher nếu bạn thêm vào.
Match MCP tools
MCP server tool xuất hiện như tool thường trong tool event, theo pattern mcp__<server>__<tool>. Để match mọi tool từ một server, append .* vào prefix (bắt buộc):
mcp__memory__.*— mọi tool từ servermemorymcp__brave-search__.*— server có tên chứa dấu gạch ngangmcp__.*__write.*— mọi tool bắt đầu bằngwritetừ mọi server
Lọc chi tiết với field if
Với tool event, bạn lọc chặt hơn bằng field if trên từng handler. if dùng cú pháp permission rule, khớp theo cả tool name lẫn arguments — nên hook process chỉ spawn khi tool call khớp:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
}
]
}
]
}
}
if chỉ giữ đúng một permission rule (không có &&, ||, hay danh sách) và chỉ đánh giá trên tool event (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied). Trên event khác, hook có if sẽ không bao giờ chạy. Với Bash pattern: leading VAR=value bị strip trước khi match; mỗi subcommand (kể cả trong $() và backticks) được kiểm tra riêng. Bộ lọc là best-effort (fail open khi không parse được) — dùng permission system để enforce cứng, không dùng hook.
Hook handler types
Mỗi object trong mảng hooks bên trong là một handler. Có năm loại:
type | Chạy gì |
|---|---|
command | Chạy shell command. Nhận JSON input qua stdin, trả kết quả qua exit code + stdout |
http | POST JSON input tới một URL. Endpoint trả kết quả qua response body |
mcp_tool | Gọi một tool trên MCP server đã kết nối. Text output xử lý như stdout của command |
prompt | Gửi prompt tới model để đánh giá single-turn, trả yes/no dạng JSON |
agent | Spawn subagent có thể dùng tool (Read, Grep, Glob) để verify. Experimental |
Common fields (mọi loại)
| Field | Bắt buộc | Mô tả |
|---|---|---|
type | Có | "command", "http", "mcp_tool", "prompt", hoặc "agent" |
if | Không | Permission rule để lọc; chỉ trên tool event |
timeout | Không | Giây trước khi hủy. Mặc định: 600 cho command/http/mcp_tool; 30 cho prompt; 60 cho agent |
statusMessage | Không | Message spinner tùy chỉnh hiển thị khi hook chạy |
Command hook fields
| Field | Mô tả |
|---|---|
command | Shell command để chạy. Với args, đây là executable spawn trực tiếp |
args | Danh sách argument. Khi có, command được resolve như executable và spawn không qua shell |
async | Nếu true, chạy nền không block |
shell | Shell dùng: "bash" hoặc "powershell" |
Shell form (không có args): command truyền cho shell — hỗ trợ pipe, &&, biến. Exec form (có args): spawn trực tiếp, mỗi phần tử args là một argument y nguyên, không quoting. Dùng exec form khi tham chiếu path placeholder.
Tham chiếu script bằng path
Dùng các placeholder này để trỏ script bất kể working directory:
${CLAUDE_PROJECT_DIR}— project root${CLAUDE_PLUGIN_ROOT}— thư mục cài đặt plugin${CLAUDE_PLUGIN_DATA}— thư mục dữ liệu bền của plugin
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-style.sh",
"args": []
}
Input và output JSON
Command hook nhận JSON qua stdin và giao tiếp kết quả qua exit code, stdout, stderr. HTTP hook nhận JSON như POST body và trả kết quả qua response body.
Common input fields
Mọi event nhận các field chung sau, cùng với field riêng của từng event:
| Field | Mô tả |
|---|---|
session_id | Định danh session hiện tại |
transcript_path | Đường dẫn tới file JSON hội thoại |
cwd | Working directory khi hook được gọi |
permission_mode | Permission mode hiện tại: default, plan, acceptEdits, auto, dontAsk, bypassPermissions |
hook_event_name | Tên event đã fire |
Ví dụ, một PreToolUse hook cho lệnh Bash nhận trên stdin:
{
"session_id": "abc123",
"cwd": "/home/user/my-project",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}
tool_name và tool_input là field riêng theo event. UserPromptSubmit nhận prompt; SessionStart nhận source (startup/resume/clear/compact), v.v.
Exit codes
Exit code từ command của bạn cho Claude Code biết hành động nên tiếp tục, bị block, hay bị bỏ qua:
| Exit code | Ý nghĩa |
|---|---|
| 0 | Thành công. stdout được parse tìm JSON output (chỉ xử lý JSON khi exit 0) |
| 2 | Blocking error. stdout bị bỏ qua; stderr được feed lại cho Claude như error |
| Bất kỳ code khác | Non-blocking error (với hầu hết event). Transcript hiện <hook> hook error, thực thi tiếp tục |
Lưu ý: chỉ exit 2 block hành động. Exit 1 bị coi là non-blocking error và hành động vẫn tiếp tục — dù 1 là mã lỗi Unix thông thường. Ngoại lệ:
WorktreeCreate, nơi mọi exit code khác 0 đều hủy việc tạo worktree.
Với UserPromptSubmit, UserPromptExpansion, và SessionStart, stdout khi exit 0 được thêm vào context mà Claude thấy được; với các event khác, stdout chỉ ghi vào debug log.
Exit code 2 — hành vi theo event
Tác dụng của exit 2 phụ thuộc vào event, vì một số event là hành động chưa xảy ra (block được) còn số khác đã xảy ra rồi:
| Event | Block được? | Hành vi khi exit 2 |
|---|---|---|
PreToolUse | Có | Block tool call |
PermissionRequest | Có | Từ chối permission |
UserPromptSubmit | Có | Block xử lý prompt và xóa prompt |
UserPromptExpansion | Có | Block việc expansion |
Stop | Có | Ngăn Claude dừng, tiếp tục hội thoại |
SubagentStop | Có | Ngăn subagent dừng |
PostToolBatch | Có | Dừng agentic loop trước model call tiếp theo |
ConfigChange | Có | Block config change (trừ policy_settings) |
PreCompact | Có | Block compaction |
PostToolUse | Không | Hiện stderr cho Claude; tool đã chạy rồi |
PostToolUseFailure | Không | Hiện stderr cho Claude; tool đã fail rồi |
SessionStart / Setup / SubagentStart | Không | Hiện stderr cho user; session/subagent tiếp tục |
Notification, SessionEnd, CwdChanged, FileChanged | Không | Chỉ hiện stderr cho user |
StopFailure | Không | Output và exit code bị bỏ qua |
JSON output — decision control
Exit code chỉ cho phép block hoặc im lặng. Để kiểm soát tinh hơn, exit 0 và in một JSON object ra stdout. Không trộn hai cách: nếu exit 2, JSON bị bỏ qua.
Các universal field hoạt động trên mọi event:
| Field | Mặc định | Mô tả |
|---|---|---|
continue | true | Nếu false, Claude dừng hoàn toàn. Ưu tiên hơn mọi decision field |
stopReason | none | Message hiện cho user khi continue: false (Claude không thấy) |
suppressOutput | false | Nếu true, ẩn stdout của hook khỏi transcript |
systemMessage | none | Warning message hiện cho user |
Mỗi nhóm event dùng một pattern quyết định khác nhau:
| Events | Pattern | Key fields |
|---|---|---|
UserPromptSubmit, PostToolUse, Stop, SubagentStop, ConfigChange, PreCompact | top-level decision | decision: "block", reason |
PreToolUse | hookSpecificOutput | permissionDecision (allow/deny/ask), permissionDecisionReason |
PermissionRequest | hookSpecificOutput | decision.behavior (allow/deny) |
SessionStart, Setup, SubagentStart | context only | hookSpecificOutput.additionalContext |
Với PreToolUse, các giá trị permissionDecision:
"allow"— bỏ qua prompt permission interactive (nhưng deny rule vẫn áp dụng)"deny"— hủy tool call, gửipermissionDecisionReasonvề cho Claude"ask"— hiện prompt permission như bình thường
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Use rg instead of grep for better performance"
}
}
Với UserPromptSubmit, dùng hookSpecificOutput.additionalContext để inject text vào context của Claude:
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Current branch: release-42. Deploy freeze until Friday."
}
}
Hook và permission mode:
PreToolUsehook fire trước mọi kiểm tra permission mode, kể cảbypassPermissions. Một hook trảpermissionDecision: "deny"block được tool ngay cả với--dangerously-skip-permissions. Chiều ngược lại thì không:"allow"không vượt qua được deny rule. Hook có thể siết chứ không nới.
Ví dụ tự động hóa
Auto-format code sau khi edit
Chạy Prettier trên mọi file Claude edit. Dùng PostToolUse với matcher Edit|Write, trích file path bằng jq:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
Block edit vào file được bảo vệ
Ngăn Claude sửa file nhạy cảm như .env, package-lock.json, hay bất cứ gì trong .git/. Script kiểm tra path và exit 2 để block.
Lưu vào .claude/hooks/protect-files.sh:
#!/bin/bash
# protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
exit 2
fi
done
exit 0
Cho script quyền chạy rồi đăng ký PreToolUse hook:
chmod +x .claude/hooks/protect-files.sh
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}
Block lệnh Bash nguy hiểm
Dùng JSON output để deny và giải thích cho Claude:
#!/bin/bash
# .claude/hooks/block-rm.sh
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q 'rm -rf'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Destructive command blocked by hook"
}
}'
else
exit 0 # không có quyết định; permission flow bình thường áp dụng
fi
Lưu ý quan trọng: exit 0 không có output không phê duyệt tool call — nó chỉ nghĩa là "hook không có ý kiến", và tool call tiếp tục qua permission flow bình thường. Hook có thể deny nhưng im lặng thì không allow.
Notification khi Claude cần input
Nhận desktop notification khi Claude chờ input. Dùng Notification event:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code' 'Claude Code needs your attention'"
}
]
}
]
}
}
(macOS: osascript -e 'display notification ...'; Windows: PowerShell MessageBox.) Matcher rỗng fire trên mọi loại; đặt permission_prompt hoặc idle_prompt để lọc hẹp hơn.
Log mọi lệnh Bash
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt"
}
]
}
]
}
}
Re-inject context sau compaction
Dùng SessionStart với matcher compact. Mọi text ghi ra stdout được thêm vào context của Claude:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Reminder: use Bun, not npm. Run bun test before committing.'"
}
]
}
]
}
}
Prompt-based hook — verify bằng phán đoán
Với quyết định cần phán đoán, dùng type: "prompt". Model (Haiku mặc định) trả yes/no dạng JSON: "ok": true cho phép tiếp tục, "ok": false thì reason được feed lại cho Claude:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}."
}
]
}
]
}
}
Menu /hooks, disable, và debug
Gõ /hooks trong Claude Code để mở trình duyệt read-only cho các hook đã cấu hình — hiện mỗi event với số lượng hook, cho drill vào matcher và xem chi tiết handler cùng nguồn (User, Project, Local, Plugin, ...). Để thêm/sửa/xóa hook, edit JSON trực tiếp hoặc nhờ Claude làm.
Để xóa hook, xóa entry khỏi file settings. Để tạm tắt tất cả hook mà không xóa, đặt "disableAllHooks": true trong settings. Không có cách tắt một hook riêng lẻ.
Debug nhanh:
- Run
/hooksxác nhận hook xuất hiện đúng event; matcher case-sensitive, phải khớp chính xác tool name. - Test script thủ công:
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh; echo $? - Nếu "command not found": dùng absolute path hoặc
${CLAUDE_PROJECT_DIR}; thêm"args": []để chuyển sang exec form. - Nếu "jq: command not found": cài
jqhoặc dùng Python/Node để parse JSON. - Nếu script không chạy:
chmod +x ./my-hook.sh. - Xem debug log đầy đủ:
claude --debug-file /tmp/claude.logrồitail -f /tmp/claude.log.
Stop hook và block cap
Stop hook fire mỗi khi Claude hoàn tất phản hồi, không chỉ lúc hoàn thành task. Claude Code cho phép tối đa 10 continuation do Stop hook kích hoạt trong một turn; sau cap này Claude dừng dù hook tiếp tục block. Script vẫn nên kiểm tra field stop_hook_active và exit sớm nếu là true:
#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # cho phép Claude dừng
fi
# ... phần còn lại của hook
Xem thêm
content/en/docs/claude-code/hooks.md— Hooks reference: full event schema, JSON output, async/HTTP/MCP tool hooks, security considerations.content/en/docs/claude-code/hooks-guide.md— Automate actions with hooks: hướng dẫn quickstart với ví dụ.content/en/docs/claude-code/settings.md— Cấu hình settings và thứ tự resolve file.content/en/docs/claude-code/slash-commands.md— Slash command (/hooks).content/en/docs/claude-code/plugins.md— Đóng gói hook trong plugin để share.- Chapter 09 — Permissions: cách permission rule và deny list tương tác với hook.