Câu chuyện thật:
- Áp dụng quy trình 4 bước thiết kế một hook từ đầu
- Đọc hiểu JSON mà Claude Code push vào stdin của hook
- Dùng đúng exit code để communicate với Claude (0 vs 2)
- List được các built-in tool (Read, Edit, Bash...) và schema input của chúng
- Nhận diện khi nào hook cần nhìn tool_input.file_path vs field khác
Quy trình 4 bước thiết kế hook
Bước 1: Pre hay Post?
Câu hỏi quyết định: "Tôi muốn chặn hay muốn react?"
Khó quyết? Nghĩ về "nếu hook fail, tool đã chạy chưa?" — Pre: chưa, Post: rồi.
Bước 2: Matcher — Tool nào trigger?
Xem list built-in tool:
Kết hợp:
Bước 3: Command nhận stdin JSON
Hook command phải:
Ví dụ Node.js:
Tương đương Python:
- OR: "Edit|Write|MultiEdit" → mọi edit-like
- All: "*" → mọi tool (ví dụ: log audit)
- Wildcard prefix: "mcp__playwright__*" (nếu runtime hỗ trợ — xem docs mới nhất)
- Đọc toàn bộ stdin (JSON object)
- Parse JSON
- Kiểm tra điều kiện
- Exit với code + stderr phù hợp
| Tool | Matcher string | Khi nào hook bạn cần trigger? |
|---|---|---|
| Read | Read | Track/block đọc file |
| Grep | Grep | Track/block search file |
| Glob | Glob | Ít khi cần hook |
| Edit | Edit | Format/typecheck sau sửa |
| Write | Write | Validate new file naming |
| MultiEdit | MultiEdit | Tương tự Edit |
| Bash | Bash | Block destructive commands |
| WebFetch | WebFetch | Block fetch URL nhất định |
| WebSearch | WebSearch | Rate limit |
| MCP tools | mcp__<server>__<tool> | Control MCP-specific |
┌──────────────────────────────────────────────────────────┐ │ │ │ 1. Chọn Pre hay Post? │ │ │ │ │ ▼ │ │ 2. Chọn matcher (tool nào trigger)? │ │ │ │ │ ▼ │ │ 3. Viết command nhận stdin JSON │ │ │ │ │ ▼ │ │ 4. Exit code + stderr để feedback Claude │ │ │ └──────────────────────────────────────────────────────────┘
async function main() {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
const input = JSON.parse(Buffer.concat(chunks).toString());
// Logic check ở đây
// input = { session_id, hook_event_name, tool_name, tool_input, ... }
if (shouldBlock(input)) {
console.error("Reason why blocked");
process.exit(2);
}
process.exit(0);
}
main().catch(err => {
console.error("Hook error:", err);
process.exit(1); // 1 = error (unexpected), Claude continues
});Bước 3: Command nhận stdin JSON
Hoặc shell (nhanh cho task đơn giản):
import sys, json
def main():
data = json.load(sys.stdin)
if should_block(data):
print("Reason", file=sys.stderr)
sys.exit(2)
sys.exit(0)
main()Quy trình 4 bước thiết kế hook (tiếp)
Bước 4: Exit code
Lưu ý: Exit 2 chỉ có effect trên PreToolUse. Trong PostToolUse, tool đã execute — exit 2 chỉ gửi error về Claude để nó react (thường là fix), không rollback.
┌──────────────────────────────────────────────────────────┐ │ │ │ Exit 0 → OK, let Claude proceed │ │ (Post: continue; Pre: tool executes) │ │ │ │ Exit 2 → BLOCK (chỉ PreToolUse) │ │ Tool không chạy. stderr gửi cho Claude như │ │ error message. │ │ │ │ Exit 1 → Hook itself errored │ │ Claude tiếp tục như không có hook. │ │ (Không an toàn nếu hook là security) │ │ │ └──────────────────────────────────────────────────────────┘
#!/bin/bash
jq -r '.tool_input.file_path' | grep -q "\.env$" && {
echo "Blocked .env access" >&2
exit 2
}
exit 0JSON input — Cấu trúc chi tiết
Fields chung cho mọi hook event
PreToolUse — thêm tool_name + tool_input
- session_id: unique per Claude Code session
- transcript_path: log file của session — hữu ích cho audit
- hook_event_name: PreToolUse, PostToolUse, và các event khác (Bài 4.17)
{
"session_id": "2d6a1e4d-6abc-...",
"transcript_path": "/Users/you/.claude/transcripts/...",
"hook_event_name": "PreToolUse"
}PreToolUse — thêm tool_name + tool_input
tool_input shape khác nhau tùy tool:
PostToolUse — thêm tool_response
| Tool | tool_input shape |
|---|---|
| Read | { file_path: string } |
| Grep | { pattern: string, path?: string, glob?: string } |
| Glob | { pattern: string, path?: string } |
| Edit | { file_path: string, old_string: string, new_string: string } |
| Write | { file_path: string, content: string } |
| MultiEdit | { file_path: string, edits: Array<{old,new}> } |
| Bash | { command: string, description?: string } |
| WebFetch | { url: string, prompt: string } |
| MCP tools | Schema specific theo server |
{
"session_id": "...",
"transcript_path": "...",
"hook_event_name": "PreToolUse",
"tool_name": "Read",
"tool_input": {
"file_path": "/Users/you/Projects/app/.env"
}
}PostToolUse — thêm tool_response
tool_response cung cấp output của tool — hữu ích để check "có thành công không" trước khi follow-up.
Code defensive khi đọc field
Field có thể khác tùy tool. Code defensive:
{
"session_id": "...",
"hook_event_name": "PostToolUse",
"tool_name": "Edit",
"tool_input": {
"file_path": "src/app.ts",
"old_string": "...",
"new_string": "..."
},
"tool_response": {
"success": true
}
}Code defensive khi đọc field
Đừng assume field tồn tại. Dùng optional chaining + default.
const path =
input.tool_input?.file_path || // Read, Edit, Write
input.tool_input?.path || // Grep, Glob
'';
const cmd = input.tool_input?.command || ''; // BashVí dụ: Thiết kế 3 hook từng bước
Hook A: Block .env access
Bước 1: Pre hay Post? → Pre (block)
Bước 2: Matcher? → Read|Grep (tools có thể access file content)
Bước 3: Command logic
Bước 4: Exit code
Hook B: Auto-format TS
Bước 1? → Post (format sau khi edit)
Bước 2? → Edit|Write|MultiEdit
Bước 3?
- Match pattern → exit 2 + stderr
- Else → exit 0
const path = input.tool_input?.file_path || input.tool_input?.path || '';
if (/\.env(\.|$)/.test(path)) {
console.error("Blocked .env file access. Use process.env at runtime.");
process.exit(2);
}
process.exit(0);Hook B: Auto-format TS
Bước 4? Exit 0 sau format. Exit 1 nếu Prettier crash (nhưng không rollback edit).
Hook C: Block dangerous bash
Bước 1? → Pre
Bước 2? → Bash
Bước 3?
const path = input.tool_input?.file_path || '';
if (!/\.(ts|tsx|js|jsx)$/.test(path)) process.exit(0);
const { execSync } = require('child_process');
try {
execSync(`npx prettier --write "${path}"`, { stdio: 'pipe' });
process.exit(0);
} catch (err) {
console.error("Prettier failed:", err.stderr?.toString());
process.exit(1); // 1 không block, chỉ log error
}Hook C: Block dangerous bash
Bước 4? Block + stderr.
const cmd = input.tool_input?.command || '';
const DANGEROUS = [
/\brm\s+-rf\s+\//,
/\bdd\s+if=/,
/\b:\(\)\s*\{\s*:\|:/, // fork bomb
/\bmkfs/,
/\bsudo\s+(rm|dd|mkfs)/,
];
for (const pat of DANGEROUS) {
if (pat.test(cmd)) {
console.error(`Blocked dangerous command: ${cmd}`);
process.exit(2);
}
}
process.exit(0);Ví dụ thực chiến: Team setup 3 hook foundational
Context
Team 10 dev, fintech. Setup hook baseline:
settings.json
Kết quả
4 hook, ~150 dòng code. Guardrails:
Team confidence dùng Claude Code lên cao — infrastructure enforce boundaries, không dựa vào prompt.
- Block .env + secrets/
- Auto-format + typecheck TS
- Log mọi Bash command
- Claude không đọc .env / secrets/
- Claude không rm -rf
- Mọi TS edit auto-format + typecheck
- Mọi command shell Claude chạy có trong audit log
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Grep",
"hooks": [
{ "type": "command", "command": "node $PWD/.claude/hooks/block-secrets.js" }
]
},
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "node $PWD/.claude/hooks/block-dangerous.js" }
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{ "type": "command", "command": "node $PWD/.claude/hooks/format-and-check.js" }
]
},
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "node $PWD/.claude/hooks/audit-log.js" }
]
}
]
}
}Case studies theo ngành
💰 Fintech — PreToolUse block prod DB destructive
Setup: PreToolUse trên Bash match pattern DROP|TRUNCATE|DELETE FROM với target prod host → block.
🏥 Health tech — PreToolUse enforce audit
Setup: PreToolUse mọi tool, log vào append-only audit log với tamper-proof.
🛠️ Platform — PostToolUse Kubernetes manifest validation
Setup: PostToolUse trên Write/Edit file *.yaml — chạy kubectl apply --dry-run.
🔐 Security — PreToolUse block public repo push
Setup: PreToolUse Bash match git push với remote public → confirm.
🎨 Design system — PostToolUse enforce token usage
Setup: PostToolUse trên file *.tsx — scan hardcoded color/spacing, feedback "use token instead".
- Kết quả: Zero incident data loss trong 1 năm.
- Kết quả: HIPAA audit pass, có evidence mọi action.
- Kết quả: Zero prod manifest error, catch before deploy.
- Kết quả: Zero accidental secret leak vào repo public trong 8 tháng.
- Kết quả: Design token adoption 95%, visual consistency tăng rõ.
Anti-patterns
❌ Không check tool_name, apply logic cho mọi tool
Biểu hiện: Matcher *, nhưng logic expect file_path → crash khi gặp Bash.
Cách đúng: Matcher precise HOẶC early return nếu field không tồn tại.
❌ Hook làm heavy work trong hot path
Biểu hiện: PostToolUse chạy npm run test full suite → 60s.
Cách đúng: Chạy subset, background, hoặc async queue.
❌ Exit code sai
Biểu hiện: Dev nghĩ "exit 1" block — thực ra exit 1 chỉ báo hook error, Claude continue.
Cách đúng: Nhớ exit 2 để block (chỉ Pre).
❌ Swallow error silent
Biểu hiện: Hook crash, catch nuốt error, exit 0.
Cách đúng: Log error to stderr, exit code phù hợp (1 cho error hook, không phải 0 giả vờ OK).
❌ Regex quá lỏng
Biểu hiện: /env/ match cả environment.ts (public file).
Cách đúng: Tight regex: /\.env(\.[a-z]+)?$/ match .env, .env.local, .env.production.
Mẹo nâng cao
Mẹo 1: Test hook độc lập
Trước khi deploy team, test hook trong isolation:
Mock input → chạy hook → check exit code. Nhanh.
Mẹo 2: Share hook qua npm package
Team lớn: publish hook library @acme/claude-hooks, mỗi dev npm install, config trỏ đến package. Update một chỗ, cả team hưởng.
Mẹo 3: Conditional hook based trên branch
echo '{"hook_event_name":"PreToolUse","tool_name":"Read","tool_input":{"file_path":".env"}}' | node your-hook.js
echo "Exit: $?"Mẹo 3: Conditional hook based trên branch
Mẹo 4: Timeout cho hook
Nếu hook có thể hang (network call), add timeout:
const branch = execSync('git branch --show-current').toString().trim();
if (branch === 'main' || branch.startsWith('release/')) {
// strict hooks
} else {
// relaxed hooks for dev branches
}Mẹo 4: Timeout cho hook
const timeout = setTimeout(() => {
console.error("Hook timed out");
process.exit(1);
}, 10_000);
// ... async work ...
clearTimeout(timeout);
process.exit(0);Áp dụng ngay
Bài tập 1: Design hook trên giấy (10 phút)
Chọn 1 rule team bạn cần enforce. Trả lời 4 bước:
Bài tập 2: Đọc JSON thực tế (15 phút)
Setup hook log-only (giống Bài 4.12 bài tập 1). Trigger các prompt khác nhau, check JSON cho:
Ghi lại field chính cho mỗi. Giúp bạn viết hook tương lai không phải đoán.
- Pre/Post? _____
- Matcher? _____
- Pseudo-code logic:
- Exit code strategy:
- Happy case: _____
- Violation: _____
- Prompt đọc file → JSON cho Read tool
- Prompt edit file → JSON cho Edit tool (chú ý old_string, new_string)
- Prompt chạy npm test → JSON cho Bash tool
- Prompt grep code → JSON cho Grep tool
_____Tóm tắt bài học
🎯 4-bước design: Pre/Post → Matcher → Command → Exit code.
🎯 Exit 0 = allow, Exit 2 = block (Pre only), Exit 1 = hook error.
🎯 JSON stdin shape tùy tool — code defensive với optional chaining.
🎯 Test hook cô lập trước deploy team — mock input dễ.
🎯 Tight regex — precise match, tránh false positive.
- Claude Code hooks schema
- Built-in tools reference
- Bài 4.14 — Implement hook chi tiết
- Bài 4.17 — Hook events khác (Stop, SubagentStop, ...)