Free

Hướng dẫn đầy đủ về Claude Code Hooks: Kiểm soát mọi hành động của Claude

Phân tích đầy đủ cơ chế Claude Code Hooks: năm loại sự kiện, kiểm soát mã thoát và bốn cấu hình thực tế để làm cho các hành động AI có thể kiểm toán, chặn và tự động hóa.


Mỗi lần Claude Code đọc file, viết code hoặc thực thi lệnh, một hệ thống sự kiện đang chạy trong nền. Hooks là giao diện để kết nối vào hệ thống đó — bạn có thể chèn logic của mình vào bất kỳ thời điểm nào: tự động chạy linter, ghi log, chặn các hành động nguy hiểm, hoặc kích hoạt bất kỳ lệnh shell nào.

Bài viết này trình bày đầy đủ cơ chế Hooks, cách cấu hình và sử dụng thực tế.


Hooks là gì

Hooks là các lệnh shell được cấu hình trong settings.json mà Claude Code tự động thực thi khi các sự kiện cụ thể xảy ra.

Phép so sánh trực tiếp nhất: git hooks. Git có thể kích hoạt script trước và sau các thao tác như commit, push. Claude Code Hooks hoạt động hoàn toàn tương tự — điểm khác biệt là các điểm kích hoạt là lời gọi công cụ của AI.

Tại sao điều này quan trọng?

Claude Code càng mạnh, bạn càng cần một lớp kiểm soát xác định. Hooks cung cấp:
- Đảm bảo thực thi không phụ thuộc vào prompt (Claude có thể bỏ qua hướng dẫn, nhưng hook luôn chạy)
- Bản ghi hoạt động có thể kiểm tra
- Kiểm tra chất lượng tự động


Năm loại Hook

Loại Kích hoạt Dùng phổ biến
PreToolUse Trước khi gọi công cụ Chặn thao tác nguy hiểm, ghi log ý định
PostToolUse Sau khi gọi công cụ Auto-lint, chạy test
PreCompact Trước khi nén context Lưu snapshot trạng thái hiện tại
Notification Khi Claude gửi thông báo Thông báo desktop, tin nhắn Slack
Stop Khi Claude hoàn thành phản hồi Tổng hợp log, kích hoạt luồng tiếp theo

Được dùng nhiều nhất là PreToolUsePostToolUse, để chặn và xử lý sau các lời gọi công cụ.


Định dạng cấu hình

Hooks được viết trong ~/.claude/settings.json (toàn cục) hoặc .claude/settings.json ở thư mục gốc dự án (cấp dự án):

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npm run lint --silent"
          }
        ]
      }
    ]
  }
}

Ba trường cốt lõi:

  • matcher: Regex khớp với tên công cụ, xác định hook này kích hoạt sau lời gọi công cụ nào. "Write|Edit" kích hoạt khi viết hoặc chỉnh sửa file. Để trống hoặc dùng ".*" để khớp tất cả công cụ.
  • type: Hiện chỉ có "command".
  • command: Bất kỳ lệnh shell nào.

Tham chiếu tên công cụ

Tên công cụ cần biết khi viết matcher:

Tên công cụ Thao tác
Write Viết file mới
Edit Chỉnh sửa file
Bash Thực thi lệnh shell
Read Đọc file
Glob Tìm kiếm file
Grep Tìm kiếm nội dung
TodoWrite Cập nhật danh sách việc cần làm

Môi trường thực thi Hook

Khi thực thi, dữ liệu được truyền dưới dạng JSON qua stdin:

{
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/path/to/file.rb",
    "content": "..."
  },
  "tool_response": "..."
}

PreToolUse nhận tool_input (đối số trước khi gọi). PostToolUse cũng nhận tool_response (giá trị trả về của công cụ).

Ví dụ hook với logic điều kiện:

#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path')

# Chỉ chạy rubocop trên file .rb
if [[ "$file" == *.rb ]]; then
  rubocop "$file" --autocorrect-all --no-color
fi

Ý nghĩa mã thoát

Mã thoát của hook kiểm soát hành vi tiếp theo của Claude Code:

Mã thoát Ý nghĩa
0 Thành công, tiếp tục thực thi
2 Chặn: hủy lời gọi công cụ hiện tại, phản hồi stderr cho Claude
Khác (khác 0) Ghi log lỗi, nhưng tiếp tục thực thi

Mã thoát 2 hữu ích nhất — cho phép viết logic chặn trong PreToolUse để ngăn Claude thực hiện một thao tác và giải thích lý do.


Thực chiến: Bốn cấu hình Hook

1. Tự động format sau khi viết file

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'file=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .file_path 2>/dev/null); [[ \"$file\" == *.rb ]] && rubocop -A \"$file\" --no-color -q || true'"
          }
        ]
      }
    ]
  }
}

2. Chặn xóa thư mục cụ thể

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'cmd=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .command); if echo \"$cmd\" | grep -qE \"rm.*/(migrations|seeds)\"; then echo \"Không được phép xóa thư mục migrations hoặc seeds\" >&2; exit 2; fi'"
          }
        ]
      }
    ]
  }
}

3. Log kiểm tra hoạt động

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|Bash",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"$(date '+%Y-%m-%d %H:%M:%S') $CLAUDE_TOOL_NAME: $(echo $CLAUDE_TOOL_INPUT | jq -c .)\" >> ~/.claude/audit.log"
          }
        ]
      }
    ]
  }
}

4. Thông báo desktop khi hoàn thành

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude đã hoàn thành\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

Toàn cục vs. Cấp dự án

Hooks có thể đặt ở hai nơi:

Toàn cục (~/.claude/settings.json): Quy tắc áp dụng cho tất cả dự án — log, thông báo.

Cấp dự án (.claude/settings.json): Kiểm tra đặc thù của dự án. Hooks cấp dự án và toàn cục được gộp lại và cùng chạy.

Với dự án nhóm, nên commit settings.json cấp dự án vào git để mọi người chạy cùng hooks.


Debug Hook

Các bước xử lý sự cố khi hook không hoạt động:

  1. Kiểm tra chính tả matcher: Tên công cụ phân biệt chữ hoa/thường — write khác Write
  2. Chạy lệnh riêng lẻ: Sao chép lệnh hook và chạy trực tiếp trong terminal
  3. Kiểm tra output Claude Code: Stderr của hook hiển thị trong hội thoại
  4. Đơn giản hóa để test: Bắt đầu với echo "hook triggered" >> /tmp/hook.log để xác nhận kích hoạt

Tổng kết

Giá trị cốt lõi của Hooks là kết nối hành vi AI không thể đoán trước với các tiêu chuẩn kỹ thuật xác định. Claude có thể quên chạy test, nhưng PostToolUse hook thì không. Claude có thể vô tình xóa file, nhưng bộ chặn PreToolUse sẽ không cho phép.

Bắt đầu với một hook: auto-lint sau khi viết file. Chạy ổn rồi mới thêm cái khác.