Hook Stop của Claude Code chạy một lệnh mỗi khi agent chính của Claude trả lời xong, nên đây là chỗ phù hợp để gửi thông báo "đã xong", ghi log kết quả hoặc chạy kiểm tra. Hook không chạy khi bạn ngắt Claude, còn lượt kết thúc do lỗi API sẽ kích hoạt StopFailure thay thế. Hook nhận tin nhắn cuối cùng của Claude trong last_assistant_message, và có thể trả về decision: "block" kèm reason để Claude tiếp tục thay vì dừng lại.
01Khi nào Stop được kích hoạt và khi nào không
Stop được kích hoạt ở cuối mỗi lượt, chứ không chỉ khi cả task hoàn tất. Claude Code không có khái niệm "task"; nó chỉ biết Claude đã ngừng trả lời và đang chờ bạn. Một câu hỏi ngắn cho ra một Stop, và một đợt refactor hai tiếng cũng vậy.
Hook không được kích hoạt:
- khi bạn nhấn Esc hoặc ngắt lượt theo cách khác;
- khi lượt kết thúc do lỗi API như rate limit, quá tải hoặc lỗi xác thực. Với các trường hợp này, Claude Code kích hoạt
StopFailure, kèm trườngerrormà bạn có thể so khớp (rate_limit,overloaded,server_errorvà các giá trị khác); - khi một subagent làm xong. Trường hợp đó là
SubagentStop.
Stop không hỗ trợ matcher, nên mọi matcher bạn thêm vào đều bị bỏ qua và hook chạy ở mọi lượt.
02Stop hook đơn giản nhất
Thêm đoạn này vào ~/.claude/settings.json để chạy một lệnh mỗi khi Claude làm xong. Ví dụ này phát âm thanh trên macOS:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "afplay /System/Library/Sounds/Glass.aiff"
}
]
}
]
}
}
Để xem các lệnh thông báo desktop trên Mac, Windows và Linux, hãy xem cách nhận thông báo khi Claude Code làm xong. Phần còn lại của hướng dẫn này nói về những gì bạn có thể làm với input và output của hook.
03Hook nhận được gì
Ngoài các trường chung (session_id, transcript_path, cwd, permission_mode, hook_event_name), hook Stop còn nhận được:
last_assistant_messageNội dung câu trả lời cuối cùng của Claude trong lượt nàystop_hook_activetrue nếu Claude đang tiếp tục vì một Stop hook đã chặnbackground_tasksCác task vẫn đang chạy, như shell chạy nền hoặc subagentsession_cronsCác lần đánh thức đã lên lịch trong session này, ví dụ từ /loopHãy dùng last_assistant_message thay vì đọc file transcript. Transcript được ghi bất đồng bộ nên có thể chưa chứa tin nhắn cuối khi hook chạy.
background_tasks và session_crons cho bạn biết session đã thực sự xong hay chỉ tạm dừng cho đến khi tác vụ nền đánh thức nó lại. Script thông báo có thể kiểm tra cả hai đều rỗng trước khi báo "đã xong".
04Đưa tin nhắn cuối của Claude vào thông báo
Script này (macOS và Linux, cần jq) hiển thị phần đầu câu trả lời của Claude và im lặng nếu tác vụ nền vẫn đang chạy:
#!/bin/bash
# ~/.claude/hooks/notify-done.sh
input=$(cat)
# Skip if background tasks are still in flight
busy=$(jq '(.background_tasks // []) | length' <<<"$input")
[ "$busy" -gt 0 ] && exit 0
msg=$(jq -r '(.last_assistant_message // "Claude has finished")
| gsub("\\s+"; " ") | .[0:140]' <<<"$input")
if command -v osascript >/dev/null 2>&1; then
osascript -e 'on run argv' \
-e 'display notification (item 1 of argv) with title "Claude has finished"' \
-e 'end run' "$msg"
elif command -v notify-send >/dev/null 2>&1; then
notify-send "Claude has finished" "$msg"
fi
exit 0
Lưu lại, chạy chmod +x ~/.claude/hooks/notify-done.sh, rồi trỏ hook tới nó:
{
"hooks": {
"Stop": [
{
"hooks": [{ "type": "command", "command": "~/.claude/hooks/notify-done.sh" }]
}
]
}
}
Script luôn thoát với mã 0. Điều này quan trọng: với Stop, exit code 2 nghĩa là "đừng dừng", nên một script thông báo vô tình thoát với mã 2 sẽ khiến Claude tiếp tục làm việc.
05Để Claude tiếp tục làm việc
Stop là một trong những event có thể chặn. Chặn việc dừng nghĩa là Claude tiếp tục lượt hiện tại, dùng lý do của bạn làm chỉ dẫn tiếp theo. Có hai cách:
- thoát với mã
2, và Claude nhận stderr của bạn làm lý do; hoặc - thoát với mã
0và in ra JSON:
{
"decision": "block",
"reason": "The test suite is failing. Run npm test, fix the failures, then finish."
}
Nếu hook đưa ra hướng dẫn thay vì báo lỗi, hookSpecificOutput.additionalContext cũng giữ cho cuộc hội thoại tiếp tục, nhưng transcript sẽ ghi nhận nó là phản hồi từ Stop hook thay vì lỗi hook:
{
"hookSpecificOutput": {
"hookEventName": "Stop",
"additionalContext": "Please run the test suite before finishing"
}
}
Tránh vòng lặp vô hạn
Một Stop hook luôn chặn sẽ không bao giờ cho Claude dừng. Có hai cơ chế bảo vệ bạn:
stop_hook_activelàtruekhi Claude đang tiếp tục do một Stop hook. Hãy kiểm tra nó và thoát sớm.- Claude Code vẫn sẽ kết thúc lượt sau khi các Stop hook đã chặn tám lần liên tiếp mà không có tool call nào xen giữa. Biến môi trường
CLAUDE_CODE_STOP_HOOK_BLOCK_CAPcho phép nâng giới hạn đó.
Đây là một hook khiến Claude sửa các test bị fail một lần, rồi cho phép nó dừng:
#!/bin/bash
# ~/.claude/hooks/tests-must-pass.sh
input=$(cat)
[ "$(jq -r '.stop_hook_active' <<<"$input")" = "true" ] && exit 0
if ! npm test --silent >/dev/null 2>&1; then
jq -nc '{decision: "block", reason: "npm test is failing. Fix the failures, then finish."}'
fi
exit 0
Hook chạy trong thư mục làm việc của session, nên npm test sẽ chạy trong project của bạn. Hãy giữ cho các bước kiểm tra thật nhanh: mặc định một command hook có thể chạy tối đa 600 giây, và Claude sẽ chờ nó.
Nếu bạn chỉ muốn Claude tiếp tục làm việc hướng tới một mục tiêu mà không cần viết script, lệnh /goal có sẵn của Claude Code sẽ thiết lập giúp bạn một Stop hook trong phạm vi session.
06Thông báo cả khi gặp lỗi
Vì lỗi API sẽ bỏ qua Stop, thông báo "đã xong" sẽ không cho bạn biết Claude đã dừng do dính rate limit. Nếu muốn được báo trường hợp này, hãy thêm cùng script đó vào StopFailure. Output và exit code của nó bị bỏ qua, nên nó chỉ có thể thông báo hoặc ghi log. Với StopFailure, last_assistant_message chứa nội dung lỗi, ví dụ "API Error: Rate limit reached".
07Claude Code Notifier nằm ở đâu trong bức tranh này
Một Stop hook gọi tới dịch vụ push là cách hoàn toàn ổn để biết Claude đã xong khi bạn không ngồi ở máy. Nếu muốn tự làm, hướng dẫn ntfy là điểm khởi đầu tốt.
Claude Code Notifier dùng cùng Stop hook đó, chỉ khác một điểm: nó có thể gửi phản hồi ngược lại. Công cụ dòng lệnh miễn phí của nó, ccn, chạy trên Mac, Windows và Linux, và tự hiển thị thông báo trên desktop. Khi đã ghép nối app iPhone hoặc Android, bạn sẽ nhận push "Claude đã xong" ngay khi một lượt kết thúc, và có thể gõ chỉ dẫn tiếp theo trên điện thoại. ccn trả nó về từ Stop hook làm chỉ dẫn tiếp theo cho Claude, nên session vẫn tiếp tục dù bạn không ngồi trước bàn phím. Nếu bạn vừa gõ phím trong 30 giây gần nhất, sẽ không có gì chờ điện thoại cả. Đây là sản phẩm độc lập, không phải do Anthropic làm. Xem rời máy khi chạy tác vụ dài để biết quy trình này và trang ccn để cài đặt.
08Câu hỏi thường gặp
Stop hook chạy khi Claude hoàn thành toàn bộ tác vụ, hay sau mỗi câu trả lời?
Sau mỗi câu trả lời. Stop kích hoạt mỗi khi agent chính trả lời xong, bất kể yêu cầu lớn hay nhỏ. Nếu cần biết còn việc nào đang được lên lịch không, hãy kiểm tra background_tasks và session_crons trong input.
Tại sao Stop hook của tôi không chạy?
Có thể bạn đã ngắt lượt đó, vốn sẽ bỏ qua Stop, hoặc lượt kết thúc bằng lỗi API, khi đó StopFailure sẽ chạy thay thế. Nếu không phải vậy, hãy kiểm tra /hooks có liệt kê hook của bạn không và script đã có quyền thực thi chưa. Danh sách kiểm tra khắc phục sự cố sẽ giúp bạn phần còn lại.
Tại sao Claude vẫn tiếp tục làm việc sau khi Stop hook chạy?
Hook của bạn đang chặn việc dừng, bằng cách thoát với code 2 hoặc in ra decision: "block". Hãy đảm bảo hook chỉ dùng để thông báo luôn thoát với 0, và mọi hook có chặn đều kiểm tra stop_hook_active trước.
Làm sao lấy câu trả lời cuối cùng của Claude trong hook?
Đọc last_assistant_message từ JSON trên stdin, ví dụ bằng jq -r '.last_assistant_message'. Đừng dựa vào transcript_path: lúc Stop chạy, file có thể chưa chứa tin nhắn cuối.
Stop và SubagentStop khác nhau thế nào?
Stop dành cho agent chính. SubagentStop chạy khi một subagent hoàn thành, match theo loại agent và hỗ trợ cùng các trường chặn. Thông báo "Claude đã xong" nên đặt ở Stop.