· 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ể:
- 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.
- 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
PostToolUsehook trên các lệnhEdit|Writesẽ 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. - 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
PreToolUsehook 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:
| Cadence | Events |
|---|---|
| Một lần mỗi session | SessionStart, SessionEnd |
| Một lần mỗi turn | UserPromptSubmit, Stop, StopFailure |
| Mỗi tool call | PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied |
| Khi batch hoàn tất | PostToolBatch |
| Các system events | WorktreeCreate, 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
| File | Phạm vi | Commit vào repo? |
|---|---|---|
~/.claude/settings.json | Tất cả dự án của bạn | Không |
.claude/settings.json | Dự án này | Có |
.claude/settings.local.json | Dự án này | Khô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" }
}
PreToolUse và PostToolUse còn có thêm tool_name và tool_input trong payload.
Exit codes và response format
| Exit code | Ý nghĩa |
|---|---|
0 | Thành công; stdout được parse dạng JSON |
2 | Chặn cứng; stderr được gửi đến Claude như thông báo lỗi |
| Khác | Cả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 gianhttp— POST đến một URL; response body được parse dạng JSON như trênmcp_tool— gọi một tool trên MCP server đã kết nốiprompt— 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 jq và npx 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 hook | Timeout mặc định | Override cho UserPromptSubmit |
|---|---|---|
command, http, mcp_tool | 10 phút | 30 giây |
prompt | 30 giây | — |
agent | 60 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