Lộ trình học›Claude Code›Claude Code in Action Mở bản đồ

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

Bài viếtTrung cấp → Nâng25 phút đọc

Câu chuyện thật:

Sau bài này, bạn sẽ
  1. Áp dụng quy trình 4 bước thiết kế một hook từ đầu
  2. Đọc hiểu JSON mà Claude Code push vào stdin của hook
  3. Dùng đúng exit code để communicate với Claude (0 vs 2)
  4. List được các built-in tool (Read, Edit, Bash...) và schema input của chúng
  5. Nhận diện khi nào hook cần nhìn tool_input.file_path vs field khác

Mở đầu: Tại sao viết hook lần đầu khó?

Dev: "Tôi muốn hook block nếu Claude định rm -rf. Nhưng tôi không biết JSON Claude push vào stdin có field gì, hay exit code nào để block...

Vấn đề không phải khái niệm, mà là thiếu chỉ dẫn về data shape và control flow.

Bài này filled that gap. Bạn sẽ biết:

  1. Quy trình 4-bước để thiết kế hook trước khi code
  2. JSON shape cho mỗi loại tool
  3. Exit code rules
  4. Cách inspect JSON nếu không nhớ

Quy trình 4 bước thiết kế hook

┌──────────────────────────────────────────────────────────┐
│                                                          │
│   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               │
│                                                          │
└──────────────────────────────────────────────────────────┘

Bước 1: Pre hay Post?

Câu hỏi quyết định: "Tôi muốn chặn hay muốn react?"

Mục đíchLoại hook
Chặn tool call không an toànPreToolUse
Require confirm trước actionPreToolUse
Format/lint sau editPostToolUse
Run test sau changePostToolUse
Feedback type error quay về ClaudePostToolUse
Audit logThường PostToolUse
Enforce naming trước WritePreToolUse

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:

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

Kết hợp:

  • 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)

Bước 3: Command nhận stdin JSON

Hook command phải:

  1. Đọc toàn bộ stdin (JSON object)
  2. Parse JSON
  3. Kiểm tra điều kiện
  4. Exit với code + stderr phù hợp

Ví dụ Node.js:

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
});

Tương đương Python:

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()

Hoặc shell (nhanh cho task đơn giản):

#!/bin/bash
jq -r '.tool_input.file_path' | grep -q "\.env$" && {
  echo "Blocked .env access" >&2
  exit 2
}
exit 0

Bước 4: Exit code

┌──────────────────────────────────────────────────────────┐
│                                                          │
│   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)         │
│                                                          │
└──────────────────────────────────────────────────────────┘

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.

JSON input — Cấu trúc chi tiết

Fields chung cho mọi hook event

{
  "session_id": "2d6a1e4d-6abc-...",
  "transcript_path": "/Users/you/.claude/transcripts/...",
  "hook_event_name": "PreToolUse"
}
  • 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)

PreToolUse — thêm tool_name + tool_input

{
  "session_id": "...",
  "transcript_path": "...",
  "hook_event_name": "PreToolUse",
  "tool_name": "Read",
  "tool_input": {
    "file_path": "/Users/you/Projects/app/.env"
  }
}

tool_input shape khác nhau tùy tool:

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

PostToolUse — thêm tool_response

{
  "session_id": "...",
  "hook_event_name": "PostToolUse",
  "tool_name": "Edit",
  "tool_input": {
    "file_path": "src/app.ts",
    "old_string": "...",
    "new_string": "..."
  },
  "tool_response": {
    "success": true
  }
}

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:

const path = 
  input.tool_input?.file_path ||    // Read, Edit, Write
  input.tool_input?.path ||          // Grep, Glob
  '';

const cmd = input.tool_input?.command || '';  // Bash

Đừng assume field tồn tại. Dùng optional chaining + default.

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

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);

Bước 4: Exit code

  • Match pattern → exit 2 + stderr
  • Else → exit 0

Hook B: Auto-format TS

Bước 1? → Post (format sau khi edit)

Bước 2? → Edit|Write|MultiEdit

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
}

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 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);

Bước 4? Block + stderr.

Ví dụ thực chiến: Team setup 3 hook foundational

Context

Team 10 dev, fintech. Setup hook baseline:

  1. Block .env + secrets/
  2. Auto-format + typecheck TS
  3. Log mọi Bash command

settings.json

{
  "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" }
        ]
      }
    ]
  }
}

Kết quả

4 hook, ~150 dòng code. Guardrails:

  • 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

Team confidence dùng Claude Code lên cao — infrastructure enforce boundaries, không dựa vào prompt.

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.

  • Kết quả: Zero incident data loss trong 1 năm.

Health tech — PreToolUse enforce audit

Setup: PreToolUse mọi tool, log vào append-only audit log với tamper-proof.

  • Kết quả: HIPAA audit pass, có evidence mọi action.

Platform — PostToolUse Kubernetes manifest validation

Setup: PostToolUse trên Write/Edit file *.yaml — chạy kubectl apply --dry-run.

  • Kết quả: Zero prod manifest error, catch before deploy.

Security — PreToolUse block public repo push

Setup: PreToolUse Bash match git push với remote public → confirm.

  • Kết quả: Zero accidental secret leak vào repo public trong 8 tháng.

Design system — PostToolUse enforce token usage

Setup: PostToolUse trên file *.tsx — scan hardcoded color/spacing, feedback "use token instead".

  • 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:

echo '{"hook_event_name":"PreToolUse","tool_name":"Read","tool_input":{"file_path":".env"}}' | node your-hook.js
echo "Exit: $?"

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

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

Nếu hook có thể hang (network call), add timeout:

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:

  1. Pre/Post? _____
  2. Matcher? _____
  3. Pseudo-code logic:
   _____
  1. Exit code strategy: — Happy case: _____ — Violation: _____

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:

  1. Prompt đọc file → JSON cho Read tool
  2. Prompt edit file → JSON cho Edit tool (chú ý old_string, new_string)
  3. Prompt chạy npm test → JSON cho Bash tool
  4. Prompt grep code → JSON cho Grep tool

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.

Tóm tắt bài học

Những điều cốt lõi
  • 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

Kiểm tra nhanh1 câu hỏi · chọn đáp án để xem giải thích
    Bạn đã hoàn thành bài này chưa?Tiến độ lưu trên trình duyệt của bạn.