Mục tiêu học tập

Sau bài này, bạn sẽ có thể:
  • ✅ Giải thích hook là gì và tại sao nó deterministic (tất định) trong khi CLAUDE.md và skill thì probabilistic (xác suất)
  • ✅ Nắm rõ 5 lifecycle events: PreToolUse, PostToolUse, UserPromptSubmit, Stop, Notification — và khi nào dùng cái nào
  • ✅ Cấu hình hooks đúng cách trong settings.json với matcher, command, và script
  • ✅ Dùng exit codes (0 / 2) để allow hoặc block tool call từ PreToolUse hook
  • ✅ Share hooks với cả team bằng cách commit settings.json và script vào repo

Mở đầu: “Claude quên” không còn là lý do hợp lý nữa

Một dev backend ở startup fintech đã viết vào CLAUDE.md:
“Sau mỗi lần edit file TypeScript, hãy chạy Prettier để format code.”
Đơn giản, rõ ràng. Trong 2 tuần đầu, Claude làm đúng khoảng 80% thời gian. Đủ tốt để bỏ qua. Cho đến buổi chiều thứ Sáu trước ngày release. CI pipeline báo đỏ: 47 file lỗi format. Dev phải ngồi lại chạy Prettier tay toàn bộ thư mục src/. Mất 40 phút. Sprint bị trễ. Vấn đề không phải Claude “lười”. Vấn đề là CLAUDE.md là probabilistic — model đọc rồi quyết định có thực hiện hay không, dựa trên ngữ cảnh hiện tại, độ dài cuộc hội thoại, và hàng chục yếu tố khác. 80% tốt là một con số tốt trong ML. Nhưng trong production workflow, 80% nghĩa là 1 trong 5 lần bạn sẽ bị bất ngờ. Hook giải quyết chính xác vấn đề này. Khi bạn đặt logic vào hook, Claude không còn là người quyết định nữa. Hệ thống Claude Code chạy script của bạn tại event được chỉ định — mà không cần model “nhớ” hay “quyết định”. 100% mỗi lần, không ngoại lệ.
“If something needs to happen every time without fail, don’t put it in a prompt. Put it in a hook.” — Hooks documentation, Anthropic
Đây là bài học bạn sẽ nhớ mãi sau khi đọc xong bài này.

Hook là gì?

Hook là một script chạy tự động tại các điểm cụ thể trong lifecycle của Claude Code. Bạn cấu hình hook trong settings.json — khai báo: event nào, tool nào (matcher), và command nào cần chạy. Điểm cốt lõi phân biệt hook với mọi cơ chế khác: Hook không phụ thuộc vào “ký ức” hay “quyết định” của model. Đó là lý do duy nhất khiến nó deterministic.

Lifecycle của Claude Code — nơi hooks can thiệp

5 Hook Events

Lưu ý: Matcher là regex match trên tên tool (Edit, Bash, Write, v.v.). PreToolUse và PostToolUse thường cần matcher để tránh hook chạy trên mọi tool call.

Anatomy của hook config

Hook được cấu hình trong .claude/settings.json (project-level, có thể commit vào repo) hoặc ~/.claude/settings.json (user-level, áp dụng mọi project).

Cấu trúc JSON

Giải thích từng thành phần

  • Event key (PostToolUse, PreToolUse, v.v.) — lifecycle event bạn muốn hook vào
  • matcher — regex string match với tên tool. "Edit|MultiEdit|Write" nghĩa là hook chỉ chạy khi Claude dùng một trong 3 tools này. Để trống "" nghĩa là match mọi tool call
  • type — hiện tại chỉ có "command" (chạy shell command)
  • command — lệnh shell hoặc đường dẫn tới script. Dùng $CLAUDE_PROJECT_DIR thay vì hardcode absolute path

Script nhận gì từ Claude Code?

Script của bạn nhận JSON qua stdin chứa thông tin về event:
Script output ra stdout/stderr, và exit code là tín hiệu quan trọng nhất.

Exit Codes và Behavior

Quan trọng: Khi PreToolUse hook exit với code 2, Claude nhận được stderr message như một “giải thích tại sao bị từ chối”. Claude sẽ tự điều chỉnh — ví dụ thay đổi approach hoặc hỏi lại bạn. Đây là cơ chế feedback loop thông minh.

Ví dụ thực chiến: Auto-format hook

Đây là hook phổ biến nhất — auto-format file sau mỗi lần Claude edit.

Bước 1: Tạo script

Tạo file .claude/hooks/format-on-edit.sh:

Bước 2: Cấp quyền thực thi

Bước 3: Thêm vào settings.json

Bước 4: Commit vào repo

Toàn bộ team clone repo về sẽ tự động có hook này.

Bước 5: Test

Claude edit file. Ngay sau khi edit xong, hook chạy Prettier. File được format trước khi Claude tiếp tục bất kỳ bước nào. Không cần nhắc, không có 20% fail rate.

Blocking với PreToolUse

PreToolUse hook là cơ chế enforce “hard rules” — những điều phải được đảm bảo, không phải chỉ “đề xuất”.

Script nhận gì?

Ví dụ: Block lệnh Bash nguy hiểm

Tạo .claude/hooks/block-dangerous-commands.sh:
Config trong settings.json:
Khi Claude cố chạy rm -rf /tmp/cache:

Hook vs Skill vs CLAUDE.md vs MCP

Đây là bảng so sánh quan trọng nhất trong bài — quyết định khi nào dùng cơ chế nào: Quy tắc quyết định đơn giản:
  • Nếu bạn cần đảm bảo điều gì đó xảy ra → Hook
  • Nếu bạn muốn Claude biết cách làm gì đó khi bạn hỏi → Skill
  • Nếu bạn muốn Claude luôn nhớ context chung của project → CLAUDE.md
  • Nếu bạn cần Claude truy cập hệ thống ngoài → MCP
Cross-reference: xem thêm Bài 2.9 (Skills) và Bài 2.7 (CLAUDE.md) để so sánh chi tiết hơn.

Case studies theo role

Backend Engineer: Lint guarantee

Mỗi lần Claude edit bất kỳ file .ts, PostToolUse hook auto-chạy eslint --fix. Không bao giờ có CI lint failure nữa — dù session dài 4 tiếng hay Claude đang cuối context.

DevOps / Security Engineer: Prod file protection

PreToolUse hook block mọi write vào infra/prod/*. Claude chỉ được phép thao tác infra/staging/*. Block + stderr message rõ ràng giúp Claude tự chọn đường dẫn staging, không cần dev can thiệp.

Engineering Team: Async notification

Stop hook gửi Slack message khi Claude hoàn thành task. Team có thể giao Claude task dài (30+ phút), làm việc khác, nhận Slack ping khi xong. Không cần ngồi nhìn màn hình chờ.

Compliance team: Audit log bắt buộc

PreToolUse hook log mọi Bash command vào file audit với timestamp, user, command. Không có exception. Đây là regulatory requirement mà hook đảm bảo 100% — không thể “quên” như khi viết trong CLAUDE.md.

Open Source Maintainer: Test trước commit

PostToolUse hook chạy test suite sau mỗi lần Claude edit file source (không phải test file). Nếu test fail, script log ra stderr — Claude biết có regression và tự fix trước khi tiếp tục.

Solo founder: macOS notification

Notification hook chạy say “Claude xong rồi” (macOS text-to-speech). Bạn có thể rời bàn phím, về máy sẽ nghe thông báo khi Claude hoàn thành task background.

Anti-patterns

Script chạy quá lâu (>10 giây)

Hook là synchronous trong lifecycle. Nếu script mất 30 giây (ví dụ chạy full test suite mỗi edit), mỗi tool call của Claude sẽ bị block 30 giây. UX cực kỳ tệ. Giải pháp: chạy test nhanh, hoặc dùng background process (&) với Stop hook thay vì PostToolUse.

PreToolUse không xử lý input rỗng

Nếu tool call không có field bạn expect (ví dụ Bash không có command khi là subshell), script crash → false-positive block. Luôn handle gracefully: || echo "" và check empty trước khi process.

Hardcode absolute path trong command

CLAUDE_PROJECT_DIR là env var Claude Code inject vào khi chạy hook — luôn trỏ đến root của project hiện tại.

Không commit settings.json và script vào repo

Nếu chỉ có bạn có hook, team sẽ commit code không format, không lint. Mất đi toàn bộ lợi ích “deterministic for the whole team”. Hook chỉ thực sự mạnh khi toàn team có cùng config.

Hook chứa secret / API key

Script được commit vào repo. Nếu script hardcode SLACK_TOKEN=xoxb-..., bạn vừa leak secret. Dùng environment variable: $SLACK_WEBHOOK_URL và set trong môi trường local / CI secrets.

Dùng hook cho việc nên là Skill

Hook là cho guarantees. Nếu bạn viết hook để “nhắc Claude viết commit message đúng chuẩn”, đó là việc của Skill hoặc CLAUDE.md — không cần guarantee mỗi tool call. Hook không cần thiết = overhead vô nghĩa.

Matcher quá rộng (no matcher)

Matcher rỗng nghĩa là hook chạy mỗi lần Claude gọi bất kỳ tool nào. Với PostToolUse formatter, điều này vô nghĩa và chậm.

Mẹo nâng cao

Dùng CLAUDE_PROJECT_DIR cho mọi path reference

Env var này được inject tự động bởi Claude Code. Script của bạn work bất kể cwd của Claude đang ở đâu trong project.

Combine hooks: PreToolUse → PostToolUse → Stop pipeline

Ba hooks phối hợp tạo một pipeline hoàn chỉnh: validate trước, cleanup sau, notify khi xong.

Idempotent scripts — chạy nhiều lần không có side effect

Prettier, gofmt, ruff đều idempotent (chạy nhiều lần ra cùng kết quả). Tuy nhiên nếu script của bạn append vào file log, đảm bảo format đủ thông tin để avoid duplicate entries gây nhầm lẫn.

Test hook isolation trước khi enable

Chạy script trực tiếp với mock stdin trước khi enable trong settings.json. Dễ debug hơn nhiều so với enable rồi chạy Claude.

Conditional logic trong script (không chỉ dựa vào matcher)

Matcher chỉ match tên tool. Script có thể implement logic phức tạp hơn:

Debug: comment tạm matcher để isolate hook behavior

Khi debug Claude behavior, bạn nghi hook đang can thiệp. Cách nhanh nhất: đổi matcher thành một string không bao giờ match ("DISABLED_Edit|MultiEdit"). Claude behavior trở về “thuần” — so sánh để xác nhận hook là culprit.

Multiple matchers trong 1 entry

Một hook entry cover 3 tools. Thực tế với file editing, bạn muốn cover cả 3: Edit (single block), MultiEdit (nhiều blocks), Write (tạo file mới).

Áp dụng ngay

Bài tập 1: Auto-format hook (~25 phút)

Tạo PostToolUse hook auto-format cho project của bạn. Bước 1: Xác định formatter bạn đang dùng (Prettier, eslint, ruff, gofmt, v.v.) Bước 2: Tạo .claude/hooks/format-on-edit.sh (xem script ở phần trên, adapt cho stack của bạn) Bước 3: Thêm vào .claude/settings.json:
Bước 4: Test script isolation với mock input trước Bước 5: Nhờ Claude edit 5 file khác nhau. Verify mỗi lần đều format. Không có ngoại lệ. Bước 6: Commit cả script + settings vào repo

Bài tập 2: PreToolUse blocking hook (~20 phút)

Tạo hook block 1 dangerous pattern trong project của bạn. Chọn 1 trong các scenario:
  • Block rm -rf trong Bash commands
  • Block write vào .env files
  • Block edit files trong infra/prod/
  • Block git push —force
Bước 1: Tạo .claude/hooks/block-dangerous.sh với logic check và exit 2 Bước 2: Test reject case: tạo mock input với command nguy hiểm, verify script exit 2 với đúng stderr message Bước 3: Test allow case: mock input với command bình thường, verify script exit 0 Bước 4: Enable trong settings.json, test với Claude thực tế Bonus: Thử nhờ Claude làm action bị block → xem Claude tự adjust như thế nào dựa vào stderr feedback

Tóm tắt

Hook là cơ chế duy nhất trong Claude Code mà hành vi là deterministic — không phụ thuộc vào model “nhớ” hay “quyết định”. Đây là công cụ để bạn enforce hard rules, không phải đề xuất. 5 takeaways cốt lõi:
  • Hook chạy tại lifecycle events — script thông thường, không có technology mới để học
  • PostToolUse cho guarantees sau action: format, lint, log, test — chạy mỗi lần không trừ
  • PreToolUse cho blocking: exit 2 + stderr message → Claude tự điều chỉnh
  • Stop / Notification cho async notifications — để làm việc khác, nhận ping khi Claude xong
  • Commit settings.json + script vào repo → toàn team có cùng hook, không ai bị “thiếu”
Quote đáng nhớ:
“If something needs to happen every time without fail, don’t put it in a prompt. Put it in a hook.”

Bài tiếp theo

Bạn đã nắm toàn bộ hệ sinh thái mở rộng của Claude Code: CLAUDE.md, Skills, MCP, và Hooks. Bài 2.12 sẽ là quiz tổng hợp — kiểm tra lại toàn bộ kiến thức từ Bài 2.1 đến 2.11, với câu hỏi thực tế về cách chọn đúng công cụ trong từng tình huống. Cross-reference: Bài 2.4 (hook trong EPCC workflow) — Bài 2.6 (hook auto-run lint trước commit) — Bài 2.7 (hook vs CLAUDE.md khi nào dùng cái nào) — Bài 2.9 (hook vs skill — deterministic vs probabilistic) ➡️ Bài tiếp theo: Bài 2.12 — Quiz tổng hợp Claude Code 101

Tài liệu tham khảo

Khoá học “Claude Code 101” — bản tiếng Việt v1.0Bản quyền 2026 Anthropic. Mọi quyền được bảo lưu.