Skip to content

Thông tin kiểm chứng ngày 2026-10-24T00:00:00.000Z

Hook ​

Hook (móc tự động) là lệnh chạy ở một sự kiện trong phiên: trước khi agent dùng tool, khi agent định dừng... Hook là cổng kiểm tra tất định: nó luôn chạy, model không bỏ qua được.

Giải quyết vấn đề gì ​

CLAUDE.md là lời dặn. Model đọc, hiểu, và phần lớn thời gian làm theo. Nhưng "phần lớn" là không đủ cho những thứ như:

  • Không bao giờ commit .env.
  • Không bao giờ báo "xong" khi test đang đỏ.

Hook biến lời dặn thành luật. Hook không hỏi model có muốn chạy không; harness (phần mềm chạy agent) gọi nó ở đúng sự kiện, đọc exit code, và hành động theo.

Exit code quyết định tất cả ​

Exit codeÝ nghĩa
0Thành công, cho qua
2Chặn. Nội dung stderr được đưa lại cho Claude để nó đọc lý do và tự sửa
Mã khác, kể cả 1Lỗi không chặn. Hành động vẫn tiếp tục

Exit 1 không chặn

Lỗi hay gặp nhất khi viết hook: dùng process.exit(1) vì quen tay. Hook chạy, báo lỗi, rồi agent vẫn làm tiếp như không có gì. Muốn chặn thì phải là exit 2.

Loại handler: command, http, mcp_tool, prompt, agent (experimental). Starter chỉ dùng command.

Khi nào cần ​

  • Có một quy tắc mà vi phạm một lần cũng đắt (lộ bí mật, xóa dữ liệu, đẩy lên nhánh chính).
  • Muốn agent tự kiểm tra trước khi dừng, đặc biệt khi chạy dài không ai nhìn (nấc 3, nấc 4).
  • Thấy mình dặn trong CLAUDE.md mà agent vẫn vi phạm.

Hai sự kiện đủ dùng cho người mới:

  • PreToolUse: chạy trước khi agent dùng một tool. Input có tool_name, tool_input (với Bash: tool_input.command), cwd, session_id.
  • Stop: chạy khi agent định kết thúc lượt. Exit 2 thì agent phải làm tiếp.

Ví dụ tối thiểu ​

Starter viết hook bằng TypeScript, chạy bằng bun. Lý do: một file chạy được trên Windows, macOS, Linux như nhau, không phụ thuộc bash hay PowerShell. Khai báo ở practice/starter/.claude/settings.json, dạng exec command + args:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bun",
            "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/block-secrets.ts"]
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bun",
            "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check-on-stop.ts"],
            "timeout": 300
          }
        ]
      }
    ]
  }
}

CLAUDE_PROJECT_DIR là gốc dự án, nên hook chạy đúng dù agent đang đứng ở thư mục con. timeout: 300 cho Stop hook đủ thời gian chạy typecheck và test.

PreToolUse: chặn đưa bí mật vào git ​

practice/starter/.claude/hooks/block-secrets.ts, phần lõi:

ts
// Exit 2 + lý do ở stderr = chặn lệnh, Claude đọc lý do và tự sửa. Exit 0 = cho qua.

function block(reason: string): never {
  console.error(`Đã chặn: ${reason} Hãy bỏ file đó khỏi git (git restore --staged <file>) và kiểm tra .gitignore.`);
  process.exit(2);
}

const input = await Bun.stdin.json().catch(() => ({}));
const command: string = input?.tool_input?.command ?? "";

if (isSecretCommit(command)) block(`lệnh "${command}" đưa file bí mật (.env, .pem, .key...) vào git.`);

Hook đọc JSON từ stdin, lấy tool_input.command, và nếu lệnh là git add/git commit có nhắc tới .env, .pem, .key... thì exit 2. Với git commit không nhắc tên file (sau git add -A), hook còn kiểm tra những gì đang thật sự nằm trong index. Đã chạy thử: lệnh git add apps/server/.env bị chặn.

Stop: không cho báo "xong" khi test đỏ ​

practice/starter/.claude/hooks/check-on-stop.ts, phần lõi:

ts
// Rào chắn tất định: model không thể "quên" bước kiểm tra này.
const input = await Bun.stdin.json().catch(() => ({}));

// Đang ở vòng được hook nhắc lại rồi: cho dừng để không lặp vô hạn khi test không thể pass.
if (input?.stop_hook_active) process.exit(0);

// ... chạy `bun run check-types` và `bun test` ...

if (failures.length) {
  console.error(`Chưa xong: kiểm tra tự động còn lỗi. Sửa rồi mới báo hoàn thành.\n\n${failures.join("\n\n")}`);
  process.exit(2);
}

Hai điểm phải nhớ với Stop hook:

  • stop_hook_active là true khi Claude đang tiếp tục vì một stop hook trước đó. Không kiểm tra nó thì một test không thể pass sẽ giữ agent lặp mãi.
  • Claude Code có giới hạn riêng: tối đa 8 lần tiếp tục liên tiếp do Stop hook (bộ đếm reset khi Claude gọi tool), nâng được bằng CLAUDE_CODE_STOP_HOOK_BLOCK_CAP. Đừng dựa vào giới hạn này; tự kiểm tra stop_hook_active.
  • Đừng bỏ qua chỉ vì "không có gì chưa commit". Quy trình là commit xong mới dừng, nên lúc dừng cây thường sạch, và code đỏ đã commit sẽ lọt qua. Hook của starter chỉ bỏ qua khi cây sạch và commit hiện tại đã được kiểm pass lần trước (mốc lưu trong thư mục .git).

Hook của starter có test riêng (practice/starter/.claude/hooks/hooks.test.ts). Hook là code; nó cũng cần test.

Bên Codex ​

Codex có hooks, đi kèm trong plugin hoặc cấu hình. Ý tưởng giống nhau: chạy lệnh ở một sự kiện để chặn hoặc kiểm tra. Xem chương 7 để đối chiếu chi tiết.

Hook là một phần của bức tranh rào chắn lớn hơn (test, quyền, giới hạn vòng lặp, review diff), xem chương 6.

Nguồn ​