Bạn và team thống nhất: mọi TS file phải format Prettier trước commit. Bạn viết trong CLAUDE.md:
- Giải thích hook là gì và nó chèn vào luồng Claude Code ở đâu
- Phân biệt PreToolUse và PostToolUse hook, và khi nào dùng mỗi loại
- Nhận ra 3 vị trí lưu cấu hình hook (global / project shared / project local)
- Đọc hiểu structure JSON của một hook configuration
- Nhận diện 6 ứng dụng thực tế của hooks cho team
Mở đầu: "Claude tôi không chịu format file!"
Always run Prettier after editing TypeScript files.
Claude tuân... đôi khi. Quên 30% số lần. Bạn phải remind sau từng PR.
Vấn đề gốc: Claude là LLM, không deterministic. Dù viết rule rõ, vẫn skip.
Giải pháp: đừng dựa vào Claude tự nhớ. Làm action đó thành cấu hình máy bắt buộc — mỗi lần Claude Edit xong TS file, hệ thống tự chạy Prettier. Không hỏi Claude, không cần nhắc.
Đó là hooks.
Hook là gì?
Hook = command shell bạn định nghĩa, chạy tự động trước hoặc sau khi Claude gọi tool.
Không phải code Claude viết. Không phải prompt. Là hệ thống chạy.
Luồng bình thường (không hook)
┌──────────────────────────────────────────────────────────┐
│ │
│ Bạn gõ prompt │
│ │ │
│ ▼ │
│ Claude Code gửi prompt + tool definitions tới LM │
│ │ │
│ ▼ │
│ LM trả về: "Use tool Edit with file_path=..." │
│ │ │
│ ▼ │
│ Claude Code thực thi Edit trên disk │
│ │ │
│ ▼ │
│ Kết quả trả về LM tiếp tục │
│ │
└──────────────────────────────────────────────────────────┘
Luồng có hook
┌──────────────────────────────────────────────────────────┐
│ │
│ Bạn gõ prompt │
│ │ │
│ ▼ │
│ LM quyết định dùng tool Edit │
│ │ │
│ ▼ │
│ ┌──────────── PreToolUse HOOK ─────────────┐ │
│ │ Hook chạy trước khi Edit │ │
│ │ → Hook có thể BLOCK, cho phép, hoặc log │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ (nếu hook cho phép) │
│ Claude Code thực thi Edit │
│ │ │
│ ▼ │
│ ┌──────────── PostToolUse HOOK ────────────┐ │
│ │ Hook chạy sau khi Edit │ │
│ │ → Format file, chạy typecheck, lint... │ │
│ │ → Feedback quay lại Claude │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Kết quả + feedback trả LM tiếp tục │
│ │
└──────────────────────────────────────────────────────────┘
Hook chèn vào giữa — cung cấp cho bạn điểm can thiệp deterministic.
Hai loại hook căn bản
PreToolUse — Chặn trước
Chạy trước khi tool được execute. Có thể:
- Cho phép tool chạy bình thường (exit code 0)
- Block tool (exit code 2) + gửi error message quay lại Claude
Ứng dụng điển hình:
- Chặn Claude đọc file sensitive (
.env,secrets/) - Block command nguy hiểm (
rm -rf /, destructive migrations) - Enforce naming convention (Claude định tạo file với tên sai → block)
- Require confirmation cho tool nhất định
PostToolUse — Phản ứng sau
Chạy sau khi tool đã execute. Không thể block (đã xong). Nhưng có thể:
- Chạy follow-up action (format file vừa edit, chạy test)
- Cung cấp feedback về output Claude vừa làm
- Log / audit để review sau
Ứng dụng điển hình:
- Auto-format file sau Edit
- Run typecheck, push error quay lại Claude để fix
- Update changelog, audit log
- Trigger tests in watch mode
Vị trí cấu hình hook
Hook định nghĩa trong settings.json. Có 3 vị trí, tương tự CLAUDE.md:
┌──────────────────────────────────────────────────────────┐
│ │
│ 1. ~/.claude/settings.json │
│ → Global, áp dụng MỌI project │
│ → Dùng cho habit cá nhân │
│ │
│ 2. .claude/settings.json (trong project) │
│ → Commit Git, chia sẻ team │
│ → Rule team cần enforce │
│ │
│ 3. .claude/settings.local.json (trong project) │
│ → Gitignore — không commit │
│ → Preference cá nhân cho project này │
│ │
└──────────────────────────────────────────────────────────┘
Khi nào dùng từng vị trí?
| Rule | Đặt ở đâu | Lý do |
|---|---|---|
| Auto-format TS khi edit | project/.claude/settings.json | Team convention |
| Block đọc .env | project/.claude/settings.json | Team security |
| Run typecheck sau edit | project/.claude/settings.json | Team quality |
| Log mọi tool call của Claude vào file cá nhân để audit | .claude/settings.local.json | Debug cá nhân |
| Không cho Claude rm trong mọi project | ~/.claude/settings.json | Safety cá nhân |
Structure của hook config
Shape tổng quát
{
"hooks": {
"PreToolUse": [
{
"matcher": "<pattern>",
"hooks": [
{
"type": "command",
"command": "<shell command>"
}
]
}
],
"PostToolUse": [
{
"matcher": "<pattern>",
"hooks": [
{
"type": "command",
"command": "<shell command>"
}
]
}
]
}
}
Giải thích từng field
PreToolUse/PostToolUse: loại hookmatcher: pattern tên tool sẽ trigger hook. Hỗ trợ:- · Tên cụ thể:
"Edit"→ chỉ trigger khi tool Edit - · OR:
"Edit|Write|MultiEdit"→ bất kỳ tool edit - · Wildcard:
"*"→ mọi tool hooks[].type: hiện tại chỉ"command"hooks[].command: shell command Claude Code chạy
Ví dụ cấu hình 1: PreToolUse
Chặn Claude đọc file .env:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Grep",
"hooks": [
{
"type": "command",
"command": "node /absolute/path/to/hooks/block-env.js"
}
]
}
]
}
}
- Matcher
Read|Grep— cả 2 tool có thể đọc file - Command chạy một Node script
- Script check path file có chứa
.env→ exit 2 (block) hoặc exit 0 (allow)
Chi tiết implement ở Bài 4.14.
Ví dụ cấu hình 2: PostToolUse
Format file sau khi Claude edit TS:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "/usr/bin/env node /absolute/path/hooks/format-and-typecheck.js"
}
]
}
]
}
}
- Matcher: bất kỳ edit/write operation
- Script: chạy Prettier +
tsc --noEmit, nếu có error → feedback lại Claude
Dữ liệu hook nhận (stdin)
Khi hook chạy, Claude Code push JSON vào stdin của command. Command đọc, parse, xử lý.
Ví dụ JSON cho PreToolUse trên Read tool
{
"session_id": "2d6a1e4d-6abc-...",
"transcript_path": "/Users/john/.claude/transcripts/2d6a...",
"hook_event_name": "PreToolUse",
"tool_name": "Read",
"tool_input": {
"file_path": "/Users/john/Projects/app/.env"
}
}
Hook script có thể:
const input = JSON.parse(await readStdin());
if (input.tool_input.file_path.includes('.env')) {
console.error("Blocked: cannot read .env files");
process.exit(2); // 2 = BLOCK (PreToolUse only)
}
process.exit(0); // 0 = ALLOW
Chi tiết JSON shape cho mỗi hook event → Bài 4.13.
6 ứng dụng thực tế của hooks
1. Code formatting tự động
Edit → PostToolUse: prettier --write <file>
Claude edit xong, Prettier format. Code luôn consistent.
2. Type checking + feedback
Edit → PostToolUse: tsc --noEmit, nếu error → stderr
Nếu error, Claude nhận feedback, tự fix. Bạn không phải babysit.
3. Access control
Read/Grep → PreToolUse: block nếu path chứa pattern sensitive
Claude không bao giờ đọc được .env, /secrets/, etc.
4. Running tests on file changes
Edit → PostToolUse: nếu file X.ts → chạy X.test.ts
Test chạy ngay, fail sớm.
5. Linting / convention enforcement
Edit → PostToolUse: eslint, nếu warning → feedback
Claude auto-fix lint issue dựa trên feedback.
6. Logging / audit
Bash → PostToolUse: log mọi command vào audit.log
Review sau đó: Claude đã chạy gì, khi nào.
Ví dụ thực chiến: Team setup hook đầu tiên
Tình huống
Team 6 dev, stack TypeScript + React. Team muốn 2 rule enforce:
- Không ai (Claude) được đọc
.env - Mọi file TS edit xong phải pass
tsc --noEmit
Setup
Tạo project/.claude/hooks/ chứa script:
hooks/block-env.js:
#!/usr/bin/env node
const chunks = [];
for await (const chunk of process.stdin) chunks.push(chunk);
const input = JSON.parse(Buffer.concat(chunks).toString());
const path = input.tool_input?.file_path || input.tool_input?.path || '';
if (path.includes('.env')) {
console.error("Cannot access .env files. Use env vars at runtime.");
process.exit(2);
}
process.exit(0);
hooks/typecheck.js:
#!/usr/bin/env node
const { execSync } = require('child_process');
const chunks = [];
for await (const chunk of process.stdin) chunks.push(chunk);
const input = JSON.parse(Buffer.concat(chunks).toString());
const path = input.tool_input?.file_path || '';
if (!path.endsWith('.ts') && !path.endsWith('.tsx')) process.exit(0);
try {
execSync('npx tsc --noEmit', { stdio: 'pipe' });
process.exit(0);
} catch (err) {
console.error(err.stdout.toString());
process.exit(2);
}
.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Grep",
"hooks": [{ "type": "command", "command": "node $PWD/.claude/hooks/block-env.js" }]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [{ "type": "command", "command": "node $PWD/.claude/hooks/typecheck.js" }]
}
]
}
}
Note $PWD: Bài 4.15 sẽ giải thích tại sao dùng placeholder thay absolute path, và cách khi setup làm sao swap lại.
Kết quả
- Claude không bao giờ đọc
.envtrong 100+ session - Type error bắt sớm, không lọt vào commit
- PR quality cao hơn nhanh, review human giảm
Case studies theo ngành
Health tech — Enforce HIPAA boundary
Setup: PreToolUse hook block nếu Claude định write file ngoài phi-vault/ với PHI data pattern.
- Kết quả: Zero PHI leak event trong 6 tháng.
Fintech — Audit log bắt buộc
Setup: PostToolUse hook log mọi Bash/Edit với session ID + timestamp vào append-only log.
- Kết quả: SOC 2 audit pass clean, có evidence mọi change.
EdTech — Student code safety
Setup: PreToolUse block rm, sudo, curl cho student env.
- Kết quả: Zero incident system damage trong 500 student.
Content team — Style guide enforce
Setup: PostToolUse cho file .md: chạy style checker, flag "corporate AI" phrase.
- Kết quả: Output content consistency với brand voice 95%.
Platform — Cost control
Setup: PreToolUse block MCP call đến service đắt tiền nếu usage quota ngày đã vượt.
- Kết quả: AI cost predictable, không bill sốc cuối tháng.
Anti-patterns
Hook làm task chậm (>30s)
Biểu hiện: PostToolUse chạy full test suite sau mỗi edit → mỗi edit tốn 45 giây.
- Hậu quả
- Claude session chậm như rùa, UX tệ.
- Cách đúng
- Chỉ chạy test cho file đã edit, hoặc chạy test song song (background).
Hook lỗi silent
Biểu hiện: Hook crash nhưng exit 0 → Claude tưởng OK.
Cách đúng: Hook exit code chuẩn, log error rõ ràng.
Hook cần network call mỗi lần
Biểu hiện: PostToolUse hook upload mọi edit lên server → chậm + tốn bandwidth.
Cách đúng: Batch upload, or queue async.
Hook chặn false positive
Biểu hiện: Block mọi file có substring "env" → block luôn environment.ts (là public).
Cách đúng: Pattern precise: /\.env(\.|$)/ thay vì /env/.
Không test hook
Biểu hiện: viết hook, deploy team, hôm sau mọi người kêu Claude Code lag/crash.
Cách đúng: Test hook cá nhân 1-2 ngày trước khi push team.
Mẹo nâng cao
Mẹo 1: Debug hook bằng log-only hook
Muốn xem JSON Claude Code push vào stdin? Tạo hook log-only:
{
"hooks": {
"PostToolUse": [
{
"matcher": "*",
"hooks": [{ "type": "command", "command": "jq . > /tmp/claude-hook.log" }]
}
]
}
}
Trigger một tool call → xem /tmp/claude-hook.log. Từ đó biết structure để viết hook thật.
Chi tiết Bài 4.17.
Mẹo 2: Chia hook theo loại tool
Thay vì 1 hook monster, có nhiều hook nhỏ:
{
"hooks": {
"PostToolUse": [
{ "matcher": "Edit|Write", "hooks": [...] }, // format
{ "matcher": "Bash", "hooks": [...] }, // log
{ "matcher": "Edit", "hooks": [...] } // typecheck
]
}
}
Modularity, dễ maintain.
Mẹo 3: Hook trong Docker (isolation)
Hook chạy trong container → an toàn hơn, consistent giữa dev. Advanced setup nhưng đáng cho team lớn.
Mẹo 4: Giới hạn hook bằng /hooks
Nội bộ Claude Code có command /hooks — xem hook đang active, bật/tắt tạm thời khi debug.
Áp dụng ngay
Bài tập 1: Tạo hook log-only (15 phút)
Bước 1: Tạo .claude/settings.json trong project:
{
"hooks": {
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "jq . > /tmp/claude-hook.json"
}
]
}
]
}
}
Hoặc nếu không có jq:
"command": "cat > /tmp/claude-hook.json"
- Bước 2
- Restart Claude Code.
- Bước 3
- Prompt task đơn giản: "Tạo file hello.txt với nội dung 'test'"
- Bước 4
- Check
/tmp/claude-hook.json— JSON trông như thế nào?
Ghi:
hook_event_name: ____________tool_name: ____________tool_inputcó fields gì: ____________tool_responsecó gì: ____________
Bài tập 2 (optional, 15 phút): Lên danh sách hook team cần
Brainstorm: team bạn có rule nào đang dùng CLAUDE.md để enforce mà hay bị Claude quên? Liệt kê 3-5 rule:
- ____________
- ____________
- ____________
Mỗi rule, quyết định:
- PreToolUse hay PostToolUse?
- Matcher tool nào?
- Command làm gì?
Preview cho Bài 4.13 (Định nghĩa hook) và 4.14 (Implement).
Tóm tắt bài học
- Hook = command shell chạy tự động trước (Pre) hoặc sau (Post) tool call.
- PreToolUse có thể block (exit 2), PostToolUse chỉ react.
- 3 vị trí config — global (cá nhân habit), project (team shared), local (cá nhân project-specific).
- Structure JSON —
hooks.{PreToolUse|PostToolUse}[].matcher+hooks[].hooks[].command. - Hook = deterministic enforcement — không dựa vào Claude nhớ, hệ thống làm.
Tài liệu tham khảo
- Claude Code hooks docs
- Anthropic security for hooks
- Bài 4.13 — Định nghĩa hook (chi tiết 4 bước)
- Bài 4.14 — Implement hook đầu tiên