Claude Code Hooks: Chặn Lệnh Xóa Nguy Hiểm trước khi Agent Chạy
Dòng dặn trong CLAUDE.md, model có thể bỏ qua. Hook là code chạy mỗi lần sự kiện xảy ra. Đây là cấu hình và script mình đã thử với 12 lệnh mẫu, kèm những chỗ nó vẫn lọt.

Mình từng nghĩ chỉ cần dặn một dòng trong CLAUDE.md là agent sẽ không chạy lệnh xóa bừa. Nhưng dòng dặn đó chỉ là lời nhắc, model vẫn có thể bỏ qua. Hook thì khác: đó là lệnh do bạn viết, và Claude Code tự chạy nó ở những thời điểm cố định, như trước và sau khi dùng công cụ, khi phiên bắt đầu, khi agent dừng, cùng nhiều thời điểm khác. Vì hook là code chạy mỗi lần sự kiện xảy ra, model không bỏ qua được.

CLAUDE.md có thể bị quên, còn thanh chắn thì lần nào cũng hạ.Trong bài này, mình dùng hook cho một việc cụ thể: chặn lệnh xóa đệ quy trước khi nó kịp chạy. Mình đã đối chiếu mọi hành vi với tài liệu hooks chính thức ngày 01/11/2026. Tính năng này thay đổi khá nhanh, nên khi bạn áp dụng, hãy mở lại tài liệu để chắc chắn.
Ba hiểu lầm làm hook "chặn" mà không chặn
Trước khi viết dòng code nào, mình muốn gỡ ba hiểu lầm mà tài liệu cho thấy là dễ mắc nhất, vì chính chúng làm một hook trông như đang chặn mà thực ra không chặn gì:
Tưởng: exit 1 là chặnThực tế: chỉexit 2mới chặn. Script "báo lỗi" theo thói quen Unix (exit 1) để lệnh chạy tiếp.Tưởng: hook treo thì lệnh dừngThực tế: hook bị timeout cũng không chặn; lệnh đi tiếp qua luồng xin quyền bình thường.Tưởng: hook trả allow là xongThực tế:allowkhông thắng được luậtdenybạn đã đặt trong phần quyền.
Hook là gì và khai báo ở đâu
Ba file settings và phạm vi của từng file
| File | Phạm vi | Commit? |
|---|---|---|
~/.claude/settings.json | Mọi dự án trên máy bạn | Không, file cá nhân |
.claude/settings.json | Một dự án, cả nhóm dùng chung | Có |
.claude/settings.local.json | Một dự án, chỉ máy bạn | Không |
Ngoài ba file này, plugin, skill và subagent cũng có thể mang hook riêng, nên đôi khi một hook đang chạy mà bạn không nhớ mình đã khai báo nó.
Sự kiện, matcher, handler
Khi đã biết đặt hook ở file nào, bước tiếp theo là hiểu một khai báo được ghép từ đâu. Mình hay hình dung nó thành ba tầng:
- Sự kiện, ví dụ
PreToolUse(trước khi chạy công cụ). - Matcher, tên công cụ cần bắt, ví dụ
Bash. - Handler, thứ sẽ chạy: thường là một lệnh, nhưng cũng có thể là HTTP endpoint, công cụ MCP, prompt gửi cho model, hoặc subagent.
Chặn rm đệ quy trong 3 bước
Giờ đến phần làm thật. Mỗi lần agent gọi công cụ, Claude Code gửi thông tin về lần gọi đó vào stdin của hook dưới dạng JSON; với công cụ Bash, lệnh sắp chạy nằm ở tool_input.command. Cả bộ chặn của mình chỉ xoay quanh việc đọc trường đó.
Bước 1: khai báo hook trong settings.json
Vì sao cần bước này: Claude Code chỉ chạy hook khi bạn ghi nó vào file cài đặt, giống việc dán tờ nội quy lên cổng thì người gác mới biết phải kiểm gì.
Làm ở đâu: trong thư mục dự án, mở (hoặc tạo) thư mục
.claude, rồi tạo filesettings.jsonbằng trình soạn thảo bạn quen, như VS Code hay Notepad. Dán nguyên đoạn dưới vào và lưu lại. Nói bằng lời thường, đoạn này bảo Claude Code: trước mỗi lệnh Bash hoặc PowerShell, hãy chạy scriptchan-xoa.mjsbằng Node; script hỏng thì cũng chặn luôn..claude/settings.jsonjson { "hooks": { "PreToolUse": [ { "matcher": "Bash|PowerShell", "hooks": [ { "type": "command", "command": "node", "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/chan-xoa.mjs"], "onFailure": "block" } ] } ] } }argsbật dạng exec: Claude Code gọi thẳngnodevới đường dẫn script, không qua shell, nên đường dẫn có dấu cách vẫn đúng. Tài liệu khuyên dùng dạng này khi có biến đường dẫn như${CLAUDE_PROJECT_DIR}.onFailure: "block"làm hook bị lỗi hay treo cũng tính là chặn. Thiếu nó, một script gõ sai đường dẫn sẽ âm thầm cho mọi lệnh đi qua.
Bước 2: viết script chặn bằng Node
Vì sao cần bước này: file ở bước 1 mới chỉ nói "hãy gọi người gác"; script này chính là người gác, quyết định lệnh nào bị chặn.
Làm ở đâu: trong thư mục
.claudevừa tạo, tạo thêm thư mụchooks, rồi tạo filechan-xoa.mjsvà dán đoạn dưới vào. Máy cần cài sẵn Node.js để chạy file này. Nói gọn, script đọc lệnh Claude sắp chạy, nếu thấy đó là lệnh xóa cả thư mục thì in lý do và trả mã 2 để chặn; mọi lệnh khác nó để yên..claude/hooks/chan-xoa.mjsjs let raw = ''; for await (const chunk of process.stdin) raw += chunk; const cmd = JSON.parse(raw).tool_input?.command ?? ''; // rm kèm cờ đệ quy ở bất kỳ vị trí nào: -r, -R, -rf, -fr, -r -f, --recursive const rmDeQuy = /\brm\b[^;&|\n]*\s(-[a-zA-Z]*[rR][a-zA-Z]*|--recursive)\b/; // PowerShell: Remove-Item (hoặc alias ri, del, rd, rmdir) kèm -Recurse const psDeQuy = /\b(Remove-Item|ri|del|rd|rmdir)\b[^;\n]*-Recurse/i; if (rmDeQuy.test(cmd) || psDeQuy.test(cmd)) { console.error('Lệnh xóa đệ quy bị chặn. Hãy xóa từng file cụ thể.'); process.exit(2); }Thoát mã 2 nghĩa là chặn: lệnh không chạy, và nội dung stderr được gửi cho Claude để nó biết lý do và đổi cách làm. Thoát mã 0 mà không in gì nghĩa là hook không có ý kiến, lệnh đi tiếp qua luồng xin quyền như thường. Im lặng không phải là duyệt.
Mình chọn viết script bằng Node thay vì Bash để nó chạy giống nhau trên Windows, macOS và Linux, miễn là máy có Node. Ví dụ Bash trong tài liệu chính thức thì cần cài thêm
jq.Bước 3: thử với 12 cách viết của cùng một lệnh
Vì sao cần bước này: một người gác chưa từng diễn tập rất dễ để lọt người. Thử với nhiều cách viết của cùng một lệnh là cách duy nhất biết hook chặn thật hay không.
Viết xong, mình chạy thử script với 12 lệnh mẫu trên Node 24:
Kết quả Lệnh Bị chặn rm -rf,rm -fr,rm -Rf,rm -r -f,rm --recursive --force,sudo rm -rf /,cd a && rm -rf b,Remove-Item -Recurse -ForceCho qua ls -la,rm -f file.txt,git rm --cached,Remove-Item a.txtẢnh chụp thật
Mình chạy lại đúng script ở bước 2 trên Node 24.14.1 ngày 01/11/2026 với 12 lệnh trong bảng: 8 lệnh xóa đệ quy nhận exit 2, 4 lệnh an toàn nhậnexit 0.Bản đầu tiên mình viết chỉ dò
-rfvà-frliền nhau, nên để lọtrm -r -fvàrm --recursive. Mình chỉ phát hiện ra nhờ bước thử này, và đó cũng là lý do mình khuyên bạn tự thử hook với nhiều cách viết của cùng một lệnh.
Hỏi lại thay vì chặn: trả lời bằng JSON
allow, deny, ask, defer khác nhau thế nào
Chặn hẳn không phải lúc nào cũng là điều mình muốn. Có những lệnh mình chỉ cần được hỏi lại trước khi chạy, và cho trường hợp đó, hook PreToolUse có thể thoát mã 0 rồi in ra stdout (phần chữ chương trình in ra màn hình) một object JSON thay vì thoát mã 2. Đoạn dưới là thứ script in ra, không phải file bạn tạo; nó nói với Claude Code: "đừng tự quyết, hãy hỏi người dùng, kèm lý do này":
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": "Lệnh git push cần bạn xác nhận"
}
}permissionDecision nhận allow, deny, ask hoặc defer. ask hữu ích khi bạn không muốn chặn hẳn mà muốn được hỏi lại, ví dụ trước mọi lệnh git push. defer chỉ có tác dụng khi chạy không tương tác với cờ -p (dành cho app tự gọi Claude Code); trong phiên bình thường, Claude Code bỏ qua nó.
Khi nhiều hook trả lời khác nhau
Mọi chuyện phức tạp hơn một chút khi bạn có nhiều hook cùng bắt một sự kiện, vì chúng chạy song song. Nếu chúng trả quyết định khác nhau, thứ tự ưu tiên là deny > defer > ask > allow.
Hook trên Windows và PowerShell
Mình làm việc chủ yếu trên Windows, nên phần này mình kiểm khá kỹ. Trên Windows, công cụ shell của Claude Code có thể là PowerShell thay vì Bash. Matcher Bash|PowerShell ở trên bắt cả hai, và script đã có sẵn mẫu cho Remove-Item -Recurse.
Nếu muốn viết hook bằng PowerShell, ví dụ trong tài liệu gọi powershell.exe với -NoProfile (bỏ qua profile để hook khởi động nhanh) và -ExecutionPolicy Bypass (cho phép chạy file script local). Tài liệu cũng dùng trường if, ví dụ "if": "PowerShell(Remove-Item *)", để chỉ khởi chạy hook khi lệnh khớp mẫu. Mỗi trường if chỉ chứa một luật, nên Bash và PowerShell cần hai handler riêng.
Những gì hook không chặn được
Đến đây mình phải thành thật về giới hạn của cách làm này. Regex chỉ khớp chuỗi ký tự, nó không hiểu lệnh làm gì. Chính tài liệu cũng nói trường if chỉ lọc ở mức cố gắng, và khuyên dùng hệ thống quyền (luật deny) nếu cần chặn cứng.
| Tình huống | Script trên | Nên dùng thêm |
|---|---|---|
find . -delete | Lọt | Luật deny |
| Script Python tự xóa file, hoặc công cụ sửa file | Lọt | Quyền hạn hợp lý, bản sao lưu |
git rm -r thu-muc | Chặn nhầm, vì khớp mẫu rm ... -r | Chỉnh regex theo cách bạn làm |
Khi hook không chạy như mong đợi
Lúc tự dựng hook, mình gặp nhiều nhất là bốn tình huống dưới đây. Cách xử lý nào cũng lấy từ mục troubleshooting trong tài liệu hooks, nên bạn có thể đối chiếu lại khi tài liệu đổi.
/hooks không thấy hook nào
Thường là do file settings chứ không phải script. Mình kiểm theo thứ tự: JSON có hợp lệ không (dấu phẩy thừa và comment đều không được phép), file có nằm đúng .claude/settings.json hay ~/.claude/settings.json không. Claude Code tự đọc lại file khi bạn sửa; nếu vài giây sau vẫn chưa thấy, khởi động lại phiên. Nếu menu báo Only hooks from managed settings run here, tổ chức của bạn đã bật allowManagedHooksOnly và hook cá nhân sẽ không chạy.
Hook có trong /hooks nhưng không bao giờ chạy
Matcher phân biệt hoa thường, nên bash không khớp công cụ Bash. Mình cũng từng nhầm sự kiện: PreToolUse chạy trước lệnh, PostToolUse chạy sau khi lệnh đã xong, nên muốn chặn thì phải dùng PreToolUse.
Transcript hiện "PreToolUse hook error"
Script đã thoát với mã khác 0 và 2. Cách nhanh nhất là tự đóng vai Claude Code: mở terminal tại thư mục dự án, gõ hai dòng dưới (dòng đầu đưa cho script một lệnh mẫu ls, dòng sau in ra mã thoát; trong PowerShell thay echo $? bằng echo $LASTEXITCODE):
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | node .claude/hooks/chan-xoa.mjs
echo $?Nếu lỗi là command not found hay No such file or directory, đường dẫn script sai. Đây là lý do mình dùng ${CLAUDE_PROJECT_DIR} kèm args, và bật onFailure: "block" để đường dẫn sai thì chặn chứ không cho lọt. Trên Windows, nếu thông báo có dòng blocking because Git Bash wasn't found, máy không có Git Bash và hook đã chặn vì thất bại.
Hook in JSON nhưng quyết định không có tác dụng
Có hai nguyên nhân tài liệu nêu rõ. Một là có chữ khác in ra stdout trước JSON, thường là lệnh echo trong profile shell, khiến output không còn bắt đầu bằng {. Hai là đặt trường sai tầng: permissionDecision phải nằm bên trong hookSpecificOutput, không phải ở ngoài cùng. Chạy claude --debug rồi tìm Hook output does not start with { hoặc Hook JSON output had unrecognized keys trong log là thấy ngay.
Công cụ dựng sẵn trong Skills hub
Nếu bạn không muốn tự viết từ đầu, mình đã ghi lại hai công cụ dựng sẵn trong Skills hub:
- destructive-command-guard là bộ chặn lệnh nguy hiểm viết sẵn, nhiều mẫu hơn ví dụ ở trên. Trang hub ghi lại hai điều cần biết trước khi cài: giấy phép có hạn chế riêng, và có báo cáo Windows Defender chặn file chạy của nó.
- tdd-guard dùng hook để chặn agent viết code khi chưa có test thất bại. Nó gọi thêm model để kiểm tra mỗi lần sửa, nên tốn thêm token; một phép đo độc lập không thấy code tốt hơn.
Ngoài việc chặn lệnh, hook còn giúp tiết kiệm token: tài liệu chi phí có ví dụ hook PreToolUse sửa lệnh chạy test để chỉ in ra phần thất bại. Xem thêm trong bài tiết kiệm token Claude Code.
- exit 1 không chặn gì cả. Chỉ exit 2 mới chặn.
- Hook timeout thì lệnh vẫn chạy, trừ khi đặt
onFailure: "block". - CLAUDE.md là lời nhắc, hook là code: model không bỏ qua được.
- Regex khớp chữ, không hiểu lệnh:
find . -deletevẫn lọt.
Đọc tiếp
Claude CodeClaude Code + Remotion: Từ Prompt ra Video MP4Remotion biến component React thành video, Claude Code viết và sửa nó bằng chat. Quy trình 4 cấp độ, cách chọn phong cách và 9 lỗi cần kiểm trước khi render.→
Claude CodeQuảng Cáo Giảm 65% Token, Đo Thật Chỉ 8,5%Một skill hứa giảm 65% token, phép đo của JetBrains chỉ thấy 8,5%. Các cách miễn phí có sẵn trong Claude Code, và cách đọc phép đo trước khi cài công cụ ngoài.→
Next.jsTăng Tốc Next.js: Lighthouse Mobile 58 lên 71Những gì đã làm trên một site Next.js static export: LCP từ 8,3s còn 5,4s, trang nhẹ từ 1.129 KiB còn 604 KiB. Cả thay đổi có ích lẫn thay đổi làm trang tệ đi.→