Phát âm thanh báo khi Claude Code thật sự hoàn tất
Một số tác vụ trong Claude Code chạy khá lâu. Bạn khởi động một cái, rồi ngồi chờ — nhìn terminal, đợi nó ngừng cuộn. Chuyện nhỏ thôi, nhưng làm cả ngày thì mỏi. Mắt bạn dán vào một màn hình phần lớn thời gian chẳng thay đổi, chỉ để biết đúng lúc nào tới lượt mình.
Cách tôi chốt lại rất đơn giản: phát một âm thanh khi Claude xong. Rồi tôi có thể rời mắt hẳn — đọc gì đó, đi pha cà phê — và tai sẽ báo tôi biết khi nào quay lại. Điểm mấu chốt, cũng là phần thú vị của bài này, là làm cho chữ "xong" mang nghĩa thật sự xong khi đang có nhiều agent chạy song song.
Vấn đề: canh chừng rất tốn kém
Chờ một tác vụ dài có cái giá thật sự:
- Nó chiếm sự tập trung của bạn. Bạn không thể chuyển hẳn sang việc khác, vì đang nửa mắt canh xem tác vụ xong chưa. Thế là bạn nhận cả hai cái tệ: mắt không được nghỉ, mà việc kia cũng chẳng thật sự làm.
- Bạn kiểm tra quá thường xuyên. Cứ 20 giây lại liếc về terminal. Chẳng có gì thay đổi. Chính cái liếc liên tục đó bào mòn bạn qua một ngày.
- "Xong" rất mập mờ khi có subagent. Khi một tác vụ sinh ra vài agent chạy song song, phần trả lời chính có thể im ắng trong khi các agent nền vẫn đang làm. Nên "nó ngừng in ra" không đáng tin là "nó đã xong".
Tôi muốn thôi nhìn và bắt đầu nghe — nhưng chỉ nghe một tín hiệu mà tôi tin được.
Viên gạch nền: hook Stop
Claude Code có thể chạy một lệnh shell khi có sự kiện xảy ra. Đó là các hook, và
bạn cấu hình chúng trong settings.json. Cái tôi quan tâm ở đây là Stop — nó kích
hoạt khi Claude kết thúc một lượt và trả quyền lại cho bạn.
Phiên bản đơn giản nhất chỉ việc phát âm thanh khi stop:
"Stop": [
{ "hooks": [
{ "type": "command",
"command": "paplay /usr/share/sounds/freedesktop/stereo/complete.oga" }
]}
]
(paplay là trình phát của PulseAudio trên Linux; trên macOS bạn dùng afplay, còn
Windows thì một dòng PowerShell.) Với các tác vụ đơn giản, cái này đã hữu ích rồi.
Nghịch thử năm phút là thấy tuyệt — cho tới cái ngày nó nói dối bạn.
Vì sao bản đơn giản chưa đủ
Sự kiện Stop có thể kích hoạt trong khi công việc nền vẫn đang chạy. Nếu tác vụ của
bạn đã bung ra các subagent song song, hoặc khởi động một lệnh shell dài ở chế độ
nền, thì lượt đó có thể "dừng" ở tiền cảnh trong khi những thứ kia vẫn tiếp tục. Cái
hook ngây thơ phát ra âm thanh vui vẻ của nó, bạn quay lại — và Claude vẫn đang bận.
Một âm báo "xong" mà bạn không tin được thì còn tệ hơn là không có, vì giờ bạn lại hoài nghi nó và vẫn phải nhìn màn hình. Nên mục tiêu thật sự không phải là "phát âm thanh khi stop". Mà là chỉ phát âm thanh khi mọi thứ thật sự đã xong.
Nghĩa là tại thời điểm stop phải trả lời hai câu hỏi:
- Còn subagent nào đang chạy không?
- Còn lệnh shell nền nào đang chạy không?
Nếu một trong hai là "còn", hãy im lặng. Chỉ khi cả hai đều "hết" thì mới an toàn để kêu.
Đếm agent bằng hook
Subagent chạy bên trong tiến trình Claude, nên chúng không hiện ra như những chương trình riêng để bạn tìm. Tôi tự theo dõi chúng bằng một bộ đếm nhỏ — một file tạm cho mỗi phiên — được cập nhật bởi ba hook nữa:
UserPromptSubmitđặt lại bộ đếm về0ở đầu mỗi lượt.PreToolUsetrên công cụAgentcộng một mỗi khi một agent bắt đầu.SubagentStoptrừ một mỗi khi một agent kết thúc.
"UserPromptSubmit": [{ "hooks": [{ "type": "command",
"command": "sid=$(cat | jq -r '.session_id'); echo 0 > /tmp/claude-agents-$sid.count" }]}],
"PreToolUse": [{ "matcher": "Agent", "hooks": [{ "type": "command",
"command": "sid=$(cat | jq -r '.session_id'); f=/tmp/claude-agents-$sid.count; echo $(( $(cat $f) + 1 )) > $f" }]}],
"SubagentStop": [{ "hooks": [{ "type": "command",
"command": "sid=$(cat | jq -r '.session_id'); f=/tmp/claude-agents-$sid.count; echo $(( $(cat $f) - 1 )) > $f" }]}]
Giờ, tại bất kỳ thời điểm nào, file đó giữ số agent còn đang chạy. Nếu nó lớn hơn 0 lúc lượt dừng lại, tức là còn việc sắp tới.
Script "người gác cổng"
Thay vì phát âm thanh trực tiếp, hook Stop của tôi gọi một script nhỏ để quyết
định có nên phát hay không. Script trả về 0 nghĩa là "cứ chạy đi" và 1 nghĩa là
"im lặng", và hook chỉ phát âm thanh khi nhận 0:
sid=$(cat | jq -r '.session_id')
echo idle > /tmp/claude-sl-$sid.state
~/.claude/hooks/notify-done.sh "$sid" && paplay .../complete.oga 2>/dev/null || true
Bản thân script chạy hai lần kiểm tra. Đầu tiên là bộ đếm agent:
n=$(cat "/tmp/claude-agents-$sid.count" 2>/dev/null)
[ "${n:-0}" -gt 0 ] && exit 1 # còn agent chạy -> im lặng
Thứ hai là các lệnh shell nền. Khác với agent, các lệnh bash nền là tiến trình
con thật của Claude CLI, nên script duyệt cây tiến trình: nó tìm tiến trình Claude,
rồi tìm bất kỳ tiến trình con bash nào còn sót lại (cẩn thận bỏ qua chính nó). Nếu
tìm thấy, tức là vẫn còn việc đang chạy:
# (rút gọn) với mỗi tiến trình có cha là Claude CLI:
# nếu nó là một shell bash không phải chính ta -> việc nền còn sống -> exit 1
Nếu cả hai lần kiểm tra đều "sạch" — bộ đếm bằng 0, không còn shell nền lạc lõng nào —
script trả về 0, và âm thanh vang lên. Giờ tiếng chuông mang đúng nghĩa tôi muốn:
mọi thứ đã xong, quay lại đi.
Script cũng ghi lại mỗi quyết định vào một file tạm, nên nếu có lúc nó im lặng trong khi tôi mong có âm thanh, tôi có thể xem tại sao: "agent counter=2" hay "background bash alive" cho tôi biết nó đã đúng khi chờ.
Không chỉ là âm thanh
Một khi bạn có một tín hiệu "xong" đáng tin, âm thanh chỉ là một trong nhiều thứ bạn
có thể làm với nó. Cũng chính hook đó có thể bật một thông báo desktop (notify-send
trên Linux, osascript trên macOS), chớp nháy terminal, hoặc gửi tin nhắn tới điện
thoại. Tôi thích âm thanh vì nó ở dạng nền — tôi thậm chí không cần đang nhìn đúng
màn hình.
Vài điểm cần lưu ý
- Chọn trình phát hợp với hệ điều hành.
paplaylà của Linux/PulseAudio. macOS cóafplay, Windows có PowerShell. Hãy chọn một file âm thanh thật sự tồn tại trên máy bạn. - Chạy headless hoặc qua SSH có thể không có âm thanh. Cái
|| trueở cuối giúp một trình phát bị thiếu không biến thành lỗi — đáng để giữ lại. - Giữ các bước kiểm tra rẻ. Cái này chạy ở mỗi lần stop, nên script cần nhanh và
không gọi mạng. Đọc một file đếm và quét
/proclà đủ nhanh.
Kết lại
Mặc định người ta cho rằng bạn sẽ nhìn màn hình để biết khi nào Claude xong. Với các
tác vụ dài, đó là một khoản thuế đánh vào mắt và sự tập trung của bạn. Một hook Stop
phát âm thanh sẽ gỡ bỏ khoản thuế đó — miễn là âm thanh trung thực. Toàn bộ mẹo là
làm cho "xong" nghĩa là tất cả agent và việc nền đã kết thúc, chứ không chỉ là tiền
cảnh đã im. Đếm agent bằng một bộ đếm, kiểm tra các shell nền còn sót, rồi mới kêu.
Sau đó, bạn có thể rời mắt và tin vào tai mình.
Link tham khảo
Bài viết gốc (tiếng Anh): Play a Sound When Claude Code Is Actually Done
All rights reserved