Hướng dẫn

Hooks của Claude Code: biến quy tắc của agent thành đảm bảo

Cập nhật: · Skillaria.Top

Hooks của Claude Code là các lệnh shell mà chính Claude Code chạy vào những sự kiện nhất định: trước và sau khi gọi công cụ, khi gửi prompt, khi bắt đầu phiên và trước khi agent dừng. Chúng được cấu hình trong settings.json, và mã thoát 2 chặn hành động rồi trả thông báo lỗi về cho agent, nên hook biến một quy tắc trong CLAUDE.md thành đảm bảo mà mô hình không thể quên.

Hooks của Claude Code là gì

Hook là một lệnh không do mô hình chạy, mà do chính chương trình Claude Code chạy vào thời điểm đã định trong quá trình agent làm việc. Agent sắp chạy rm -rf — hook được kích hoạt và quyết định có cho phép lệnh hay không. Agent vừa sửa một file — hook chạy công cụ định dạng code. Agent muốn kết thúc công việc — hook chạy test và không cho dừng khi test còn đỏ.

Điểm khác biệt chính so với hướng dẫn trong CLAUDE.md: hướng dẫn là văn bản mà mô hình có thể tuân theo với một xác suất nào đó, còn hook là mã mang tính quyết định. Nó luôn chạy, bất kể ngữ cảnh dài đến đâu, agent đã «quyết định» gì và có quên quy tắc ở phút thứ một trăm của phiên hay không. Vì vậy hooks của Claude Code là cách chính để biến một mong muốn thành đảm bảo.

Tại sao quy tắc trong CLAUDE.md là chưa đủ

Bức tranh điển hình: agent vừa hoàn thành một tính năng, các unit test đều xanh, trong chat nó tự tin viết «Xong». Nhưng không ai chạy kịch bản đầu cuối, linter không được chạy, và migration thì bị lỗi giữa chừng. Trong CLAUDE.md có ghi «trước khi kết thúc hãy chạy test». Mô hình không cố ý lừa dối — nó chỉ tự đánh giá bản thân, và tự đánh giá thì không đáng tin.

Từ đó có nguyên tắc của harness: mọi thứ có thể kiểm tra bằng máy thì để cơ chế kiểm tra, không để mô hình. Quy tắc «chạy linter» trở thành một hook chạy linter. Quy tắc «không đụng vào cấu hình production» trở thành một hook chặn việc ghi vào file đó. Hướng dẫn bằng văn bản vẫn được giữ lại, nhưng chỉ để giải thích cho agent lý do nó bị chặn.

Các sự kiện: khi nào hook được kích hoạt

Mỗi hook gắn với một sự kiện trong vòng đời của phiên làm việc. Các sự kiện chính:

Sự kiệnKhi nào kích hoạtỨng dụng điển hình
PreToolUsetrước khi gọi một công cụ (Bash, Edit, Write, v.v.)chặn lệnh nguy hiểm, bảo vệ file
PostToolUsesau khi gọi công cụ thành côngcông cụ định dạng, linter, test nhanh sau khi sửa
UserPromptSubmitkhi người dùng gửi prompt, trước khi xử lýthêm ngữ cảnh, chặn prompt có bí mật
Stopkhi agent sắp kết thúc câu trả lờikhông cho dừng khi các kiểm tra chưa đạt
SubagentStopkhi subagent kết thúc công việckiểm tra kết quả của subagent
SessionStartkhi phiên bắt đầu hoặc được tiếp tụcnạp trạng thái: git status, kế hoạch, nhật ký tiến độ
SessionEndkhi phiên kết thúcdọn dẹp, ghi log
Notificationkhi Claude Code gửi thông báo (ví dụ đang chờ quyền)âm thanh hoặc tin nhắn tới điện thoại
PreCompacttrước khi nén ngữ cảnhlưu thông tin quan trọng trước khi nén

Những sự kiện hữu ích nhất cho độ tin cậy là PreToolUse, PostToolUse và Stop: chúng bao quát ba điểm yếu của agent — hành động nguy hiểm, các sửa đổi chưa kiểm tra và việc báo «xong» quá sớm.

Cấu hình hooks ở đâu: settings.json

Hooks được khai báo trong các file cấu hình của Claude Code:

  • ~/.claude/settings.json — cá nhân bạn, dùng cho mọi dự án;
  • .claude/settings.json trong repository — dùng chung cho cả nhóm, được commit vào git;
  • .claude/settings.local.json — cục bộ cho dự án, không đưa vào git.

Cấu trúc như sau: sự kiện → danh sách nhóm có matcher → danh sách lệnh.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint.sh",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

matcher là tên công cụ hoặc biểu thức chính quy trên tên đó: Bash, Edit|Write, .*. Nó cần cho các sự kiện liên quan đến công cụ; với Stop hoặc UserPromptSubmit có thể bỏ qua. Biến CLAUDE_PROJECT_DIR trỏ tới thư mục gốc của dự án, vì vậy các script nên đặt trong .claude/hooks/ và gọi qua biến này. Xem các hook hiện có bằng lệnh /hooks bên trong Claude Code.

Hook nhận dữ liệu và trả lời như thế nào

Đầu vào: JSON qua stdin

Claude Code gửi cho hook một JSON chứa thông tin về sự kiện: mã phiên, thư mục làm việc, tên sự kiện, và với công cụ thì là tool_name và tool_input. Với Bash, lệnh nằm trong tool_input.command; với Edit và Write, đường dẫn file nằm trong tool_input.file_path. Đọc thuận tiện nhất bằng jq.

{
  "session_id": "abc123",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "rm -rf build/" },
  "cwd": "/home/dev/project"
}

Đầu ra: mã trả về

MãĐiều gì xảy ra
0thành công, công việc tiếp tục; với UserPromptSubmit và SessionStart, stdout được thêm vào ngữ cảnh của agent
2lỗi chặn: stderr được chuyển cho agent và hành động bị dừng (với PreToolUse — lời gọi công cụ bị hủy, với Stop — agent tiếp tục làm việc)
kháclỗi không chặn: stderr hiển thị cho người dùng, công việc vẫn tiếp tục

Exit code 2 là cơ chế then chốt. Agent nhận nội dung stderr như phản hồi và có thể tự sửa: viết lại lệnh, sửa test, không đụng vào file. Vì vậy hãy viết vào stderr không phải «lỗi», mà là lý do cụ thể và việc nên làm thay thế.

Ngoài mã trả về, hook còn có thể in ra stdout một JSON chứa quyết định, ví dụ "decision": "block" và "reason". Cách này dùng để tinh chỉnh, nhưng với hầu hết tác vụ thì mã 2 cùng thông báo trong stderr là đủ.

Ví dụ 1: linter và test sau mỗi lần sửa

Hook trên PostToolUse với matcher Edit|Write chạy sau khi file bị thay đổi. Công cụ đã thực thi, không thể hoàn tác việc sửa, nhưng mã 2 sẽ trả đầu ra của linter về cho agent, và nó sửa lỗi ngay thay vì sau nửa tiếng.

#!/usr/bin/env bash
# .claude/hooks/lint.sh
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
  *.py)
    if ! out=$(ruff check "$file" 2>&1); then
      echo "Linter tìm thấy lỗi trong $file:" >&2
      echo "$out" >&2
      exit 2
    fi ;;
  *.php)
    if ! out=$(php -l "$file" 2>&1); then
      echo "$out" >&2
      exit 2
    fi ;;
esac
exit 0

Đừng đặt ở đây toàn bộ bộ test chạy mười phút: hook chạy sau mỗi lần sửa. Sau khi sửa chỉ nên chạy các kiểm tra nhanh (cú pháp, linter, test của một module), còn các kiểm tra nặng để cho hook Stop.

Ví dụ 2: chặn lệnh nguy hiểm

Hook trên PreToolUse với matcher Bash thấy lệnh trước khi nó chạy. Mã 2 hủy lời gọi và agent nhận được lời giải thích.

#!/usr/bin/env bash
# .claude/hooks/guard.sh
cmd=$(jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq 'rm -rf /|git push (-f|--force)|DROP (TABLE|DATABASE)'; then
  echo "Lệnh bị chặn theo chính sách dự án: $cmd. Các thao tác nguy hiểm do con người thực hiện." >&2
  exit 2
fi
exit 0

Tương tự, có thể bảo vệ file: hook trên Edit|Write kiểm tra file_path và chặn ghi vào .env, cấu hình production, code được sinh tự động hoặc các file migration đã được áp dụng. Lưu ý rằng tìm kiếm bằng biểu thức chính quy có thể bị vượt qua — agent có thể viết lệnh theo cách khác. Hook là lớp bảo vệ mạnh, nhưng không thay thế được quyền truy cập và hộp cát.

Ví dụ 3: agent không thể nói «xong» khi test còn đỏ

Hook Stop chạy khi agent sắp kết thúc. Nếu test không đạt, mã 2 ngăn không cho dừng, và đầu ra của test được chuyển cho agent như một việc tiếp theo. Đây là phiên bản cơ học của Definition of Done.

#!/usr/bin/env bash
# .claude/hooks/done.sh
input=$(cat)
# chống vòng lặp vô hạn: hook đã một lần kéo dài công việc
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi
if ! out=$(npm test --silent 2>&1); then
  echo "Test không đạt, công việc chưa thể kết thúc:" >&2
  echo "$out" | tail -n 40 >&2
  exit 2
fi
exit 0

Trường stop_hook_active bằng true khi agent đang tiếp tục làm việc vì một stop-hook. Nếu không có kiểm tra này, hook có thể kéo agent chạy vòng tròn mãi mãi — đúng tình huống «làm cả đêm nhưng sai hướng». Trong các vòng lặp tự trị, hãy thêm giới hạn số lần thử và điều kiện gọi người vào.

Ví dụ 4: nạp trạng thái khi bắt đầu phiên

Với SessionStart, stdout của hook đi vào ngữ cảnh. Tiện nhất là in nhánh hiện tại, các thay đổi chưa commit và tác vụ tiếp theo từ kế hoạch, để agent không phải bắt đầu từ con số không.

#!/usr/bin/env bash
echo "Nhánh: $(git branch --show-current)"
git status --short | head -n 20
[ -f PROGRESS.md ] && tail -n 15 PROGRESS.md
exit 0

Hooks biến quy tắc thành đảm bảo như thế nào

Có thể chia quy tắc của harness thành ba tầng. Tầng một là văn bản trong CLAUDE.md: giải thích cách làm việc, nhưng được tuân theo theo xác suất. Tầng hai là các kiểm tra chạy được: test, E2E, các quy tắc kiến trúc dưới dạng script sẽ báo lỗi khi bị vi phạm. Tầng ba là hooks, chạy các kiểm tra này tự động vào đúng thời điểm, không cần mô hình tham gia.

Quy tắc thực tế: nếu agent vi phạm cùng một quy tắc hai lần, đừng thêm vào CLAUDE.md một cảnh báo viết hoa nữa. Hãy hỏi xem vi phạm đó có thể kiểm tra bằng script không. Nếu được — viết script và gắn nó vào hook. Khi đó hướng dẫn bằng văn bản ngắn lại, còn hành vi ổn định hơn.

Quy tắc bằng văn bảnĐảm bảo qua hook
«Định dạng code theo chuẩn»PostToolUse chạy công cụ định dạng trên file vừa sửa
«Không push với force»PreToolUse trên Bash chặn lệnh, exit 2
«Không nói xong khi test còn đỏ»Stop chạy test và không cho kết thúc
«Không sửa .env»PreToolUse trên Edit|Write kiểm tra đường dẫn
«Nhớ mình đã dừng ở đâu»SessionStart in kế hoạch và git status

Lỗi thường gặp

  • Mã 1 thay vì 2. Script thoát với mã 1, agent không biết gì và hành động vẫn được thực hiện. Chỉ mã 2 mới chặn.
  • Thông báo ra stdout. Khi mã là 2, agent nhận stderr. Hãy ghi bằng >&2.
  • Hook chậm ở sự kiện xảy ra thường xuyên. Chạy toàn bộ test trong PostToolUse khiến mỗi lần sửa mất cả phút. Việc nặng để cho Stop.
  • Stop vô hạn. Không có kiểm tra stop_hook_active hoặc giới hạn — agent quay vòng mãi.
  • Đường dẫn tương đối. Hook chạy trong thư mục làm việc của phiên; hãy dùng $CLAUDE_PROJECT_DIR.
  • Hook của người khác chưa kiểm tra. Hook chạy với quyền của bạn và không hỏi xác nhận. Hãy đọc script trong repository của người khác trước khi tin tưởng.

Tổng kết

Hooks của Claude Code chuyển những quy tắc quan trọng nhất từ văn bản sang mã: PreToolUse chặn hành động nguy hiểm, PostToolUse kiểm tra mỗi lần sửa, Stop không cho báo sẵn sàng quá sớm, SessionStart trả lại cho agent trí nhớ về dự án. Hãy bắt đầu với hai đến ba hook cho những quy tắc mà agent đã từng vi phạm, và giữ thông báo trong stderr rõ ràng — khi đó agent tự sửa lỗi, còn bạn kiểm tra kết quả chứ không phải quá trình. Cách xác thực, kiểm tra độ sẵn sàng và vòng lặp tự trị được chúng tôi trình bày thực hành trong khóa học.

Câu hỏi thường gặp

Có cần khởi động lại Claude Code sau khi thay đổi hooks không?

Thay đổi trong settings.json được áp dụng chắc chắn nhất trong phiên mới; cấu hình hook hiện tại có thể xem bằng lệnh /hooks.

Hook có bắt buộc viết bằng bash không?

Không. Hook là một lệnh bất kỳ: script Python, Node.js hoặc file nhị phân. Điều quan trọng là đọc JSON từ stdin và trả về đúng mã thoát.

Agent có thấy đầu ra của hook thành công không?

Thường là không: với mã 0, đầu ra không đi vào ngữ cảnh. Ngoại lệ là UserPromptSubmit và SessionStart, nơi stdout được thêm vào ngữ cảnh của agent.

Hook có áp dụng cho subagent không?

Có. Các lần gọi công cụ của subagent đi qua cùng hook PreToolUse và PostToolUse, còn khi subagent kết thúc có sự kiện riêng là SubagentStop.

Có an toàn khi commit hooks vào repository không?

Có. .claude/settings.json và các script trong .claude/hooks/ là thực hành thông thường cho nhóm; hook cá nhân nên giữ trong settings.local.json hoặc ~/.claude/settings.json.