Bỏ qua để đến nội dung

Tham chiếu Hooks

Bài viết được dịch tự động từ bài viết gốc, chưa được kiểm tra lại bởi con người. Chỉ những bài viết có dấu tick xanh cạnh tiêu đề là đã được kiểm tra.

Hooks là lệnh shell, HTTP endpoint, hoặc LLM prompt do bạn định nghĩa, chạy tự động tại các điểm cụ thể trong vòng đời của Claude Code.

Claude Code chạy hooks tại các điểm cụ thể trong một phiên. Khi một sự kiện xảy ra và một matcher khớp, Claude Code truyền ngữ cảnh JSON về sự kiện đó cho handler của bạn. Với command hook, input tới qua stdin. Với HTTP hook, nó tới dưới dạng phần thân của request POST. Handler của bạn có thể kiểm tra input, thực hiện hành động, và tùy chọn trả về một quyết định.

Các sự kiện có ba nhịp độ:

  • một lần mỗi phiên: SessionStartSessionEnd
  • một lần mỗi lượt: UserPromptSubmit, Stop, và StopFailure
  • mỗi lần tool được gọi trong vòng lặp agentic: PreToolUsePostToolUse, ngoại trừ lệnh gọi EndConversation bỏ qua cả hai

Bảng dưới đây tóm tắt thời điểm mỗi sự kiện xảy ra. Phần Sự kiện hook bên dưới ghi đầy đủ input schema và tùy chọn kiểm soát quyết định cho từng sự kiện.

Sự kiệnThời điểm chạy
SessionStartKhi một phiên bắt đầu hoặc được resume
SetupKhi bạn khởi động Claude Code với --init-only, hoặc với --init/--maintenance ở chế độ -p. Dùng để chuẩn bị một lần trong CI hoặc script
UserPromptSubmitKhi bạn gửi một prompt, trước khi Claude xử lý
UserPromptExpansionKhi một lệnh bạn gõ mở rộng thành prompt, trước khi tới Claude. Có thể chặn việc mở rộng
PreToolUseTrước khi một lệnh gọi tool thực thi. Có thể chặn
PermissionRequestKhi một lệnh gọi tool cần quyết định về quyền
PermissionDeniedKhi một lệnh gọi tool bị bộ phân loại của auto mode từ chối. Trả về {retry: true} để báo model có thể thử lại
PostToolUseSau khi một lệnh gọi tool thành công
PostToolUseFailureSau khi một lệnh gọi tool thất bại
PostToolBatchSau khi toàn bộ một batch tool call song song hoàn tất, trước lệnh gọi model tiếp theo
NotificationKhi Claude Code gửi thông báo
MessageDisplayTrong lúc text của tin nhắn assistant đang được hiển thị
SubagentStartKhi một subagent được sinh ra
SubagentStopKhi một subagent hoàn tất
TaskCreatedKhi một task đang được tạo qua TaskCreate
TaskCompletedKhi một task đang được đánh dấu hoàn thành
StopKhi Claude kết thúc lượt trả lời
StopFailureKhi lượt trả lời kết thúc do lỗi API. Output và exit code bị bỏ qua
TeammateIdleKhi một teammate trong agent team sắp chuyển sang idle
InstructionsLoadedKhi một file CLAUDE.md hoặc .claude/rules/*.md được nạp vào context. Chạy khi phiên bắt đầu và khi file được nạp muộn (lazy) trong phiên
ConfigChangeKhi một file cấu hình thay đổi trong lúc phiên đang chạy
CwdChangedKhi thư mục làm việc thay đổi, ví dụ khi Claude chạy lệnh cd. Hữu ích cho quản lý môi trường phản ứng (reactive) với công cụ như direnv
FileChangedKhi một file đang được theo dõi thay đổi trên đĩa. Field matcher quy định tên file nào cần theo dõi
WorktreeCreateKhi một worktree đang được tạo qua --worktree, isolation: "worktree", hoặc cho một phiên background. Thay thế hành vi git mặc định
WorktreeRemoveKhi một worktree đang bị xóa lúc phiên kết thúc, khi một subagent hoàn tất, hoặc khi bạn xóa một phiên background
PreCompactTrước khi nén context
PostCompactSau khi nén context hoàn tất
ElicitationKhi một MCP server yêu cầu input từ người dùng trong lúc gọi tool
ElicitationResultSau khi người dùng trả lời một elicitation MCP, trước khi phản hồi được gửi lại cho server
SessionEndKhi một phiên kết thúc

Để thấy các mảnh ghép này khớp với nhau ra sao, xét ví dụ một PreToolUse hook chặn lệnh shell mang tính phá hoại. matcher thu hẹp về lệnh gọi tool Bash, và điều kiện if thu hẹp thêm về các subcommand Bash khớp rm *, nên block-rm.sh chỉ chạy khi cả hai bộ lọc cùng khớp:

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
"args": []
}
]
}
]
}
}

Script đọc JSON input từ stdin, trích lệnh, và trả về permissionDecision"deny" nếu nó chứa rm -rf. Lưu vào .claude/hooks/block-rm.sh trong dự án của bạn:

.claude/hooks/block-rm.sh
#!/bin/bash
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; luồng permission bình thường áp dụng
fi

Trên macOS và Linux, cấp quyền thực thi bằng chmod +x .claude/hooks/block-rm.sh. Trên Windows, viết hook bằng PowerShell và đăng ký với "command": "powershell.exe" (xem ví dụ ở phần MessageDisplay).

Script này và các ví dụ Bash khác trên trang phân tích JSON input đều dùng jq, nên cài jq và đảm bảo nó nằm trong PATH trước khi thử.

Giả sử Claude Code quyết định chạy Bash "rm -rf /tmp/build". Quy trình diễn ra như sau:

  1. Sự kiện xảy ra - PreToolUse chạy, Claude Code gửi tool input dạng JSON qua stdin cho hook: { "tool_name": "Bash", "tool_input": { "command": "rm -rf /tmp/build" }, ... }
  2. Matcher kiểm tra - matcher "Bash" khớp tên tool nên nhóm hook này kích hoạt. Nếu bỏ qua matcher hoặc dùng "*", nhóm kích hoạt ở mọi lần xảy ra sự kiện.
  3. Điều kiện if kiểm tra - "Bash(rm *)" khớp vì rm -rf /tmp/build là subcommand khớp rm *, nên handler này chạy. Nếu lệnh là npm test, kiểm tra if sẽ không khớp và block-rm.sh sẽ không chạy, tránh chi phí spawn tiến trình. Field if là tùy chọn; không có nó, mọi handler trong nhóm khớp đều chạy.
  4. Handler hook chạy - script kiểm tra toàn bộ lệnh, thấy rm -rf, và in ra quyết định ở stdout (JSON permissionDecision: "deny" như trên). Nếu lệnh là biến thể an toàn hơn như rm file.txt, script sẽ chạy exit 0 thay vào đó. Exit code 0 không kèm output nghĩa là hook không có quyết định để báo cáo, nên lệnh gọi tool tiếp tục qua luồng permission bình thường. Hook có thể từ chối lệnh gọi, nhưng im lặng không đồng nghĩa với phê duyệt.
  5. Claude Code hành động theo kết quả - Claude Code đọc quyết định JSON, chặn lệnh gọi tool, và cho Claude thấy lý do.

Phần Cấu hình bên dưới ghi đầy đủ schema, và mỗi phần sự kiện hook mô tả input mà lệnh của bạn nhận và output nó có thể trả về.

Hooks được định nghĩa trong các file settings JSON. Cấu hình có ba tầng lồng nhau:

  1. Chọn một sự kiện hook để phản ứng, như PreToolUse hay Stop
  2. Thêm một nhóm matcher để lọc thời điểm nó chạy, ví dụ “chỉ cho tool Bash”
  3. Định nghĩa một hoặc nhiều hook handler chạy khi khớp

Xem Cách một hook được giải quyết ở trên để có một ví dụ đầy đủ, có chú giải.

Nơi bạn định nghĩa một hook quyết định phạm vi của nó:

Vị tríPhạm viChia sẻ được không
~/.claude/settings.jsonMọi dự án của bạnKhông, chỉ máy này
.claude/settings.jsonMột dự ánCó, commit được vào repo
.claude/settings.local.jsonMột dự ánKhông, gitignore khi Claude Code lưu setting vào đó
Managed policy settingsToàn tổ chứcCó, admin kiểm soát
hooks/hooks.json của PluginKhi plugin được bậtCó, đóng gói cùng plugin
Frontmatter Skill hoặc agentTrong lúc component đang hoạt độngCó, định nghĩa trong file component

Xem thêm settings để biết chi tiết cách các file settings được phân giải.

Hooks từ file settings, managed policy settings, và plugin cũng chạy bên trong subagent. Khi một subagent gọi tool, các sự kiện tool như PreToolUsePostToolUse chạy cùng hooks đã cấu hình như trong hội thoại chính, và input mang thêm field agent_idagent_type (xem field input chung) để nhận diện subagent.

Quản trị viên enterprise có thể dùng allowManagedHooksOnly để chặn hooks từ user, project, và plugin. Hooks từ plugin được ép bật trong managed settings qua enabledPlugins được miễn trừ, nên admin có thể phân phối hooks đã kiểm duyệt qua một marketplace nội bộ tổ chức.

Các mục hook được gộp giữa các tầng settings thay vì thay thế lẫn nhau: settings ở user, project, local thêm hooks riêng mà không xóa hooks managed, và setting disableAllHooks không thể tắt hooks managed nếu không đặt ở chính tầng managed.

Allowlist HTTP hook áp dụng cho hooks từ mọi nguồn, kể cả managed policy settings:

  • allowedHttpHookUrls: khi được định nghĩa ở bất kỳ tầng settings nào, Claude Code chỉ chạy một HTTP hook handler nếu URL của nó khớp allowlist đã gộp
  • httpHookAllowedEnvVars: khi được định nghĩa, Claude Code chỉ nội suy các biến môi trường nằm trong danh sách đó vào header hook

Field matcher lọc thời điểm hooks chạy. Cách một matcher được đánh giá phụ thuộc vào ký tự nó chứa:

Giá trị matcherĐược đánh giá nhưVí dụ
"*", "", hoặc bỏ trốngKhớp tất cảchạy ở mọi lần xảy ra sự kiện
Chỉ chữ, số, _, -, khoảng trắng, ,, và |Chuỗi chính xác, hoặc danh sách chuỗi chính xác ngăn bởi | hoặc , (khoảng trắng bao quanh được cho phép)Bash chỉ khớp tool Bash; Edit|WriteEdit, Write đều khớp một trong hai tool; code-reviewer chỉ khớp đúng loại agent đó
Chứa ký tự khácRegular expression JavaScript, không neo (unanchored)^Notebook khớp mọi tool có tên bắt đầu bằng Notebook; mcp__memory__.* khớp mọi tool từ server memory

Một matcher trên nhánh regex được kiểm bằng RegExp.prototype.test của JavaScript, khớp thành công khi có khớp ở bất kỳ đâu trong giá trị. Edit.* khớp cả Edit lẫn NotebookEdit; bọc mẫu trong ^$, như ^Edit$, khi cần khớp toàn chuỗi.

Dấu phẩy làm ký tự phân tách và khoảng trắng bao quanh được cho phép yêu cầu Claude Code v2.1.191 trở lên. Dấu gạch ngang trong tập ký tự khớp-chính-xác yêu cầu v2.1.195 trở lên - ở phiên bản cũ hơn, một tên có gạch ngang như code-reviewer được đánh giá như regex không neo, nên cũng khớp senior-code-reviewer; neo lại bằng ^code-reviewer$ để chỉ khớp đúng tên đó.

FileChangedStopFailure dùng tập khớp-chính-xác hẹp hơn: chỉ chữ, số, _, và |. Dấu gạch ngang, khoảng trắng, hoặc dấu phẩy trong matcher của hai sự kiện này khiến nó rơi vào nhánh regex, và chỉ | phân tách các lựa chọn. Mọi sự kiện khác có hỗ trợ matcher trong bảng dưới đây chấp nhận cả | lẫn ,.

Sự kiện FileChanged không theo các quy tắc này khi xây danh sách theo dõi - xem FileChanged.

Mỗi loại sự kiện khớp trên một field khác nhau:

Sự kiệnMatcher lọc theoVí dụ giá trị matcher
PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDeniedtên toolBash, Edit|Write, mcp__.*
SessionStartcách phiên bắt đầustartup, resume, clear, compact, fork
Setupflag CLI nào kích hoạt setupinit, maintenance
SessionEndlý do phiên kết thúcclear, resume, logout, prompt_input_exit, bypass_permissions_disabled, other
Notificationloại thông báopermission_prompt, idle_prompt, auth_success, elicitation_dialog, elicitation_complete, elicitation_response, agent_needs_input, agent_completed
SubagentStartloại agentgeneral-purpose, Explore, Plan, tên agent tùy chỉnh, hoặc tên phạm vi plugin như ^my-plugin:reviewer$
PreCompact, PostCompactđiều gì kích hoạt nénmanual, auto
SubagentStoploại agentgiống giá trị của SubagentStart
ConfigChangenguồn cấu hìnhuser_settings, project_settings, local_settings, policy_settings, skills
CwdChangedkhông hỗ trợ matcherluôn chạy ở mọi lần đổi thư mục
FileChangedtên file literal cần theo dõi (xem FileChanged).envrc|.env
StopFailureloại lỗirate_limit, overloaded, authentication_failed, oauth_org_not_allowed, billing_error, invalid_request, model_not_found, server_error, max_output_tokens, unknown
InstructionsLoadedlý do nạpsession_start, nested_traversal, path_glob_match, include, compact
UserPromptExpansiontên lệnhtên skill hoặc command của bạn
Elicitationtên MCP servertên MCP server bạn đã cấu hình
ElicitationResulttên MCP servergiống Elicitation
UserPromptSubmit, PostToolBatch, Stop, TeammateIdle, TaskCreated, TaskCompleted, WorktreeCreate, WorktreeRemove, MessageDisplaykhông hỗ trợ matcherluôn chạy ở mọi lần xảy ra

Matcher chạy trên một field trích từ JSON input mà Claude Code gửi cho hook qua stdin. Với sự kiện tool, field đó là tool_name. Mỗi phần sự kiện hook liệt kê đầy đủ giá trị matcher và input schema cho sự kiện đó.

Ví dụ này chỉ chạy script lint khi Claude ghi hoặc sửa một file:

{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "/path/to/lint-check.sh"
}
]
}
]
}
}

UserPromptSubmit, PostToolBatch, Stop, TeammateIdle, TaskCreated, TaskCompleted, WorktreeCreate, WorktreeRemove, MessageDisplay, và CwdChanged không hỗ trợ matcher và luôn chạy ở mọi lần xảy ra. Nếu bạn thêm field matcher vào các sự kiện này, nó bị bỏ qua âm thầm.

Với sự kiện tool, bạn có thể lọc hẹp hơn bằng field if trên từng hook handler. if dùng cú pháp quy tắc quyền để khớp cả tên tool lẫn tham số cùng lúc, nên "Bash(git *)" chạy khi bất kỳ subcommand nào của Bash input khớp git *, và "Edit(*.ts)" chỉ chạy cho file TypeScript.

Tool từ MCP server xuất hiện như tool thông thường trong các sự kiện tool (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied), nên bạn khớp chúng giống như bất kỳ tool nào khác.

Tool MCP theo mẫu đặt tên mcp__<server>__<tool>, ví dụ:

  • mcp__memory__create_entities: tool tạo entity của server Memory
  • mcp__filesystem__read_file: tool đọc file của server Filesystem
  • mcp__github__search_repositories: tool tìm kiếm của server GitHub

Để khớp mọi tool từ một server, thêm .* vào sau tiền tố server. .* là bắt buộc: một matcher như mcp__memory hay mcp__brave-search chỉ chứa ký tự khớp-chính-xác, nên được so sánh như chuỗi chính xác và không khớp tool nào.

  • mcp__memory__.* khớp mọi tool từ server memory
  • mcp__brave-search__.* khớp mọi tool từ một server có tên chứa gạch ngang
  • mcp__.*__write.* khớp bất kỳ tool nào có tên bắt đầu bằng write từ mọi server

Dấu gạch ngang trong tập khớp-chính-xác yêu cầu v2.1.195 trở lên; ở phiên bản cũ hơn một tiền tố có gạch ngang trần như mcp__brave-search được đánh giá như regex không neo và khớp mọi tool từ server đó. Dạng mcp__brave-search__.* hoạt động ở mọi phiên bản.

Tool từ một MCP server đóng gói trong plugin dùng đoạn tên server có phạm vi gồm cả tên plugin: mcp__plugin_<tên-plugin>_<tên-server>__<tool>. Matcher viết theo server key trần sẽ không bao giờ khớp các tool này. Với plugin tên my-plugin đóng gói server dưới key db, tool query xuất hiện dưới dạng mcp__plugin_my-plugin_db__query, nên matcher cho mọi tool từ server đó là mcp__plugin_my-plugin_db__.*. Dùng cùng tên tool có phạm vi này trong field if của handler.

Ví dụ này ghi log mọi thao tác của server memory và kiểm tra thao tác ghi từ bất kỳ MCP server nào:

{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__memory__.*",
"hooks": [
{
"type": "command",
"command": "echo 'Memory operation initiated' >> ~/mcp-operations.log"
}
]
},
{
"matcher": "mcp__.*__write.*",
"hooks": [
{
"type": "command",
"command": "/home/user/scripts/validate-mcp-write.py"
}
]
}
]
}
}

Mỗi object trong mảng hooks bên trong là một hook handler: lệnh shell, HTTP endpoint, MCP tool, LLM prompt, hoặc agent chạy khi matcher khớp. Có năm loại:

  • Command hook (type: "command"): chạy một lệnh shell. Script của bạn nhận JSON input của sự kiện qua stdin và báo kết quả qua exit code cùng stdout.
  • HTTP hook (type: "http"): gửi JSON input của sự kiện dưới dạng request POST tới một URL. Endpoint báo kết quả qua response body, dùng cùng định dạng JSON output như command hook.
  • MCP tool hook (type: "mcp_tool"): gọi một tool trên MCP server đã kết nối sẵn. Text output của tool được xử lý như stdout của command hook.
  • Prompt hook (type: "prompt"): gửi một prompt tới model Claude để đánh giá một lượt. Model trả về quyết định có/không dạng JSON. Xem Hook dựa trên prompt.
  • Agent hook (type: "agent"): sinh một subagent có thể dùng tool như Read, Grep, Glob để xác minh điều kiện trước khi trả quyết định. Agent hook đang ở dạng thử nghiệm, có thể thay đổi. Xem Hook dựa trên agent.

Mọi hook khớp chạy song song, và các handler giống hệt nhau được loại trùng tự động. Command hook loại trùng theo chuỗi command và args; HTTP hook loại trùng theo URL.

Handler chạy trong thư mục hiện tại với môi trường của Claude Code. Biến $CLAUDE_CODE_REMOTE được đặt "true" trong môi trường web từ xa và không được đặt trong CLI local. Kể từ v2.1.199, $CLAUDE_CODE_BRIDGE_SESSION_ID được đặt thành ID phiên Remote Control khi phiên local có kết nối Remote Control đang hoạt động.

Các field này áp dụng cho mọi loại hook:

FieldBắt buộcMô tả
type"command", "http", "mcp_tool", "prompt", hoặc "agent"
ifkhôngCú pháp quy tắc quyền để lọc thời điểm hook này chạy, như "Bash(git *)" hay "Edit(*.ts)". Lệnh hook chỉ chạy khi lệnh gọi tool khớp mẫu. Xem bảng khớp Bash bên dưới về cách mẫu Bash được đánh giá với subcommand, $(), và backtick. Chỉ được đánh giá trên sự kiện tool: PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, và PermissionDenied. Ở sự kiện khác, một hook có đặt if sẽ không bao giờ chạy. Dùng cùng cú pháp với quy tắc quyền
timeoutkhôngSố giây trước khi hủy. Mặc định: 600 cho command, http, mcp_tool; 30 cho prompt; 60 cho agent. UserPromptSubmit hạ mặc định của command, http, mcp_tool xuống 30, và MessageDisplay hạ xuống 10. Hooks của SessionEnd chia sẻ ngân sách 1,5 giây; nếu settings đặt timeout dài hơn cho từng hook, Claude Code nâng ngân sách lên tương ứng, tối đa 60 giây
statusMessagekhôngThông điệp spinner tùy chỉnh hiển thị trong lúc hook chạy
oncekhôngNếu true, chạy một lần mỗi phiên rồi bị gỡ bỏ. Chỉ có hiệu lực với hooks khai báo trong frontmatter của skill; bị bỏ qua trong file settings và frontmatter agent

Field if chỉ chứa đúng một quy tắc quyền. Không có &&, ||, hay cú pháp danh sách để kết hợp quy tắc; muốn áp nhiều điều kiện thì định nghĩa một handler riêng cho mỗi điều kiện.

Trong một điều kiện if cho tool file, một mẫu thư mục một-đoạn như "Edit(src/**)" chỉ khớp thư mục src ngay trong thư mục làm việc và các file bên dưới nó. Để khớp thư mục tên src ở bất kỳ độ sâu nào, viết "Edit(**/src/**)". Trước v2.1.214, "Edit(src/**)" khớp thư mục tên src ở bất kỳ độ sâu nào dưới thư mục làm việc.

Với mẫu Bash, việc lệnh hook của bạn có chạy hay không phụ thuộc vào hình dạng của mẫu và lệnh Bash mà Claude đang gọi. Các phép gán VAR=value ở đầu bị loại bỏ trước khi khớp.

Mẫu ifLệnh BashHook có chạy?Vì sao
Bash(git *)FOO=bar git pushphép gán đầu bị loại; git push khớp
Bash(git *)npm test && git pushmỗi subcommand được kiểm tra; git push khớp
Bash(rm *)echo $(rm -rf /)lệnh bên trong $() và backtick cũng được kiểm tra; rm -rf / khớp
Bash(rm *)echo $(date)khôngkhông có subcommand nào khớp rm *
Bash(git push *)echo $(date)mẫu chỉ định nhiều hơn tên lệnh vẫn chạy hook trên $(), backtick, hoặc $VAR

Bộ lọc cũng “fail open” - chạy hook của bạn bất kể mẫu - khi không phân tích được lệnh Bash. Vì if chỉ mang tính cố gắng tốt nhất (best-effort), hãy dùng hệ thống quyền thay vì hook để ép buộc allow/deny cứng.

Ngoài field chung, command hook chấp nhận các field sau:

FieldBắt buộcMô tả
commandLệnh shell để chạy. Kèm args, đây là executable được spawn trực tiếp. Xem Dạng exec và dạng shell
argskhôngDanh sách tham số. Khi có, command được phân giải như một executable và spawn trực tiếp với args làm vector tham số, không qua shell. Xem Dạng exec và dạng shell
asynckhôngNếu true, chạy ở background không chặn luồng chính. Xem Chạy hook ở background
asyncRewakekhôngNếu true, chạy ở background và đánh thức Claude khi exit code là 2. Ngầm định async. Stderr của hook (hoặc stdout nếu stderr rỗng) được hiện cho Claude dưới dạng system reminder để nó phản ứng với lỗi background chạy lâu
shellkhôngShell dùng cho hook này. Nhận "bash" hoặc "powershell". Mặc định "bash", hoặc "powershell" trên Windows khi không cài Git Bash. Đặt "powershell" chạy lệnh qua PowerShell trên Windows. Không cần CLAUDE_CODE_USE_POWERSHELL_TOOL vì hooks spawn PowerShell trực tiếp. Bị bỏ qua khi có args

Một command hook chạy ở dạng exec khi có args, và dạng shell khi không có args. Đặt args bất cứ khi nào hook tham chiếu tới một placeholder đường dẫn, vì mỗi phần tử được truyền như một tham số riêng, không cần quote. Bỏ args khi bạn cần tính năng shell như pipe hay &&, hoặc khi không cần cả hai điều trên.

Dạng exec chạy khi có args. Claude Code phân giải command như một executable trên PATH và spawn trực tiếp với args làm vector tham số. Không có shell, nên mỗi phần tử args là đúng một tham số như đã viết, và các placeholder đường dẫn như ${CLAUDE_PLUGIN_ROOT} được thay thế vào command và từng phần tử args như chuỗi trần. Ký tự đặc biệt như dấu nháy đơn, $, backtick đi qua nguyên vẹn vì không có shell diễn giải chúng. Không có tokenization của shell trên bất kỳ nền tảng nào.

Dạng shell chạy khi không có args. Chuỗi command được truyền cho một shell: sh -c trên macOS và Linux, Git Bash trên Windows, hoặc PowerShell khi không cài Git Bash. Đặt field shell để chọn tường minh. Shell tokenize chuỗi, mở rộng biến, và diễn giải pipe, &&, redirect, glob.

Ví dụ chạy một script Node đóng gói cùng plugin. Dạng exec truyền đường dẫn script đã phân giải như một tham số duy nhất, không cần quote:

{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PLUGIN_ROOT}/scripts/format.js", "--fix"]
}

Dạng shell tương đương cần quote để xử lý đường dẫn có khoảng trắng hoặc ký tự đặc biệt:

{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}\"/scripts/format.js --fix"
}

Cả hai dạng đều hỗ trợ cùng placeholder đường dẫn, và cả hai đều export chúng dưới dạng biến môi trường CLAUDE_PROJECT_DIR, CLAUDE_PLUGIN_ROOT, và CLAUDE_PLUGIN_DATA trên tiến trình được spawn, nên script có thể đọc process.env.CLAUDE_PLUGIN_ROOT bất kể được khởi chạy cách nào.

Plugin hook còn thay thế thêm giá trị ${user_config.*}, chỉ ở dạng exec: giá trị được thay vào command và từng phần tử args như chuỗi trần, nên không có shell nào phân tích lại nó.

Một plugin hook dạng shell mà command tham chiếu ${user_config.*} sẽ lỗi thay vì chạy. Để dùng giá trị option từ một hook dạng shell, đọc biến môi trường $CLAUDE_PLUGIN_OPTION_<KEY>, ví dụ $CLAUDE_PLUGIN_OPTION_WEBHOOK_URL cho option webhook_url, hoặc đặt args để chuyển hook sang dạng exec. Trước v2.1.207, lệnh plugin hook dạng shell cũng thay thế ${user_config.*}.

Ngoài field chung, HTTP hook chấp nhận các field sau:

FieldBắt buộcMô tả
urlURL để gửi request POST
headerskhôngHeader HTTP bổ sung dạng cặp key-value. Giá trị hỗ trợ nội suy biến môi trường qua cú pháp $VAR_NAME hoặc ${VAR_NAME}. Chỉ biến nằm trong allowedEnvVars mới được phân giải
allowedEnvVarskhôngDanh sách tên biến môi trường được phép nội suy vào giá trị header. Tham chiếu tới biến không nằm trong danh sách bị thay bằng chuỗi rỗng. Bắt buộc để nội suy biến môi trường hoạt động

Claude Code gửi JSON input của hook làm body của request POST với Content-Type: application/json. Response body dùng cùng định dạng JSON output như command hook.

Xử lý lỗi khác với command hook: response không phải 2xx, lỗi kết nối, và timeout đều tạo ra lỗi không-chặn (non-blocking), cho phép thực thi tiếp tục. Để chặn một lệnh gọi tool hoặc từ chối một quyền, trả về response 2xx với body JSON chứa decision: "block" hoặc hookSpecificOutput với permissionDecision: "deny".

Ví dụ này gửi sự kiện PreToolUse tới một dịch vụ xác thực nội bộ, xác thực bằng token từ biến môi trường MY_TOKEN:

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "http",
"url": "http://localhost:8080/hooks/pre-tool-use",
"timeout": 30,
"headers": {
"Authorization": "Bearer $MY_TOKEN"
},
"allowedEnvVars": ["MY_TOKEN"]
}
]
}
]
}
}

Ngoài field chung, MCP tool hook chấp nhận các field sau:

FieldBắt buộcMô tả
serverTên một MCP server đã cấu hình. Với server đóng gói trong plugin, đây là tên có phạm vi plugin:<tên-plugin>:<tên-server>, ví dụ plugin:my-plugin:db, không phải server key trần. Server phải đã kết nối sẵn; hook không bao giờ tự kích hoạt luồng OAuth hay kết nối
toolTên tool cần gọi trên server đó
inputkhôngTham số truyền cho tool. Giá trị chuỗi hỗ trợ thay thế ${path} từ JSON input của hook, ví dụ "${tool_input.file_path}"

Nội dung text của tool được xử lý như stdout của command hook: nếu nó phân tích được thành JSON output hợp lệ thì được xử lý như một quyết định, ngược lại nó được hiển thị như văn bản thuần. Nếu server được nêu tên chưa kết nối, hoặc tool trả về isError: true, hook tạo ra lỗi không-chặn và thực thi tiếp tục.

MCP tool hook khả dụng trên mọi sự kiện hook, sau khi Claude Code đã kết nối tới MCP server của bạn. SessionStartSetup thường xảy ra trước khi server kết nối xong, nên hooks trên các sự kiện đó nên lường trước lỗi “not connected” ở lần chạy đầu.

Ví dụ này gọi tool security_scan trên MCP server my_server sau mỗi lần Write hoặc Edit, truyền đường dẫn file đã sửa:

{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "mcp_tool",
"server": "my_server",
"tool": "security_scan",
"input": { "file_path": "${tool_input.file_path}" }
}
]
}
]
}
}

Ngoài field chung, prompt hook và agent hook chấp nhận các field sau:

FieldBắt buộcMô tả
promptNội dung prompt gửi cho model. Dùng $ARGUMENTS làm placeholder cho JSON input của hook. Escape bằng backslash để có text literal: \$1.00 hiển thị thành $1.00
modelkhôngModel dùng để đánh giá. Mặc định là một model nhanh

Dùng các placeholder này để tham chiếu script hook tương đối với gốc project hoặc plugin, bất kể thư mục làm việc lúc hook chạy là gì:

  • ${CLAUDE_PROJECT_DIR}: gốc project. Claude Code cũng đặt biến này trong môi trường của MCP server dạng stdio và LSP server của plugin.
  • ${CLAUDE_PLUGIN_ROOT}: thư mục cài đặt của plugin, cho script đóng gói cùng một plugin. Thay đổi mỗi lần plugin cập nhật.
  • ${CLAUDE_PLUGIN_DATA}: thư mục dữ liệu bền vững của plugin, cho dependency và state cần sống sót qua các lần cập nhật plugin.

Ưu tiên dạng exec cho bất kỳ hook nào tham chiếu placeholder đường dẫn. Dạng exec truyền mỗi phần tử args như một tham số, không cần shell tokenize, nên đường dẫn có khoảng trắng hay ký tự đặc biệt không cần quote. Ở dạng shell, bọc mỗi placeholder trong dấu ngoặc kép.

Ví dụ dùng ${CLAUDE_PROJECT_DIR} để chạy một style checker từ thư mục .claude/hooks/ của project sau mỗi lệnh gọi tool Write hoặc Edit:

{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-style.sh",
"args": []
}
]
}
]
}
}

Hook của plugin được định nghĩa trong hooks/hooks.json với field description tùy chọn ở cấp cao nhất. Khi một plugin được bật, hooks của nó gộp với hooks của user và project. Ví dụ chạy một script format đóng gói cùng plugin:

{
"description": "Automatic code formatting",
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
"args": [],
"timeout": 30
}
]
}
]
}
}

Xem tham chiếu component của plugin để biết chi tiết tạo plugin hooks.

Ngoài file settings và plugin, hooks có thể được định nghĩa trực tiếp trong skillsubagent qua frontmatter. Các hook này gắn phạm vi với vòng đời của component và chỉ chạy khi component đó đang hoạt động.

Mọi sự kiện hook đều được hỗ trợ. Với subagent, hook Stop tự động được chuyển thành SubagentStop vì đó là sự kiện chạy khi một subagent hoàn tất.

Hooks dùng chung định dạng cấu hình với hooks dựa trên settings nhưng gắn phạm vi với vòng đời của component và được dọn dẹp khi component kết thúc.

Skill sau định nghĩa một PreToolUse hook chạy script kiểm tra bảo mật trước mỗi lệnh Bash:

---
name: secure-operations
description: Perform operations with security checks
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/security-check.sh"
---

Subagent dùng cùng định dạng trong frontmatter YAML của chúng.

Kể từ v2.1.218, frontmatter hooks trong một project subagent chỉ chạy sau khi bạn chấp nhận hộp thoại workspace trust cho thư mục chứa file agent đó. Trước v2.1.218, các hook này có thể chạy từ những thư mục bạn chưa tin cậy.

/hooks trong Claude Code để mở trình duyệt chỉ-xem cho các hook đã cấu hình. Menu hiện mọi sự kiện hook kèm số lượng hook đã cấu hình, cho phép bạn đi sâu vào từng matcher, và hiện chi tiết đầy đủ của mỗi hook handler. Dùng nó để xác minh cấu hình, kiểm tra hook nào tới từ file settings nào, hoặc xem lệnh, prompt, hay URL của một hook.

Menu hiện cả năm loại hook: command, prompt, agent, http, và mcp_tool. Mỗi hook được gắn nhãn tiền tố [type] và nguồn cho biết nó được định nghĩa ở đâu:

  • User Settings: từ ~/.claude/settings.json
  • Project Settings: từ .claude/settings.json
  • Local Settings: từ .claude/settings.local.json
  • Plugin Hooks: từ hooks/hooks.json của một plugin
  • Session Hooks: đăng ký trong bộ nhớ cho phiên hiện tại
  • Built-in Hooks: đăng ký nội bộ bởi Claude Code

Chọn một hook mở view chi tiết hiện sự kiện, matcher, loại, file nguồn, và toàn bộ lệnh, prompt, hoặc URL. Menu chỉ-xem: muốn thêm, sửa, hay xóa hooks, sửa trực tiếp JSON settings hoặc nhờ Claude thực hiện thay đổi.

Để xóa một hook, xóa mục của nó trong file settings JSON.

Để tạm tắt toàn bộ hooks mà không xóa chúng, đặt "disableAllHooks": true trong file settings của bạn. Không có cách nào tắt riêng một hook trong khi vẫn giữ nó trong cấu hình.

Setting disableAllHooks tôn trọng hệ thống phân cấp managed settings. Nếu quản trị viên đã cấu hình hooks qua managed policy settings, disableAllHooks đặt ở user, project, hoặc local settings không thể tắt các hooks managed đó. Chỉ disableAllHooks đặt ở tầng managed settings mới tắt được hooks managed.

Chỉnh sửa trực tiếp hooks trong file settings thường được file watcher tự động phát hiện.

Command hook nhận dữ liệu JSON qua stdin và báo kết quả qua exit code, stdout, và stderr. HTTP hook nhận cùng JSON đó dưới dạng body của request POST và báo kết quả qua HTTP response body. Phần này bao quát các field và hành vi chung cho mọi sự kiện. Mỗi phần Sự kiện hook bên dưới có input schema riêng và tùy chọn kiểm soát quyết định cho sự kiện đó.

Trên macOS và Linux, command hook chạy trong session riêng không có controlling terminal kể từ v2.1.139. Tiến trình hook và mọi tiến trình con không thể mở /dev/tty hay gửi escape sequence trực tiếp tới giao diện Claude Code. Windows không có /dev/tty. Để hiện thông điệp cho người dùng trên mọi nền tảng, trả về systemMessage trong JSON output. Để kích hoạt thông báo desktop, đặt tiêu đề cửa sổ, hoặc rung chuông, trả về terminalSequence thay vào đó.

Sự kiện hook nhận các field này dưới dạng JSON, thêm vào các field đặc thù cho sự kiện được ghi trong mỗi phần sự kiện hook. Với command hook, JSON này tới qua stdin; với HTTP hook, nó tới dưới dạng body của request POST.

FieldMô tả
session_idĐịnh danh phiên hiện tại
prompt_idUUID định danh prompt người dùng đang được xử lý. Khớp với attribute prompt.id trên sự kiện OpenTelemetry, để bạn tương quan output hook với telemetry cho một prompt cụ thể. Vắng mặt cho tới lần input đầu tiên của người dùng. Yêu cầu Claude Code v2.1.196 trở lên
transcript_pathĐường dẫn tới JSON hội thoại. File transcript được ghi bất đồng bộ nên có thể trễ so với hội thoại trong bộ nhớ, nghĩa là có thể chưa gồm tin nhắn gần nhất của lượt hiện tại khi hook chạy. Hooks cần text cuối cùng của assistant trong lượt hiện tại nên dùng last_assistant_message trên StopSubagentStop thay vì đọc transcript
cwdThư mục làm việc hiện tại khi hook được gọi
permission_modeChế độ quyền hiện tại: "default", "plan", "acceptEdits", "auto", "dontAsk", hoặc "bypassPermissions". Chế độ nhãn Manual tới dưới dạng "default", không bao giờ là "manual", nên script khớp "default" vẫn hoạt động đúng. Không phải sự kiện nào cũng nhận field này - kiểm tra ví dụ JSON trong mỗi phần sự kiện hook
effortObject có field level chứa mức effort đang hoạt động cho lượt này: "low", "medium", "high", "xhigh", hoặc "max". Nếu effort yêu cầu vượt quá mức model hiện tại hỗ trợ, đây là mức đã bị hạ mà model thực sự dùng. Ultracode không phải mức riêng, báo cáo là "xhigh". Có mặt ở các sự kiện xảy ra trong ngữ cảnh dùng tool, như PreToolUse, PostToolUse, Stop, và SubagentStop, khi model hiện tại hỗ trợ tham số effort. Mức này cũng khả dụng cho lệnh hook và tool Bash qua biến môi trường $CLAUDE_EFFORT
hook_event_nameTên sự kiện đã xảy ra

Khi chạy với --agent hoặc bên trong một subagent, có thêm hai field:

FieldMô tả
agent_idĐịnh danh duy nhất cho subagent. Chỉ có mặt khi hook chạy bên trong một lệnh gọi subagent. Dùng để phân biệt lệnh gọi hook của subagent với lệnh gọi ở luồng chính
agent_typeTên agent (ví dụ "Explore" hoặc "security-reviewer"). Có mặt khi phiên dùng --agent hoặc hook chạy bên trong subagent. Với subagent, loại của subagent được ưu tiên hơn giá trị --agent của phiên. Với subagent tùy chỉnh, đây là field name trong frontmatter của agent, không phải tên file. Với subagent do một plugin cung cấp, đây là định danh có phạm vi plugin như my-plugin:reviewer, không phải tên frontmatter trần

Chỉ hook SessionStart mới có thể nhận field model, và không đảm bảo luôn có mặt. Không có biến môi trường $CLAUDE_MODEL. Một tiến trình hook kế thừa môi trường cha, nên có thể đọc $ANTHROPIC_MODEL nếu bạn đặt nó trong shell, nhưng giá trị đó không đổi khi bạn chuyển model bằng /model trong phiên. Có một nhóm biến không được kế thừa: Claude Code loại bỏ các biến export OTEL_* khỏi mọi subprocess nó spawn, kể cả hooks.

Ví dụ, một PreToolUse hook cho lệnh Bash nhận trên stdin:

{
"session_id": "abc123",
"prompt_id": "550e8400-e29b-41d4-a716-446655440000",
"transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
"cwd": "/home/user/my-project",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test",
"description": "Run test suite",
"timeout": 120000,
"run_in_background": false
},
"tool_use_id": "toolu_01ABC123..."
}

Các field tool_name, tool_input, và tool_use_id là đặc thù cho sự kiện. Mỗi phần sự kiện hook ghi các field bổ sung cho sự kiện đó.

Exit code từ lệnh hook của bạn báo cho Claude Code biết hành động nên tiếp tục, bị chặn, hay bị bỏ qua.

Exit 0 nghĩa là thành công. Claude Code phân tích stdout tìm field JSON output. JSON output chỉ được xử lý khi exit 0. Với hầu hết sự kiện, stdout được ghi vào debug log nhưng không hiện trong transcript. Ngoại lệ là UserPromptSubmit, UserPromptExpansion, và SessionStart, nơi stdout được thêm làm context mà Claude có thể thấy và hành động theo.

Exit 2 nghĩa là lỗi chặn (blocking). Claude Code bỏ qua stdout và mọi JSON trong đó. Thay vào đó, text ở stderr được đưa lại cho Claude như một thông điệp lỗi. Hiệu ứng tùy sự kiện: PreToolUse chặn lệnh gọi tool, UserPromptSubmit từ chối prompt, v.v. Xem hành vi exit code 2 theo từng sự kiện để có danh sách đầy đủ.

Một hook exit 2 trong khi in JSON không qua được validation schema của JSON output vẫn chặn: Claude Code dùng stderr làm lý do chặn và ghi lỗi validation vào debug log. Trước v2.1.214, Claude Code coi kết hợp này là lỗi không-chặn và hành động vẫn tiếp tục.

Bất kỳ exit code nào khác là lỗi không-chặn với hầu hết sự kiện hook. Transcript hiện thông báo <hook name> hook error kèm dòng đầu của stderr, để bạn nhận diện nguyên nhân mà không cần --debug. Thực thi tiếp tục và toàn bộ stderr được ghi vào debug log.

Ví dụ, một script hook chặn lệnh Bash nguy hiểm:

#!/bin/bash
# Đọc JSON input từ stdin, kiểm tra lệnh
input=$(cat)
command=$(jq -r '.tool_input.command' <<<"$input")
if [[ "$command" == rm* ]]; then
echo "Blocked: rm commands are not allowed" >&2
exit 2 # Lỗi chặn: lệnh gọi tool bị ngăn
fi
exit 0 # Không có quyết định: luồng permission bình thường áp dụng

Exit code 2 là cách một hook báo “dừng lại, đừng làm điều này”. Hiệu ứng tùy sự kiện, vì một số sự kiện đại diện cho hành động có thể chặn (như một lệnh gọi tool chưa xảy ra), số khác đại diện cho điều đã xảy ra rồi hoặc không thể ngăn.

Sự kiện hookChặn được?Điều gì xảy ra khi exit 2
PreToolUseChặn lệnh gọi tool
PermissionRequestTừ chối quyền
UserPromptSubmitChặn xử lý prompt và xóa prompt
UserPromptExpansionChặn việc mở rộng
StopNgăn Claude dừng, tiếp tục hội thoại
SubagentStopNgăn subagent dừng
TeammateIdleNgăn teammate chuyển sang idle, để nó tiếp tục làm việc
TaskCreatedRollback việc tạo task
TaskCompletedNgăn task được đánh dấu hoàn thành
ConfigChangeChặn thay đổi cấu hình có hiệu lực (trừ policy_settings)
StopFailureKhôngOutput và exit code bị bỏ qua
PostToolUseKhôngHiện stderr cho Claude; tool đã chạy rồi
PostToolUseFailureKhôngHiện stderr cho Claude; tool đã thất bại rồi
PostToolBatchDừng vòng lặp agentic trước lệnh gọi model tiếp theo
PermissionDeniedKhôngExit code và stderr bị bỏ qua vì việc từ chối đã xảy ra rồi. Dùng JSON hookSpecificOutput.retry: true để báo model có thể thử lại
NotificationKhôngHiện stderr chỉ cho người dùng
SubagentStartKhôngHiện stderr chỉ cho người dùng
SessionStartKhôngHiện stderr chỉ cho người dùng
SetupKhôngHiện stderr chỉ cho người dùng
SessionEndKhôngHiện stderr chỉ cho người dùng
CwdChangedKhôngHiện stderr chỉ cho người dùng
FileChangedKhôngHiện stderr chỉ cho người dùng
PreCompactChặn việc nén
PostCompactKhôngHiện stderr chỉ cho người dùng
ElicitationTừ chối elicitation
ElicitationResultChặn phản hồi (hành động trở thành decline)
WorktreeCreateBất kỳ exit code khác 0 nào cũng khiến việc tạo worktree thất bại
WorktreeRemoveKhôngLỗi chỉ được ghi log ở chế độ debug
InstructionsLoadedKhôngExit code bị bỏ qua
MessageDisplayKhôngText gốc vẫn được hiển thị

Với SessionStart, Setup, và SubagentStart, stderr của exit code 2 hiện trong transcript như một thông báo <hook name> hook error, giống lỗi không-chặn. Claude không thấy nó, và phiên hoặc subagent vẫn tiếp tục. Với SubagentStart, thông báo hiện trong transcript riêng của subagent, không phải hội thoại cha.

Kể từ Claude Code v2.1.199, SessionStart, Setup, và SubagentStart hiện stderr của exit code 2 trong transcript. Phiên bản trước đó chỉ ghi vào debug log.

HTTP hook dùng mã trạng thái HTTP và response body thay vì exit code và stdout:

  • 2xx với body rỗng: thành công, tương đương exit code 0 không có output
  • 2xx với body text thuần: thành công, text được thêm làm context
  • 2xx với body JSON: thành công, được phân tích dùng cùng schema JSON output như command hook
  • Status không phải 2xx: lỗi không-chặn, thực thi tiếp tục
  • Lỗi kết nối hoặc timeout: lỗi không-chặn, thực thi tiếp tục

Khác với command hook, HTTP hook không thể báo lỗi chặn chỉ qua mã trạng thái. Để chặn một lệnh gọi tool hay từ chối một quyền, trả về response 2xx với body JSON chứa field quyết định phù hợp.

Exit code chỉ cho phép chặn hoặc im lặng, nhưng JSON output cho kiểm soát chi tiết hơn. Thay vì exit code 2 để chặn, exit 0 và in một object JSON ra stdout. Claude Code đọc các field cụ thể từ JSON đó để kiểm soát hành vi, gồm cả kiểm soát quyết định để chặn, cho phép, hoặc leo thang cho người dùng quyết định.

Stdout của hook chỉ được chứa object JSON. Nếu shell profile của bạn in text lúc khởi động, nó có thể gây nhiễu việc phân tích JSON.

Các chuỗi output của hook, gồm additionalContext, systemMessage, và stdout thuần, bị giới hạn ở 10.000 ký tự. Output vượt giới hạn này được lưu vào file và thay bằng bản xem trước cùng đường dẫn file, giống cách kết quả tool lớn được xử lý.

Object JSON hỗ trợ ba loại field:

  • Field phổ quát như continue hoạt động trên mọi sự kiện. Liệt kê trong bảng dưới.
  • decisionreason ở cấp cao nhất được một số sự kiện dùng để chặn hoặc phản hồi.
  • hookSpecificOutput là object lồng cho các sự kiện cần kiểm soát chi tiết hơn. Yêu cầu field hookEventName đặt bằng tên sự kiện.
FieldMặc địnhMô tả
continuetrueNếu false, Claude dừng xử lý hoàn toàn sau khi hook chạy. Ưu tiên hơn mọi field quyết định đặc thù sự kiện khác
stopReasonkhông cóThông điệp hiện cho người dùng khi continuefalse. Không hiện cho Claude
suppressOutputfalseNếu true, ẩn stdout của hook khỏi transcript. Stdout vẫn hiện trong debug log
systemMessagekhông cóThông điệp cảnh báo hiện cho người dùng
terminalSequencekhông cóMột chuỗi escape terminal để Claude Code phát thay bạn, như thông báo desktop, tiêu đề cửa sổ, hay chuông. Giới hạn ở OSC 0/1/2/9/99/777 và BEL. Nếu giá trị chứa gì đó ngoài allowlist, field bị bỏ qua. Dùng cái này thay vì ghi vào /dev/tty, vốn không khả dụng cho hooks

Để dừng Claude hoàn toàn bất kể loại sự kiện:

{ "continue": false, "stopReason": "Build failed, fix errors before continuing" }

Với hook PreToolUsePostToolUse, việc dừng áp dụng kể cả khi lệnh gọi tool thất bại hoặc hoàn tất trong lúc Claude vẫn đang stream phản hồi.

Field terminalSequence yêu cầu Claude Code v2.1.141 trở lên.

Hooks chạy không có controlling terminal, nên ghi escape sequence trực tiếp vào /dev/tty sẽ thất bại. Thay vào đó, trả về escape sequence trong field terminalSequence và Claude Code phát nó thay bạn qua đường ghi terminal riêng của nó. Cách này không có race condition, hoạt động trong tmux và GNU screen, và hoạt động trên Windows nơi không có /dev/tty.

Field này nhận một chuỗi gồm một hoặc nhiều escape sequence trong allowlist:

  • OSC 0, 1, 2: tiêu đề cửa sổ và icon
  • OSC 9: thông báo của iTerm2, ConEmu, Windows Terminal, và WezTerm, gồm cả 9;4 cho tiến độ trên taskbar
  • OSC 99: thông báo của Kitty
  • OSC 777: thông báo của urxvt, Ghostty, và Warp
  • BEL trần

Sequence có thể kết thúc bằng BEL hoặc ST. Bất cứ gì ngoài allowlist, gồm sequence con trỏ và màu CSI, sequence bảng màu OSC, hyperlink OSC 8, ghi clipboard OSC 52, và OSC 1337, đều bị từ chối và field bị bỏ qua.

Ví dụ dưới đây kích hoạt thông báo desktop từ một Notification hook. Escape sequence được dựng bằng octal escape của printf để các byte điều khiển không bao giờ xuất hiện trên dòng lệnh shell, và jq -n --arg dựng JSON output để dấu ngoặc kép, backslash, và newline trong thông điệp thông báo được escape đúng:

#!/bin/bash
# Notification hook: ping desktop khi Claude Code cần chú ý.
input=$(cat)
title="Claude Code"
body=$(jq -r '.message // "Needs your attention"' <<<"$input")
seq=$(printf '\033]777;notify;%s;%s\007' "$title" "$body")
jq -nc --arg seq "$seq" '{terminalSequence: $seq}'

Dạng { "terminalSequence": "..." } giống nhau từ bất kỳ shell hay ngôn ngữ nào. Trên Windows, dựng chuỗi escape trong PowerShell hoặc một script rồi phát cùng object JSON đó.

Field additionalContext truyền một chuỗi từ hook của bạn vào context window của Claude. Claude Code bọc chuỗi trong một system reminder và chèn nó vào hội thoại tại điểm hook chạy. Claude đọc reminder đó ở request model tiếp theo, nhưng nó không hiện như một tin nhắn chat trong giao diện.

Trả additionalContext bên trong hookSpecificOutput cùng với tên sự kiện:

{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "This file is generated. Edit src/schema.ts and run `bun generate` instead."
}
}

Nơi reminder xuất hiện tùy sự kiện:

Khi nhiều hook cùng trả additionalContext cho cùng một sự kiện, Claude nhận tất cả các giá trị. Nếu một giá trị vượt quá 10.000 ký tự, Claude Code ghi toàn bộ text vào một file trong thư mục phiên và cho Claude đường dẫn file kèm bản xem trước ngắn.

Dùng additionalContext cho thông tin Claude nên biết về trạng thái hiện tại của môi trường hoặc thao tác vừa chạy:

  • Trạng thái môi trường: branch hiện tại, deployment target, hay feature flag đang bật
  • Quy tắc project có điều kiện: lệnh test nào áp dụng cho file vừa sửa, thư mục nào chỉ-đọc trong worktree này
  • Dữ liệu bên ngoài: issue đang mở gán cho bạn, kết quả CI gần đây, nội dung lấy từ một dịch vụ nội bộ

Với hướng dẫn không bao giờ đổi, ưu tiên CLAUDE.md - nó nạp mà không cần chạy script và là nơi chuẩn cho quy ước project tĩnh.

Viết text dưới dạng phát biểu sự thật thay vì chỉ thị mệnh lệnh dạng hệ thống. Cách diễn đạt như “Deployment target hiện là production” hay “Repo này dùng bun test” đọc như thông tin project. Text được đóng khung như lệnh hệ thống ngoài luồng có thể kích hoạt cơ chế phòng vệ chống prompt-injection của Claude, khiến Claude hiện text đó cho bạn thay vì coi nó là context.

Claude Code lưu text đã tiêm vào transcript phiên. Với sự kiện giữa phiên như PostToolUse hay UserPromptSubmit, khi bạn resume bằng --continue hoặc --resume, Claude Code phát lại text đã lưu thay vì chạy lại hook cho các lượt trước, nên giá trị như timestamp hay commit SHA có thể trở nên cũ. Hooks SessionStart chạy lại khi resume với source đặt là "resume", hoặc "fork" nếu bạn thêm --fork-session, để chúng có thể làm mới context.

Không phải mọi sự kiện đều hỗ trợ chặn hay kiểm soát hành vi qua JSON. Các sự kiện có hỗ trợ dùng bộ field khác nhau để diễn đạt quyết định đó. Dùng bảng này để tra cứu nhanh trước khi viết hook:

Sự kiệnKiểu quyết địnhField chính
UserPromptSubmit, UserPromptExpansion, PostToolUse, PostToolUseFailure, PostToolBatch, Stop, SubagentStop, ConfigChange, PreCompactdecision cấp cao nhấtdecision: "block", reason. Stop và SubagentStop cũng nhận hookSpecificOutput.additionalContext cho phản hồi không-lỗi tiếp tục hội thoại
TeammateIdle, TaskCreated, TaskCompletedExit code hoặc continue: falseExit code 2 chặn hành động kèm phản hồi qua stderr. JSON {"continue": false, "stopReason": "..."} cũng dừng teammate hoàn toàn, giống hành vi hook Stop
PreToolUsehookSpecificOutputpermissionDecision (allow/deny/ask/defer), permissionDecisionReason
PermissionRequesthookSpecificOutputdecision.behavior (allow/deny)
PermissionDeniedhookSpecificOutputretry: true báo model có thể thử lại lệnh gọi tool bị từ chối
WorktreeCreatetrả về đường dẫnCommand hook in đường dẫn ra stdout; HTTP hook trả hookSpecificOutput.worktreePath. Hook thất bại hoặc thiếu đường dẫn khiến việc tạo thất bại
ElicitationhookSpecificOutputaction (accept/decline/cancel), content (giá trị field form cho accept)
ElicitationResulthookSpecificOutputaction (accept/decline/cancel), content (ghi đè giá trị field form)
MessageDisplayhookSpecificOutputdisplayContent thay text hiển thị trên màn hình. Chỉ ảnh hưởng hiển thị: transcript và những gì Claude thấy giữ nguyên bản gốc
SessionStart, Setup, SubagentStartChỉ contexthookSpecificOutput.additionalContext thêm context cho Claude. SessionStart còn nhận initialUserMessage, watchPaths, sessionTitle, và reloadSkills. Không có kiểm soát chặn hay quyết định
WorktreeRemove, Notification, SessionEnd, PostCompact, InstructionsLoaded, StopFailure, CwdChanged, FileChangedKhông cóKhông có kiểm soát quyết định. Dùng cho side effect như ghi log hay dọn dẹp

Một vài sự kiện còn có thể viết lại nội dung thay vì chỉ cho phép hoặc chặn:

Với nhu cầu redact hay biến đổi dữ liệu, can thiệp ở PreToolUse cho input gửi ra của tool và PostToolUse cho kết quả trả về của tool.

Dưới đây là ví dụ cho từng kiểu:

decision cấp cao nhất - dùng bởi UserPromptSubmit, UserPromptExpansion, PostToolUse, PostToolUseFailure, PostToolBatch, Stop, SubagentStop, ConfigChange, và PreCompact. Giá trị duy nhất là "block". Để cho phép hành động tiếp tục, bỏ decision khỏi JSON, hoặc exit 0 mà không có JSON nào cả:

{
"decision": "block",
"reason": "Test suite must pass before proceeding"
}

PreToolUse - dùng hookSpecificOutput để kiểm soát chi tiết hơn: allow, deny, hoặc leo thang cho người dùng. Bạn cũng có thể sửa tool input trước khi nó chạy hoặc tiêm thêm context cho Claude:

{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Database writes are not allowed"
}
}

PermissionRequest - dùng hookSpecificOutput để cho phép hoặc từ chối một yêu cầu quyền thay mặt người dùng. Khi cho phép, bạn cũng có thể sửa input của tool hoặc áp quy tắc quyền để người dùng không bị hỏi lại:

{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow",
"updatedInput": {
"command": "npm run lint"
}
}
}
}

Xem thêm ví dụ mở rộng gồm validate lệnh Bash, lọc prompt, và script tự động phê duyệt trong hướng dẫn Hooksví dụ tham chiếu Bash command validator trên GitHub.

Mỗi sự kiện tương ứng với một điểm trong vòng đời Claude Code nơi hooks có thể chạy. Các phần bên dưới được sắp theo thứ tự vòng đời: từ lúc thiết lập phiên qua vòng lặp agentic tới lúc phiên kết thúc. Mỗi phần mô tả thời điểm sự kiện chạy, matcher nó hỗ trợ, JSON input nó nhận, và cách kiểm soát hành vi qua output.

Chạy khi Claude Code bắt đầu một phiên mới hoặc resume một phiên có sẵn. Hữu ích để nạp ngữ cảnh phát triển như issue đang mở hay thay đổi gần đây trong codebase, hoặc thiết lập biến môi trường. Với ngữ cảnh tĩnh không cần script, dùng CLAUDE.md thay vào đó.

SessionStart chạy ở mọi phiên, nên giữ hooks này nhanh. Chỉ type: "command"type: "mcp_tool" được hỗ trợ.

Giá trị matcher tương ứng với cách phiên được khởi tạo:

MatcherThời điểm chạy
startupPhiên mới
resume--resume, --continue, hoặc /resume
clear/clear
compactNén tự động hoặc thủ công
forkMột phiên mới được fork từ phiên có sẵn: --fork-session kèm --resume/--continue, bản copy background của /fork, hoặc /branch

Trước v2.1.214, các phiên fork báo source"resume".

Ngoài field input chung, hook SessionStart nhận source và tùy chọn model, agent_type, và session_title:

FieldMô tả
sourceCách phiên bắt đầu: "startup" cho phiên mới, "resume" cho phiên resume, "clear" sau /clear, "compact" sau khi nén, hoặc "fork" cho phiên mới fork từ phiên có sẵn
modelĐịnh danh model đang hoạt động. Có thể vắng mặt, ví dụ sau /clear hoặc khi phiên được khôi phục qua conversation recovery, nên kiểm tra field này trước khi đọc
agent_typeTên agent, có mặt khi bạn khởi động Claude Code với claude --agent <name>
session_titleTiêu đề phiên hiện tại nếu đã được đặt, ví dụ qua --name hoặc /rename. Một hook phát ra sessionTitle có thể kiểm tra session_title trước để tránh ghi đè tiêu đề người dùng đã tự đặt
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"hook_event_name": "SessionStart",
"source": "startup",
"model": "claude-sonnet-5"
}

Mọi text script hook của bạn in ra stdout được thêm làm context cho Claude. Ngoài các field JSON output khả dụng cho mọi hook, bạn có thể trả về các field đặc thù sự kiện sau:

FieldMô tả
additionalContextChuỗi thêm vào context của Claude ở đầu hội thoại, trước prompt đầu tiên. Xem Thêm context cho Claude
initialUserMessageChuỗi dùng làm tin nhắn người dùng đầu tiên của phiên. Áp dụng ở chế độ không tương tác với flag -p, nơi nó trở thành lượt đầu tiên kể cả khi không có prompt nào được cung cấp. Nếu có prompt, nó theo sau như lượt kế tiếp. Khác với additionalContext (gắn vào một lượt có sẵn), field này tạo ra lượt đó
sessionTitleĐặt tiêu đề phiên, hiệu ứng giống /rename. Dùng để tự động đặt tên phiên theo thư mục khởi chạy, git branch, hay tên worktree. Áp dụng khi source"startup", "resume", hoặc "fork"; bị bỏ qua ở "clear""compact"
watchPathsMảng đường dẫn tuyệt đối cần theo dõi cho sự kiện FileChanged trong phiên này
reloadSkillsBoolean. Khi true, Claude Code quét lại thư mục skill và command sau khi các hook SessionStart hoàn tất, để skill mà hook vừa cài có thể dùng được ngay trong cùng phiên, từ prompt đầu tiên
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Current branch: feat/auth-refactor\nUncommitted changes: src/auth.ts, src/login.tsx\nActive issue: #4211 Migrate to OAuth2",
"sessionTitle": "auth-refactor"
}
}

Vì stdout thuần đã tới được Claude ở sự kiện này, một hook chỉ nạp context có thể in trực tiếp ra stdout không cần dựng JSON. Dùng dạng JSON khi bạn cần kết hợp context với các field khác như suppressOutput hay sessionTitle.

Dùng reloadSkills khi một hook SessionStart cài đặt hoặc cập nhật skill. Việc phát hiện skill thường chạy trước khi hooks SessionStart hoàn tất, nên các file hook ghi vào ~/.claude/skills/ hay .claude/skills/ sẽ chỉ xuất hiện ở phiên kế tiếp nếu không có reloadSkills.

Hooks SessionStart có quyền truy cập biến môi trường CLAUDE_ENV_FILE, cung cấp đường dẫn file nơi bạn có thể lưu biến môi trường cho các lệnh Bash tiếp theo.

Để đặt từng biến môi trường, ghi các câu lệnh export vào CLAUDE_ENV_FILE. Dùng append (>>) để giữ lại biến do hook khác đặt:

#!/bin/bash
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
echo 'export DEBUG_LOG=true' >> "$CLAUDE_ENV_FILE"
echo 'export PATH="$PATH:./node_modules/.bin"' >> "$CLAUDE_ENV_FILE"
fi
exit 0

Để bắt toàn bộ thay đổi môi trường từ các lệnh setup, so sánh biến đã export trước và sau:

#!/bin/bash
ENV_BEFORE=$(export -p | sort)
# Chạy các lệnh setup làm thay đổi môi trường
source ~/.nvm/nvm.sh
nvm use 20
if [ -n "$CLAUDE_ENV_FILE" ]; then
ENV_AFTER=$(export -p | sort)
comm -13 <(echo "$ENV_BEFORE") <(echo "$ENV_AFTER") >> "$CLAUDE_ENV_FILE"
fi
exit 0

Mọi biến ghi vào file này sẽ khả dụng trong toàn bộ lệnh Bash tiếp theo mà Claude Code chạy trong phiên.

Chỉ chạy khi bạn khởi động Claude Code với --init-only, hoặc với --init/--maintenancechế độ không tương tác với flag -p. Không chạy khi khởi động bình thường. Dùng cho cài đặt dependency một lần hoặc dọn dẹp theo lịch mà bạn kích hoạt tường minh từ CI hoặc script, tách biệt với khởi động phiên thông thường. Với khởi tạo mỗi phiên, dùng SessionStart thay vào đó.

Giá trị matcher tương ứng với flag CLI đã kích hoạt hook:

MatcherThời điểm chạy
initclaude --init-only hoặc claude -p --init
maintenanceclaude -p --maintenance

Khi bạn chạy claude --init-only, Claude Code chạy hooks Setup và hooks SessionStart với matcher startup, rồi thoát mà không bắt đầu hội thoại.

--init--maintenance chỉ kích hoạt hooks Setup khi kết hợp với -p. Trong phiên tương tác, hai flag này hiện chưa kích hoạt hooks Setup.

Khi bạn bắt đầu hoặc tiếp tục hội thoại với -p, bạn cũng cần cung cấp một prompt, dưới dạng tham số hoặc pipe qua stdin. Bạn có thể bỏ qua prompt khi hook SessionStart cung cấp initialUserMessage hoặc khi resume một phiên bằng lệnh gọi tool bị hoãn.

Setup không chạy ở mọi lần khởi chạy, một plugin cần một dependency được cài không thể chỉ dựa vào Setup. Cách thực tế là kiểm tra dependency ở lần dùng đầu tiên và cài nếu thiếu, ví dụ một hook hay skill kiểm tra ${CLAUDE_PLUGIN_DATA}/node_modules và chạy npm install nếu vắng mặt.

Ngoài field input chung, hooks Setup nhận field trigger"init" hoặc "maintenance":

{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"hook_event_name": "Setup",
"trigger": "init"
}

Hooks Setup không thể chặn. Bất kỳ exit code khác 0 nào, kể cả 2, đều hiện stderr cho người dùng dưới dạng thông báo <hook name> hook error, và thực thi tiếp tục. Ở chế độ không tương tác, output của hook chỉ hiện khi bạn khởi chạy với --verbose.

Để truyền thông tin vào context của Claude, trả additionalContext trong JSON output; stdout thuần chỉ được ghi vào debug log. Ngoài các field JSON output khả dụng cho mọi hook, bạn có thể trả các field đặc thù sự kiện sau:

FieldMô tả
additionalContextChuỗi thêm vào context của Claude. Giá trị từ nhiều hook được nối lại
{
"hookSpecificOutput": {
"hookEventName": "Setup",
"additionalContext": "Dependencies installed: node_modules, .venv"
}
}

Hooks Setup có quyền truy cập CLAUDE_ENV_FILE. Biến ghi vào file đó được giữ lại cho các lệnh Bash tiếp theo trong phiên, giống hooks SessionStart. Chỉ type: "command"type: "mcp_tool" được hỗ trợ.

Chạy khi một file CLAUDE.md hay .claude/rules/*.md được nạp vào context. Sự kiện này chạy khi phiên bắt đầu cho các file được nạp sẵn (eager), và chạy lại sau khi file được nạp muộn, ví dụ khi Claude truy cập một thư mục con chứa CLAUDE.md lồng nhau, hoặc khi rule có điều kiện với frontmatter paths: khớp. Hook không hỗ trợ chặn hay kiểm soát quyết định. Nó chạy bất đồng bộ chỉ để quan sát (observability).

Matcher chạy trên load_reason. Ví dụ, dùng "matcher": "session_start" để chỉ chạy cho file nạp lúc phiên bắt đầu, hoặc "matcher": "path_glob_match|nested_traversal" để chỉ chạy cho các lần nạp muộn.

Ngoài field input chung, hooks InstructionsLoaded nhận các field sau:

FieldMô tả
file_pathĐường dẫn tuyệt đối tới file hướng dẫn vừa được nạp
memory_typePhạm vi của file: "User", "Project", "Local", hoặc "Managed"
load_reasonLý do file được nạp: "session_start", "nested_traversal", "path_glob_match", "include", hoặc "compact". Giá trị "compact" xảy ra khi file hướng dẫn được nạp lại sau một lần nén
globsMẫu glob đường dẫn từ frontmatter paths: của file, nếu có. Chỉ có mặt với lần nạp path_glob_match
trigger_file_pathĐường dẫn tới file mà việc truy cập nó kích hoạt lần nạp này, cho lần nạp muộn
parent_file_pathĐường dẫn tới file hướng dẫn cha đã include file này, cho lần nạp include
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../transcript.jsonl",
"cwd": "/Users/my-project",
"hook_event_name": "InstructionsLoaded",
"file_path": "/Users/my-project/CLAUDE.md",
"memory_type": "Project",
"load_reason": "session_start"
}

Hooks InstructionsLoaded không có kiểm soát quyết định. Chúng không thể chặn hay sửa việc nạp hướng dẫn. Dùng sự kiện này để ghi log audit, theo dõi tuân thủ, hoặc quan sát hệ thống.

Chạy khi người dùng gửi một prompt, trước khi Claude xử lý. Cho phép bạn thêm context dựa trên prompt/hội thoại, validate prompt, hoặc chặn một số loại prompt.

Hooks UserPromptSubmit có timeout mặc định 30 giây cho các loại command, http, và mcp_tool, ngắn hơn mặc định 600 giây ở hầu hết sự kiện khác. Vì hook này chạy trước mọi prompt và chặn việc xử lý model cho tới khi hoàn tất, một hook bị treo sẽ làm nghẽn cả phiên. Nếu hook của bạn cần nhiều thời gian hơn, đặt field timeout trong mục hook.

Một command, HTTP, hoặc MCP tool hook của UserPromptSubmit chạm timeout sẽ bị hủy và output của nó, gồm cả additionalContext, bị bỏ. Prompt vẫn tới Claude nhưng không có context đó. Kể từ v2.1.196, transcript hiện thông báo nêu tên hook, timeout đã xảy ra, và việc output bị bỏ. Phiên bản trước không có thông báo.

Ngoài field input chung, hooks UserPromptSubmit nhận field prompt chứa text người dùng đã gửi.

{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"permission_mode": "default",
"hook_event_name": "UserPromptSubmit",
"prompt": "Write a function to calculate the factorial of a number"
}

Hooks UserPromptSubmit có thể kiểm soát việc một prompt có được xử lý không và thêm context. Mọi field JSON output đều khả dụng.

Có hai cách thêm context vào hội thoại khi exit code 0:

  • Stdout text thuần: mọi text không phải JSON in ra stdout được thêm làm context
  • JSON với additionalContext: dùng định dạng JSON dưới đây để kiểm soát chi tiết hơn. Giá trị additionalContext được thêm làm context

Stdout thuần hiện như output hook trong transcript. Giá trị additionalContext được tiêm dưới dạng system reminder mà Claude đọc không có mục transcript hiển thị.

Để chặn một prompt, trả về object JSON với decision"block":

FieldMô tả
decision"block" ngăn prompt được xử lý và xóa nó khỏi context. Bỏ qua để cho prompt tiếp tục
reasonHiện cho người dùng khi decision"block". Không thêm vào context
additionalContextChuỗi thêm vào context của Claude cùng với prompt đã gửi. Xem Thêm context cho Claude
sessionTitleĐặt tiêu đề phiên. Dùng để tự động đặt tên phiên theo nội dung prompt
suppressOriginalPromptNếu true khi decision"block", bỏ text prompt gốc khỏi thông điệp chặn hiện cho người dùng
{
"decision": "block",
"reason": "Explanation for decision",
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "My additional context here",
"sessionTitle": "My session title"
}
}

Chạy khi một lệnh người dùng gõ mở rộng thành prompt trước khi tới Claude. Dùng để chặn một lệnh cụ thể khỏi bị gọi trực tiếp, tiêm context cho một skill nào đó, hoặc ghi log lệnh nào người dùng gọi. Ví dụ, hook khớp deploy có thể chặn /deploy trừ khi có file phê duyệt, hoặc hook khớp một skill review có thể thêm checklist review của team làm additionalContext.

Sự kiện này bao phủ đường mà PreToolUse không phủ: hook PreToolUse khớp tool Skill chỉ chạy khi Claude gọi tool, nhưng gõ /skillname trực tiếp lại bỏ qua PreToolUse. UserPromptExpansion chạy trên đường trực tiếp đó.

Khớp trên command_name. Để trống matcher để chạy trên mọi lệnh dạng prompt.

Ngoài field input chung, hooks UserPromptExpansion nhận expansion_type, command_name, command_args, command_source, và chuỗi prompt gốc. Field expansion_typeslash_command cho skill và custom command, hoặc mcp_prompt cho prompt từ MCP server.

{
"session_id": "abc123",
"transcript_path": "/Users/.../00893aaf.jsonl",
"cwd": "/Users/...",
"permission_mode": "default",
"hook_event_name": "UserPromptExpansion",
"expansion_type": "slash_command",
"command_name": "example-skill",
"command_args": "arg1 arg2",
"command_source": "plugin",
"prompt": "/example-skill arg1 arg2"
}

Hooks UserPromptExpansion có thể chặn việc mở rộng hoặc thêm context. Mọi field JSON output đều khả dụng.

FieldMô tả
decision"block" ngăn lệnh mở rộng. Bỏ qua để cho phép nó tiếp tục
reasonHiện cho người dùng khi decision"block"
additionalContextChuỗi thêm vào context của Claude cùng với prompt đã mở rộng. Xem Thêm context cho Claude
{
"decision": "block",
"reason": "This slash command is not available",
"hookSpecificOutput": {
"hookEventName": "UserPromptExpansion",
"additionalContext": "Additional context for this expansion"
}
}

Chạy trong lúc một tin nhắn assistant đang stream ra màn hình. Claude Code hiển thị tin nhắn theo từng đợt: mỗi khi một batch dòng vừa hoàn tất sẵn sàng render, hook chạy một lần với các dòng đó và Claude Code render text thay thế của hook vào chỗ đó. Một tin nhắn dài tạo ra nhiều lần gọi; một tin nhắn ngắn có thể chỉ tạo một lần.

Dùng MessageDisplay để:

  • loại bỏ markdown cho hiển thị tối giản
  • biến đổi text mà một ứng dụng Agent SDK hiện cho người dùng của nó
  • redact API key hay hostname nội bộ khỏi phản hồi của Claude

Claude Code giữ mỗi batch cho tới khi hook của bạn trả về, nên giữ hook nhanh. Nếu hook thất bại hoặc hết giờ, Claude Code hiện text gốc. Timeout mặc định của sự kiện này là 10 giây; nếu hook cần nhiều thời gian hơn, đặt field timeout trong mục hook.

MessageDisplay chỉ ảnh hưởng hiển thị: text thay thế chỉ đổi những gì render trên màn hình. Transcript và những gì Claude thấy vẫn giữ text gốc, nên Claude không bao giờ thấy bản thay thế, và chế độ verbose hiện bản gốc. Hook chỉ nhận text tin nhắn assistant, nên kết quả tool và text bạn gõ vẫn render không đổi.

MessageDisplay không hỗ trợ matcher và chạy cho mọi tin nhắn assistant có stream text; tin nhắn không có text, như phản hồi chỉ gọi tool, không kích hoạt nó.

Ở lần chạy không tương tác, gồm cả Agent SDK query và claude -p, MessageDisplay chạy một lần mỗi tin nhắn assistant thay vì mỗi batch dòng. Lệnh gọi duy nhất đó tới sau khi tin nhắn hoàn tất và mang toàn bộ text tin nhắn: index0, finaltrue, và delta chứa toàn bộ tin nhắn.

Ngoài field input chung, hooks MessageDisplay nhận định danh cho lượt và tin nhắn, vị trí của lần gọi này trong tin nhắn, và text mới trong delta.

FieldMô tả
turn_idUUID của lượt hiện tại
message_idUUID của tin nhắn assistant đang hiển thị. Ổn định qua mọi batch của cùng tin nhắn. Đây không phải id msg_… của API, nên không thể tương quan với id tin nhắn trong transcript
indexChỉ số bắt đầu từ 0 của batch này trong tin nhắn
finaltrue ở batch cuối cùng của tin nhắn. Mỗi tin nhắn có đúng một batch cuối
deltaCác dòng vừa hoàn tất kể từ batch trước, kèm ký tự xuống dòng kết thúc. Luôn là dòng trọn vẹn, trừ batch cuối có thể kết thúc giữa dòng. Ở lần chạy tương tác, delta của batch cuối rỗng khi tin nhắn kết thúc bằng xuống dòng, nên dùng final, không phải delta khác rỗng, làm tín hiệu kết thúc tin nhắn. Ở Agent SDK và claude -p, lệnh gọi duy nhất mang toàn bộ tin nhắn
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../transcript.jsonl",
"cwd": "/Users/my-project",
"hook_event_name": "MessageDisplay",
"turn_id": "0c9e6a2f-7d41-4f4e-9a15-3f4f7c2b8d10",
"message_id": "5b2a9c8e-1f63-4d8a-b7c4-9e0d2a6f1c3b",
"index": 0,
"final": false,
"delta": "Here is the plan:\n"
}

Ngoài các field JSON output khả dụng cho mọi hook, hooks MessageDisplay có thể trả displayContent để thay delta trên màn hình:

FieldMô tả
displayContentText hiển thị thay cho delta. Bỏ qua để hiện bản gốc

MessageDisplay không có kiểm soát quyết định. Không thể chặn tin nhắn hay đổi những gì lưu trong transcript hoặc gửi cho Claude.

Ví dụ dưới đây loại bỏ định dạng markdown khỏi phản hồi của Claude để hiển thị dạng text thuần. Script đọc mỗi batch từ stdin, xóa dấu bold và backtick code inline khỏi delta, và trả kết quả dưới dạng displayContent.

Đăng ký command hook cho sự kiện này trong file settings (macOS/Linux):

{
"hooks": {
"MessageDisplay": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/plain-display.sh",
"args": []
}
]
}
]
}
}

Lưu script này vào .claude/hooks/plain-display.sh trong project và cấp quyền thực thi bằng chmod +x:

#!/bin/bash
jq '{hookSpecificOutput: {hookEventName: "MessageDisplay", displayContent: (.delta | gsub("\\*\\*"; "") | gsub("`"; ""))}}'

Script cần jq trong PATH.

Trên Windows (PowerShell), đăng ký một command hook chạy script qua PowerShell:

{
"hooks": {
"MessageDisplay": [
{
"hooks": [
{
"type": "command",
"command": "powershell.exe",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"${CLAUDE_PROJECT_DIR}/.claude/hooks/plain-display.ps1"
]
}
]
}
]
}
}

-NoProfile bỏ qua việc nạp PowerShell profile để hook khởi động nhanh, và -ExecutionPolicy Bypass cho phép PowerShell chạy file script local. Lưu script vào .claude/hooks/plain-display.ps1:

Terminal window
$batch = [Console]::In.ReadToEnd() | ConvertFrom-Json
$text = $batch.delta -replace '\*\*', '' -replace '`', ''
@{
hookSpecificOutput = @{
hookEventName = "MessageDisplay"
displayContent = $text
}
} | ConvertTo-Json

Batch không có markdown đi qua không đổi. Nếu script thất bại, ví dụ vì thiếu jq, Claude Code hiện text gốc và chỉ ghi lỗi vào debug output, không hiện trong phiên.