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

4.12 · Giới thiệu hooks

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

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:

Sau bài này, bạn sẽ
  1. Giải thích hook là gì và nó chèn vào luồng Claude Code ở đâu
  2. Phân biệt PreToolUse và PostToolUse hook, và khi nào dùng mỗi loại
  3. Nhận ra 3 vị trí lưu cấu hình hook (global / project shared / project local)
  4. Đọc hiểu structure JSON của một hook configuration
  5. 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 ở đâuLý do
Auto-format TS khi editproject/.claude/settings.jsonTeam convention
Block đọc .envproject/.claude/settings.jsonTeam security
Run typecheck sau editproject/.claude/settings.jsonTeam quality
Log mọi tool call của Claude vào file cá nhân để audit.claude/settings.local.jsonDebug cá nhân
Không cho Claude rm trong mọi project~/.claude/settings.jsonSafety 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 hook
  • matcher: 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:

  1. Không ai (Claude) được đọc .env
  2. 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 .env trong 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_input có fields gì: ____________
  • tool_response có 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:

  1. ____________
  2. ____________
  3. ____________

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

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

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.