30 sự kiện hook kích hoạt trong một phiên Claude Code. Chỉ 3 sự kiện trả lời được.

Danh sách đầy đủ các sự kiện hook của Claude Code, thời điểm từng sự kiện kích hoạt, 15 sự kiện nào có thể chặn, và quy tắc stdout âm thầm nuốt chửng đầu ra của phần lớn hook. Một tài liệu tham chiếu thực chiến, đúc kết từ việc chạy hook trong môi trường sản xuất qua hàng nghìn phiên agent.

Hai kiểu hỏng hóc cứ lặp đi lặp lại khi người ta đấu hook vào Claude Code, và chúng chẳng giống nhau chút nào.

Kiểu thứ nhất: bạn thêm một hook, không có gì xảy ra. Không lỗi, không cảnh báo, không một dòng log nào. Hook đơn giản là không bao giờ chạy.

Kiểu thứ hai: hook rõ ràng có chạy, bạn thấy được tác động của nó trên đĩa, nhưng thông điệp nó in ra cho agent thì không bao giờ đến nơi. Agent hành xử như thể hook chưa nói gì.

Cả hai đều đến từ cùng một chỗ. Hệ thống hook lớn hơn và kém đồng nhất hơn nhóm sự kiện ít ỏi mà phần lớn bài viết đề cập, còn quy tắc quyết định ai được nói với agent thì không phải là quy tắc bạn đoán ra. Đây là tài liệu tham chiếu mà chúng tôi từng ước có. Chúng tôi xây AgentsRoom trên chính các hook này, và mọi thứ bên dưới đều hoặc trích từ tài liệu chính thức, hoặc đo được trong môi trường sản xuất.

Có 30 sự kiện, không phải sáu

Phần lớn hướng dẫn nói về PreToolUse, PostToolUse, UserPromptSubmit, Stop, NotificationSubagentStop. Sáu sự kiện đó là thật, và chúng gánh phần lớn công việc hữu ích. Chúng cũng chỉ là một phần năm những gì đang tồn tại.

Danh sách đầy đủ, nhóm theo thứ mà chúng quan sát:

NhómSự kiện
PhiênSessionStart, SessionEnd, Setup
PromptUserPromptSubmit, UserPromptExpansion
Công cụPreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch
QuyềnPermissionRequest, PermissionDenied
LượtStop, StopFailure
Subagent và tác vụSubagentStart, SubagentStop, TaskCreated, TaskCompleted, TeammateIdle
Ngữ cảnhPreCompact, PostCompact, InstructionsLoaded
Môi trườngFileChanged, CwdChanged, ConfigChange
WorktreeWorktreeCreate, WorktreeRemove
Giao diệnNotification, MessageDisplay
Elicitation MCPElicitation, ElicitationResult

Sơ đồ dòng thời gian của 30 sự kiện hook trong Claude Code theo đúng thứ tự kích hoạt trong một phiên agent, từ SessionStart qua UserPromptSubmit, PreToolUse, PostToolUse, Stop đến SessionEnd, cho thấy những sự kiện nào có thể chặn agent.

Thứ tự kích hoạt của các sự kiện trong một phiên. Khối công cụ lặp lại một lần cho mỗi lệnh gọi công cụ, và toàn bộ khối prompt lặp lại một lần cho mỗi lượt.

Một vài sự kiện trong số này thay đổi cách bạn nghĩ về hệ thống. PostToolUseFailure có tồn tại, nên nhánh "công cụ có chạy được không" là một sự kiện, không phải thứ bạn phải suy ra từ payload. PostToolBatch kích hoạt một lần sau khi một lô lệnh gọi công cụ song song hoàn tất, đó chính là chỗ đúng để chạy linter một lần thay vì một lần cho mỗi lần sửa. InstructionsLoaded kích hoạt khi CLAUDE.md được đọc, cho bạn một điểm móc để kiểm tra rằng agent thực sự đã nạp đúng bộ quy tắc bạn nghĩ nó đã nạp.

Quy tắc stdout nuốt chửng đầu ra của phần lớn hook

Đây là điều hữu ích nhất trên trang này.

Khi thoát với mã 0, Claude Code phân tích stdout để tìm các trường JSON đầu ra. Nhưng stdout đó có bao giờ được cho agent thấy hay không lại phụ thuộc vào sự kiện, và danh sách ngoại lệ rất ngắn. Trích từ tài liệu tham chiếu:

Với phần lớn sự kiện, stdout được ghi vào log debug nhưng không hiển thị trong transcript. Các ngoại lệ là UserPromptSubmit, UserPromptExpansionSessionStart, nơi stdout được thêm vào như ngữ cảnh mà Claude có thể thấy và hành động dựa trên đó.

Ba sự kiện trên tổng số ba mươi. Nếu bạn echo "warning: this migration is destructive" từ một hook PostToolUse và mong agent đọc được, nó sẽ không bao giờ đọc. Văn bản của bạn đã đi vào log debug.

Có đúng hai cách đưa văn bản đến trước mắt agent từ một sự kiện bất kỳ nào khác:

  1. Thoát với mã 2 và ghi ra stderr. Khi thoát với mã 2, Claude Code bỏ qua stdout cùng mọi JSON nằm trong đó, và đưa stderr trở lại cho agent như một thông điệp lỗi.
  2. Thoát với mã 0 và in ra một đối tượng JSON mang hookSpecificOutput.additionalContext.

Chú ý sự bất đối xứng ở cách thứ nhất. Thoát 0 nghĩa là stdout có giá trị còn stderr thì không. Thoát 2 nghĩa là stderr có giá trị còn stdout bị vứt bỏ hoàn toàn. Hiểu ngược điều này là lý do một hook trông hoàn toàn đúng mà vẫn câm lặng.

Mọi mã thoát khác đều là lỗi không chặn. Transcript hiển thị thông báo <hook name> hook error kèm dòng đầu tiên của stderr, quá trình thực thi tiếp tục, và toàn bộ stderr rơi vào log debug.

Sơ đồ các mã thoát của hook trong Claude Code: mã thoát 0 gửi JSON trên stdout đến agent chỉ trên ba sự kiện, mã thoát 2 chặn hành động và gửi stderr đến agent, mọi mã thoát khác là lỗi không chặn được ghi vào log debug.

Kênh nào đến được agent, theo từng mã thoát. Đường nét đứt là đường mà người ta tưởng có nhưng thực ra không có.

Đúng một nửa có thể chặn

Mười lăm sự kiện dừng hành động khi thoát với mã 2. Mười lăm sự kiện bỏ qua và đi tiếp.

Có thể chặn: PreToolUse, PermissionRequest, UserPromptSubmit, UserPromptExpansion, Stop, SubagentStop, TeammateIdle, TaskCreated, TaskCompleted, ConfigChange, PostToolBatch, PreCompact, Elicitation, ElicitationResult, WorktreeCreate.

Không thể chặn: PostToolUse, PostToolUseFailure, PermissionDenied, StopFailure, Notification, SubagentStart, SessionStart, Setup, SessionEnd, CwdChanged, FileChanged, PostCompact, WorktreeRemove, InstructionsLoaded, MessageDisplay.

Hệ quả thực tế: hàng rào an toàn thuộc về PreToolUse, không bao giờ là PostToolUse. PostToolUse kích hoạt sau khi công cụ đã chạy xong thành công. Thoát với mã 2 ở đó không hoàn tác được thao tác ghi, nó chỉ in ra một lỗi trong khi thiệt hại đã nằm sẵn trên đĩa. Nếu bạn muốn chặn rm -rf, bạn có đúng một chỗ để làm việc đó.

Chuyện PostToolBatch chặn được trong khi PostToolUse thì không đáng để nhìn lại lần nữa. Nó có nghĩa là một kiểm tra ở cấp lô vẫn có thể dừng lượt sau khi các lần sửa song song đã ghi xuống, đây là thứ gần nhất với một quyền phủ quyết sau khi ghi mà hệ thống cung cấp.

Matcher so khớp chính xác, cho đến khi đột nhiên không còn thế

Trường matcher đổi chiến lược đánh giá dựa trên chính các ký tự của nó, và không có gì cho bạn biết nó đã đi theo nhánh nào.

MatcherĐược đánh giá như
"*", "", hoặc bỏ trốngkhớp mọi thứ
Chỉ có chữ cái, chữ số, _, -, dấu cách, ,, |chuỗi chính xác, hoặc danh sách chuỗi chính xác tách theo | hoặc ,
Bất kỳ thứ gì khácbiểu thức chính quy JavaScript không neo

Không neo mới là cái bẫy. Tài liệu tham chiếu nói rõ rằng biểu thức được kiểm tra bằng RegExp.prototype.test, vốn thành công khi khớp ở bất kỳ đâu trong giá trị. Nên Edit.* khớp với Edit NotebookEdit. Nếu bạn muốn nói đến đúng một công cụ, hãy viết ^Edit$.

Hai hành vi phụ thuộc phiên bản, nên biết trước khi bạn đi gỡ nhầm chỗ:

  • Dấu phẩy làm ký tự phân tách và việc bỏ qua khoảng trắng cần Claude Code v2.1.191 trở lên.
  • Dấu gạch nối gia nhập tập ký tự so khớp chính xác ở v2.1.195. Trước đó, một matcher như code-reviewer bị xử lý như regex không neo, nên nó kích hoạt cả cho senior-code-reviewer.

Trong AgentsRoom, chúng tôi giới hạn hook gán tệp của mình bằng Write|Edit|MultiEdit|NotebookEdit, chuỗi này ở lại nhánh so khớp chính xác và chỉ khớp đúng bốn công cụ đó, không gì khác. Các hook vòng đời mà chúng tôi cài đặt hoàn toàn không có matcher, vì chúng lúc nào cũng liên quan đến chúng tôi.

Sáu nơi có thể định nghĩa hook, và chúng hợp nhất với nhau

Phản xạ đầu tiên là đi tìm thứ tự ưu tiên. Không có thứ tự nào cả, và đó mới là phần thú vị.

Vị tríPhạm vi
~/.claude/settings.jsonmọi dự án của bạn, cục bộ trên máy bạn
.claude/settings.jsonmột dự án, commit được
.claude/settings.local.jsonmột dự án, được Claude Code gitignore
Managed policy settingstoàn tổ chức, do quản trị viên kiểm soát
hooks/hooks.json của plugintrong khi plugin còn được bật
Frontmatter của skill hoặc agenttrong khi thành phần đó còn hoạt động

Trích từ tài liệu tham chiếu:

Các mục hook hợp nhất qua các cấp settings thay vì thay thế lẫn nhau: settings người dùng, dự án và cục bộ bổ sung hook của riêng chúng mà không gỡ bỏ các hook được quản lý, và thiết lập disableAllHooks không thể tắt các hook được quản lý từ bên ngoài phạm vi settings được quản lý.

Vậy nên một hook của dự án không bao giờ ghi đè hook toàn cục, nó chồng lên trên. Sáu nguồn, tất cả đều cộng dồn. Một trình định dạng PostToolUse khai báo trong settings người dùng rồi khai báo lại trong dự án sẽ chạy hai lần cho mỗi lần sửa, và triệu chứng duy nhất là cảm giác mọi thứ chậm đi.

Sơ đồ mô tả sáu vị trí settings của Claude Code có thể định nghĩa hook, tất cả hợp nhất theo kiểu cộng dồn thành một tập hook duy nhất thay vì ghi đè lẫn nhau.

Sáu nguồn, một tập hợp nhất. Không có gì ở đây ghi đè thứ gì.

Điều này cũng giải thích vì sao .claude/settings.local.json là chỗ đúng để một công cụ cài hook vào dự án của người khác. Nó giới hạn trong phạm vi dự án, Claude Code gitignore nó, và nó được nạp mà không cần cờ dòng lệnh nào. Đó là nơi AgentsRoom ghi các mục của mình, để tệp .claude/settings.json đã commit của người dùng không bao giờ bị đụng tới và đồng nghiệp của họ không bao giờ thừa hưởng một đường dẫn gắn với máy cụ thể.

Chạy hook trong môi trường sản xuất đã dạy chúng tôi điều gì

AgentsRoom cài hook vào mọi dự án mà nó mở, để theo dõi trạng thái agent một cách xác định và gán tệp đã sửa cho đúng agent. Có vài chuyện chỉ lộ ra ở quy mô đó.

Tên sự kiện lạ bị bỏ qua trong im lặng. Điều này không có trong tài liệu, và chúng tôi phụ thuộc vào nó. Khi chúng tôi thêm một sự kiện vòng đời mới vào trình cài đặt, người dùng đang chạy CLI phiên bản cũ sẽ nhận một tệp settings.local.json chứa tên sự kiện mà binary của họ chưa từng nghe đến. Không có gì hỏng, không có gì cảnh báo, mục đó bị bỏ qua. Chính điều đó khiến trình cài đặt an toàn để phát hành trước một bản CLI. Và tất nhiên, đó cũng là lý do một lỗi gõ nhầm tạo ra sự im lặng tuyệt đối chứ không phải một lỗi.

agent_id là cách bạn biết mình đang ở trong một subagent. Trường này chỉ có mặt khi hook kích hoạt bên trong một lệnh gọi subagent. Chuyện này quan trọng hơn vẻ ngoài của nó: Stop kích hoạt khi một subagent kết thúc lượt của mình, chứ không riêng gì agent chính. Một quy tắc ngây thơ kiểu "đánh dấu phiên đã xong khi gặp Stop" sẽ đánh dấu cả phiên là kết thúc ngay lần đầu tiên có một subagent trả về. Chúng tôi bỏ qua các sự kiện kết thúc lượt có mang agent_id đúng vì lý do này.

Đừng đọc transcript_path cho lượt đang chạy. Tài liệu tham chiếu cảnh báo rằng transcript được ghi bất đồng bộ và có thể chậm hơn cuộc hội thoại đang nằm trong bộ nhớ, nên các tin nhắn gần nhất có thể chưa có ở đó khi hook của bạn kích hoạt. StopSubagentStop nhận last_assistant_message chính là để bạn không bao giờ phải chạy đua với tệp.

Hook là tín hiệu trạng thái đáng tin cậy duy nhất. Trước khi có hook, chúng tôi bóc tách PTY để đoán xem agent đang suy nghĩ, đang chờ hay đã xong. Cách đó hỏng ngay khi CLI vẽ giao diện qua bộ đệm màn hình phụ của terminal, đúng thứ mà /tui fullscreen làm. Hook kích hoạt y hệt nhau dưới mọi bộ dựng hình. Nếu bạn đang xây bất cứ thứ gì quan sát một agent từ bên ngoài, đây là lớp nên xây dựng lên trên, còn việc bóc tách màn hình thì cùng lắm chỉ nên giữ làm phương án dự phòng.

async: true không tốn gì cả. Một lệnh hook có thể khai báo async: true, và agent sẽ không chờ nó. Hook của chúng tôi gửi POST đến một endpoint cục bộ với giới hạn 2 giây rồi trả quyền điều khiển; độ trễ lượt của agent không hề bị ảnh hưởng, kể cả khi ứng dụng nhận đã đóng. Nếu hook của bạn chỉ quan sát mà không bao giờ quyết định, hãy chuyển nó sang async và thôi phải trả giá cho nó.

Đừng bao giờ để hook viết rác ra terminal. Script của chúng tôi nuốt mọi ngoại lệ, kể cả ở cấp cao nhất. Một traceback Python không được bắt phát ra từ hook không chỉ thất bại lặng lẽ, nó in cả stack trace Python vào phiên terminal của người dùng, ngay giữa lúc họ đang làm việc.

Thời gian chờ

Giá trị mặc định khá rộng rãi, trừ ba ngoại lệ thì không:

Loại hookThời gian chờ mặc định
command, http, mcp_tool600 s
prompt30 s
agent60 s
UserPromptSubmit (command, http, mcp_tool)30 s
MessageDisplay (command, http, mcp_tool)10 s
SessionEnd1.5 s dùng chung cho tất cả hook, được nâng lên cho khớp với một timeout dài hơn ở từng hook, tối đa 60 s

Ngân sách của SessionEnd là thứ khiến người ta bất ngờ. Đó là ngân sách dùng chung, không phải hạn mức cho từng hook, nên ba hook dọn dẹp sẽ chia nhau 1.5 giây trừ khi bạn nâng nó lên một cách tường minh.

Bản rút gọn

  • Có 30 sự kiện. Sáu trong số đó nổi tiếng.
  • stdout chỉ đến được agent trên UserPromptSubmit, UserPromptExpansionSessionStart. Ở mọi nơi khác, hãy dùng thoát 2 với stderr, hoặc additionalContext trong JSON.
  • 15 sự kiện chặn khi thoát 2, 15 sự kiện bỏ qua. Hàng rào an toàn đặt trên PreToolUse.
  • Matcher là chuỗi chính xác cho đến khi một ký tự đặc biệt biến nó thành regex không neo.
  • Sáu nguồn settings hợp nhất theo kiểu cộng dồn. Không có gì ghi đè thứ gì.
  • Một tên sự kiện viết sai chính tả thất bại trong im lặng hoàn toàn.

Nếu bạn muốn tận mắt nhìn các sự kiện này kích hoạt thay vì ngồi suy luận về chúng, đó chính là thứ chúng tôi đã xây: AgentsRoom hiển thị mọi lần kích hoạt hook theo từng agent, từng dự án, từng lần chạy, trên hàng chục agent chạy song songcác phiên subagent. Các hook bạn tự cấu hình trong settings của mình vẫn chạy đúng như đã viết, vì nó chạy CLI thật.

Đọc thêm

Tải AgentsRoom

Chạy các agent AI của bạn (Claude, Codex, Antigravity CLI, OpenCode, Aider, Grok Build, Mistral Vibe, Kimi Code) trên tất cả dự án, trong một cửa sổ duy nhất.

Miễn phíTải AgentsRoom

Ứng dụng đồng hành: theo dõi agent khi đi đường

Sử dụng Claude, Codex, Antigravity CLI hoặc nhà cung cấp AI khác.

Tải tiện ích mở rộng
Chrome Web Store

Gửi lỗi và yêu cầu thẳng vào backlog công khai của bạn.

Một cái nhìn về AgentsRoom đang hoạt động.

Nhiều dự án
Đa nhà cung cấp
Nhiều agent
Trạng thái trực tiếp
File diff & commit
Ứng dụng đồng hành mobile
Xem trước trực tiếp
Đội agent
Tự động hóa trình duyệt
Dev theo backlog
Thư viện prompt
Thư viện skill
Xem tất cả tính năng