Định nghĩa hook — 4 bước thiết kế

5 — HooksTrung cấp25 phút

Câu chuyện thật:

Bạn sẽ học được
  • Á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
ToolMatcher stringKhi nào hook bạn cần trigger?
ReadReadTrack/block đọc file
GrepGrepTrack/block search file
GlobGlobÍt khi cần hook
EditEditFormat/typecheck sau sửa
WriteWriteValidate new file naming
MultiEditMultiEditTương tự Edit
BashBashBlock destructive commands
WebFetchWebFetchBlock fetch URL nhất định
WebSearchWebSearchRate limit
MCP toolsmcp__<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 0

JSON 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

Tooltool_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 toolsSchema 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 || '';  // Bash

Ví 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.

Tài liệu tham khảo
  • 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, ...)
Nội dung này có hữu ích không?