· claude-code / hooks / automation

Claude Code Hooks: Cẩm nang cho người dùng nâng cao

Claude Code Hooks cho phép gắn shell commands vào 29 lifecycle events — auto-format, chặn commit sai, ping Slack. Năm công thức thực chiến kèm lưu ý quan trọng.

Bởi Ethan

2.512 từ · 13 phút đọc

Hooks là câu trả lời đúng mỗi khi bạn thấy mình đang ngồi canh Claude và làm gì đó sau khi nó xong việc. Tự động format file vừa chỉnh. Chặn commit nếu test đang đỏ. Ghi lại token usage mỗi session. Ping điện thoại khi tác vụ dài kết thúc. Tất cả chỉ cần vài dòng shell và một entry trong settings.json.

Guide này giả định bạn đã dùng Claude Code hàng ngày và muốn ngừng phải canh nó. Nếu bạn vẫn đang cân nhắc xem Claude Code có đáng dùng không, hãy bắt đầu với so sánh Claude Code vs Codex.

Tại sao hooks ra đời

Trước khi có hooks, cách duy nhất để chạy gì đó sau khi Claude chạm vào một file là hoặc theo dõi output của Claude rồi phản ứng thủ công, hoặc dùng filesystem watches bên ngoài chạy độc lập với luồng thực thi thực sự của Claude. Cả hai cách đều dễ vỡ.

Hooks giải quyết ba vấn đề cụ thể:

  1. Audit trail. Bạn muốn có bản ghi của mọi tool call, chi phí token, hay ranh giới session — và bạn muốn nó xảy ra đáng tin cậy, không chỉ khi bạn nhớ kiểm tra transcript.
  2. Tự động dọn dẹp. Claude viết logic đúng nhưng không phải lúc nào cũng format theo style của dự án. Một PostToolUse hook trên các lệnh Edit|Write sẽ gọi formatter trên mọi file Claude chạm vào, giúp bạn không bao giờ phải review PR với indentation lộn xộn.
  3. Guardrails được thực thi. Có thể ra lệnh cho Claude không làm một số thứ, nhưng instructions có thể bị drift. Một PreToolUse hook chặn các pattern shell nguy hiểm và trả về thông báo lỗi được runtime thực thi — không phụ thuộc vào phán đoán của Claude.

Bảng event hiện tại bao gồm 29 lifecycle events.

Cấu trúc một hook

Các lifecycle events

Tất cả 29 events được nhóm vào năm nhóm timing:

CadenceEvents
Một lần mỗi sessionSessionStart, SessionEnd
Một lần mỗi turnUserPromptSubmit, Stop, StopFailure
Mỗi tool callPreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied
Khi batch hoàn tấtPostToolBatch
Các system eventsWorktreeCreate, WorktreeRemove, PreCompact, PostCompact, ConfigChange, CwdChanged, FileChanged, InstructionsLoaded, Notification, SubagentStart, SubagentStop, TaskCreated, TaskCompleted, UserPromptExpansion, Setup, TeammateIdle, Elicitation, ElicitationResult

Ba events quan trọng nhất trong công việc hàng ngày: PreToolUse (chặn trước khi thực thi), PostToolUse (phản ứng sau), và Stop (phản ứng khi Claude hoàn tất một turn).

PostToolUse không thể hoàn tác những gì tool đã làm. Nếu bạn cần ngăn một hành động, hãy dùng PreToolUse — đây là điểm duy nhất mà việc chặn có hiệu lực.

Vị trí config file

FilePhạm viCommit vào repo?
~/.claude/settings.jsonTất cả dự án của bạnKhông
.claude/settings.jsonDự án này
.claude/settings.local.jsonDự án nàyKhông (gitignored)

Cấu trúc chuẩn bên trong bất kỳ file nào trong số đó:

{
  "hooks": {
    "EventName": [
      {
        "matcher": "ToolName|OtherTool",
        "hooks": [
          {
            "type": "command",
            "command": "path/to/script.sh",
            "timeout": 60,
            "async": false
          }
        ]
      }
    ]
  }
}

matcher là danh sách tên tool phân tách bằng pipe — khớp chính xác, phân biệt hoa thường. matcher: "edit" sẽ không bao giờ kích hoạt; phải là "Edit".

Dữ liệu mỗi hook nhận qua stdin

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../session.jsonl",
  "cwd": "/current/working/dir",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "effort": { "level": "medium" }
}

PreToolUsePostToolUse còn có thêm tool_nametool_input trong payload.

Exit codes và response format

Exit codeÝ nghĩa
0Thành công; stdout được parse dạng JSON
2Chặn cứng; stderr được gửi đến Claude như thông báo lỗi
KhácCảnh báo không chặn; thực thi tiếp tục

Khi exit 0, bạn có thể trả về một JSON object từ stdout để ảnh hưởng đến hành vi của Claude:

{
  "continue": true,
  "decision": "block",
  "reason": "Lý do hành động bị chặn",
  "systemMessage": "Cảnh báo hiển thị trong transcript"
}

Các loại handler

Hiện có năm loại:

  • command — shell command; đây là loại bạn sẽ dùng 90% thời gian
  • http — POST đến một URL; response body được parse dạng JSON như trên
  • mcp_tool — gọi một tool trên MCP server đã kết nối
  • prompt — single-turn LLM eval trả về {"ok": true/false, "reason": "..."}
  • agent — subagent với quyền truy cập tool, timeout 60 giây, 50 tool turns (experimental)

Hãy dùng command trừ khi bạn có lý do cụ thể để chọn loại khác. agent hooks có vẻ mạnh trên lý thuyết; thực tế thì giới hạn 60 giây khiến chúng dễ thất bại với bất kỳ thứ gì không phải kiểm tra nhanh.

Năm công thức hoạt động thực tế

1. Tự động format sau mỗi lần chỉnh sửa

Thêm vào .claude/settings.json (project-scoped, commit vào repo):

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

Yêu cầu jqnpx prettier. Thay bằng black, gofmt -w, hoặc rustfmt cho các ngôn ngữ khác. Hook chạy sau khi file được lưu; Claude không thấy nội dung đã được reformat trừ khi nó đọc lại file.

Nguồn: hướng dẫn hooks chính thức — phần auto-format.

2. Git-commit guard (chặn nếu test thất bại)

Lưu thành .claude/hooks/guard-commit.sh, rồi chmod +x:

#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')

if ! echo "$COMMAND" | grep -qE '^git commit'; then
  exit 0
fi

echo "Running tests before commit..." >&2

if ! npm test --silent 2>&1; then
  echo "Tests failed — commit blocked. Fix failures before committing." >&2
  exit 2
fi

exit 0

Đăng ký trong .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git commit*)",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-commit.sh"
          }
        ]
      }
    ]
  }
}

Trường if (từ v2.1.85+) lọc hook để nó chỉ kích hoạt khi lệnh Bash bắt đầu bằng git commit, giảm overhead cho các lệnh shell khác. exit 2 gửi thông báo lỗi đến Claude, lúc đó Claude sẽ cố fix test thất bại trước khi thử lại.

Dùng $CLAUDE_PROJECT_DIR thay vì relative paths — hooks chạy với non-interactive shell và working directory không được đảm bảo. Thay npm test --silent bằng pytest -q, go test ./..., hoặc cargo test tùy ngôn ngữ.

3. Theo dõi token usage

Claude Code lưu mỗi session dưới dạng JSONL file tại transcript_path. Các dòng có message.usage chứa input_tokens, output_tokens, cache_creation_input_tokens, và cache_read_input_tokens. Hook Stop có thể đọc những dữ liệu đó và ghi thêm vào log.

Lưu thành ~/.claude/hooks/cost-tracker.sh, rồi chmod +x:

#!/bin/bash
INPUT=$(cat)
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path')
LOG_FILE="$HOME/.claude/cost-log.jsonl"

[ -f "$TRANSCRIPT" ] || exit 0

TOTALS=$(jq -c '
  select(.message.usage != null)
  | select(.isSidechain != true)
  | select(.isApiErrorMessage != true)
  | .message.usage
' "$TRANSCRIPT" 2>/dev/null | jq -sc '
  {
    input_tokens: (map(.input_tokens // 0) | add),
    output_tokens: (map(.output_tokens // 0) | add),
    cache_write_tokens: (map(.cache_creation_input_tokens // 0) | add),
    cache_read_tokens: (map(.cache_read_input_tokens // 0) | add)
  }
')

echo "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"session_id\":\"$SESSION_ID\",\"usage\":$TOTALS}" >> "$LOG_FILE"

exit 0

Đăng ký trong ~/.claude/settings.json:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$HOME\"/.claude/hooks/cost-tracker.sh",
            "async": true
          }
        ]
      }
    ]
  }
}

async: true nghĩa là Claude dừng ngay lập tức mà không chờ ghi log. Anthropic không cung cấp số liệu chi phí trực tiếp trong hook payloads — không có trường usage_data hay cost trong bất kỳ event nào — nên log này cho bạn token counts thô để tự nhân với giá hiện tại. Các trường được dùng ở trên (session_id, message.usage, isSidechain, isApiErrorMessage) được quan sát từ các JSONL transcript files mà Claude Code ghi ra đĩa.

4. Ping Slack khi dừng

Lưu thành ~/.claude/hooks/notify-stop.sh, rồi chmod +x:

#!/bin/bash
INPUT=$(cat)
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
CWD=$(echo "$INPUT" | jq -r '.cwd')
PROJECT=$(basename "$CWD")

[ -z "$SLACK_WEBHOOK_URL" ] && exit 0

PAYLOAD=$(jq -n \
  --arg text "Claude Code finished in *$PROJECT*" \
  --arg session "$SESSION_ID" \
  --arg cwd "$CWD" \
  '{
    text: $text,
    blocks: [
      { type: "section", text: { type: "mrkdwn", text: $text } },
      { type: "context", elements: [
        { type: "mrkdwn", text: ("Session: `" + $session + "`") },
        { type: "mrkdwn", text: ("Dir: `" + $cwd + "`") }
      ]}
    ]
  }')

curl -s -X POST -H 'Content-type: application/json' \
  --data "$PAYLOAD" "$SLACK_WEBHOOK_URL" >/dev/null 2>&1

exit 0

Dùng cấu hình hook Stop tương tự recipe 3, với async: true để Claude không bị chặn bởi network call. Đặt SLACK_WEBHOOK_URL trong shell profile, không phải trong settings.json. Với macOS desktop notifications không cần Slack, docs chính thức đưa ra ví dụ osascript -e 'display notification "Claude done" with title "Claude Code"' — không cần dependency ngoài.

Nếu bạn dùng Warp, hooks có thể emit OSC 9 sequences qua trường terminalSequence (từ v2.1.141+) để kích hoạt desktop notification native của Warp mà không cần gọi curl.

5. Quét từ khóa nguy hiểm (chặn cứng)

Được điều chỉnh từ ví dụ bash validator chính thức.

Lưu thành .claude/hooks/danger-scanner.sh, rồi chmod +x:

#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')

[ "$TOOL" != "Bash" ] && exit 0

COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
[ -z "$COMMAND" ] && exit 0

BLOCKED_PATTERNS=(
  "rm -rf /"
  "rm -rf ~"
  "rm -rf \$HOME"
  "> /dev/sd"
  "dd if=.* of=/dev/sd"
  "DROP DATABASE"
  "DROP TABLE"
  ":(){ :|:& };:"
)

for pattern in "${BLOCKED_PATTERNS[@]}"; do
  if echo "$COMMAND" | grep -qiE "$pattern"; then
    echo "BLOCKED: Potentially destructive pattern detected: '$pattern'" >&2
    exit 2
  fi
done

exit 0

Đăng ký trong .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/danger-scanner.sh"
          }
        ]
      }
    ]
  }
}

Viết bằng shell thuần + jq. Python cold start tốn 200–400ms cho mỗi Bash call; shell script chạy dưới 5ms. Trên các refactor lớn khi Claude gọi hàng chục Bash calls mỗi turn, sự chênh lệch 200ms đó tích lũy đáng kể.

Kết hợp nhiều hooks

Thứ tự và song song

Nhiều hooks dưới một event chạy song song (với async) hoặc theo thứ tự định nghĩa (với synchronous). Nếu hai PreToolUse hooks đều trả về updatedInput để viết lại tool arguments, hook nào kết thúc sau sẽ thắng — thứ tự không xác định. Tránh có nhiều hơn một hook chỉnh sửa input của cùng một tool.

Biến môi trường

Claude Code đặt $CLAUDE_PROJECT_DIR (project root) trước khi spawn hooks. Các biến môi trường của bạn — bao gồm secrets — đều có sẵn nếu được đặt trước khi Claude Code khởi động. Đặt webhook URLs và API keys trong shell profile, không phải trong .claude/settings.json, để chúng không lọt vào version control.

Idempotency

PostToolUse trên Edit|Write kích hoạt mỗi khi Claude chạm vào file. Viết hooks sao cho idempotent: formatter chạy hai lần trên cùng một file không sao; append-to-log mỗi lần gọi thì không ổn. Dùng Stop cho các aggregate theo session.

Những điểm cần chú ý

Shell profile làm nhiễu output

Khi Claude Code spawn một command hook, nó chạy sh -c. Nếu ~/.bashrc hay ~/.zshrc của bạn in gì đó vô điều kiện, output đó sẽ được prepend vào stdout của hook và phá vỡ JSON parsing:

Shell ready on arm64
{"decision": "block", "reason": "Not allowed"}

Claude Code thấy “Shell ready on arm64{…” và không parse được. Hãy guard output của profile với kiểm tra interactive-shell:

if [[ $- == *i* ]]; then echo "Shell ready"; fi

Đây là bug production phổ biến nhất khi hooks “bỗng dưng ngừng hoạt động.”

Vòng lặp vô tận của Stop hook

Một Stop hook báo Claude tiếp tục sẽ kích hoạt lại Stop, hook lại chạy. Docs chính thức cung cấp trường stop_hook_active trong Stop hook payload chính xác cho trường hợp này:

INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0  # Đã được yêu cầu tiếp tục một lần rồi; để nó dừng
fi

Không có guard này, bất kỳ task-completion checker nào cũng sẽ chạy vô tận.

Matchers phân biệt hoa thường

matcher: "edit" sẽ không bao giờ kích hoạt. Tên tool là PascalCase: Edit, Write, Bash, Read. MCP tool matchers theo dạng mcp__<server>__<tool>.

Giá trị timeout mặc định

Loại hookTimeout mặc địnhOverride cho UserPromptSubmit
command, http, mcp_tool10 phút30 giây
prompt30 giây
agent60 giây

Override bằng "timeout": N (tính bằng giây) trong từng hook definition. UserPromptSubmit hooks luôn được tối đa 30 giây, ngay cả với loại command — chúng chặn response của Claude đến người dùng.

Bảo mật: project hooks là bề mặt tấn công

Hooks trong .claude/settings.json thực thi các shell commands tùy ý với quyền user của bạn. CVE-2025-59536 và CVE-2026-21852 đã chứng minh rằng các file settings.json độc hại trong repo được clone có thể chạy arbitrary code theo cách này. Anthropic đã vá lỗ hổng auto-trust bypass, nhưng mô hình cơ bản không thay đổi: không bao giờ git clone && cd vào một repo không tin cậy trong khi Claude Code đang chạy. Nguồn: CheckPoint Research advisory.

PermissionRequest bị bỏ qua ở headless mode

PermissionRequest không kích hoạt ở chế độ -p (non-interactive). Dùng PreToolUse cho các quyết định permission trong CI hoặc scripts.

Hooks vs. CLAUDE.md

Khi nào nên dùng hook, khi nào nên thêm instruction vào CLAUDE.md?

Dùng CLAUDE.md cho ý định: “luôn viết test trước implementation,” “ưu tiên functional patterns,” “không import lodash.” Đây là các preference mà Claude có thể lập luận và điều chỉnh theo ngữ cảnh.

Dùng hooks để thực thi: formatting, commit guards, cost logging, security scanners. Nếu hậu quả của việc Claude bỏ qua instruction tệ hơn style hơi sai một chút, hãy biến nó thành hook. Instructions trong CLAUDE.md không được runtime thực thi; hooks thì có.

Một pattern hữu ích: đặt giải thích dễ đọc vào CLAUDE.md (“chúng tôi chặn các shell commands nguy hiểm qua một PreToolUse hook”) và việc thực thi thực sự vào hook. Claude biết lý do tại sao nó bị chặn, không chỉ là bị chặn.

Nếu bạn cần đóng gói cả một quy trình thành instruction set tái sử dụng được, hãy xem cách viết Claude Code skill — skills bổ sung cho CLAUDE.md và hooks bằng cách dạy Claude các quy trình nhiều bước theo yêu cầu.


Nguồn: hooks reference · hooks guide · ví dụ bash validator chính thức · security advisory CVE-2025-59536