# Hooks trong Claude Code 2026: hướng dẫn thiết lập từng bước và 7 ví dụ dùng ngay

> 7 hooks dùng ngay cho .claude/settings.json, sẵn sàng sao chép và dán: bảo vệ .env, prettier, chặn force-push lên main, em dash và máy chủ dev. 4 loại handler và 31 sự kiện.

Source: https://ailoot.ai/blog/vi/claude-code-hooks/
Canonical: https://ailoot.ai/blog/vi/claude-code-hooks/
Language: vi
Publisher: Ailoot (https://ailoot.ai/)
Translation: guide-121
Revision: sha256-0d86f41f664af7404cebd1ae49b8f8ea10a5a0025cafa01e73ef84b9d2f1cdf3


Hooks trong Claude Code là các lệnh shell, lệnh gọi HTTP hoặc bước kiểm tra LLM ngắn chạy tự động trước hoặc sau một hành động của Claude. Chúng thêm kiểm vào sự kiện công cụ cụ thể, định dạng mã sau sửa và chèn ngữ cảnh lúc mở phiên. PreToolUse khớp có thể từ chối lời gọi đó; regex này là guard tiện dụng, không ranh giới bảo mật qua mọi đường tệp/Git. Tiếp theo là phần phân tích 4 loại handler, 31 sự kiện vòng đời, bài kiểm tra “khi nào một quy tắc nên nằm trong hook”, 7 hook sẵn sàng sao chép vào `.claude/settings.json` và 3 cạm bẫy tôi từng gặp.



[Tài liệu Ailoot: API, khóa và thiết lập](https://ailoot.ai/docs/).

## Hooks trong Claude Code là gì?

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

Điểm chính

Hook là một lệnh shell, lệnh gọi HTTP, prompt LLM ngắn hoặc subagent mà Claude Code chạy để phản hồi một sự kiện vòng đời cụ thể. Khác với hướng dẫn trong `CLAUDE.md`, vốn có thể bị mô hình bỏ qua hoặc quên, hook luôn chạy theo cùng một cách: đó là mã, không phải lời gợi ý.

</div>

Suốt nửa năm, tôi giữ quy tắc “không bao giờ chỉnh sửa trực tiếp `.env`” trong `CLAUDE.md`. Mỗi tháng Claude lại vi phạm một lần. Không phải cố ý; đơn giản là đến cuối phiên dài, nó quên mất. Có lần một token thừa lọt vào `.env`; lần khác, một biến trùng tên ghi đè biến đang được dùng.

Tôi chuyển quy tắc sang hook. Khi công cụ sửa trực tiếp khớp cung cấp đường `.env`, hook cấu hình từ chối lời gọi đó. Bash redirection, script, MCP và đường khác cần kiểm soát riêng; ví dụ không làm tệp bất biến.

<figure>
<blockquote>
<p>Hooks là các lệnh shell, điểm cuối HTTP hoặc prompt LLM do bạn định nghĩa và tự động chạy tại những thời điểm cụ thể trong vòng đời Claude Code.</p>
</blockquote>
<figcaption>— Anthropic, https://code.claude.com/docs/en/hooks</figcaption>
</figure>

Nói cách khác, hook là lệnh của bạn mà Claude Code chạy khi một sự kiện cụ thể xảy ra. Mỗi lần chỉnh sửa tệp, khởi chạy Bash, bắt đầu phiên hoặc subagent hoàn tất đều là một sự kiện mà bạn có thể gắn hook vào.

Điều này quan trọng với người viết mã theo kiểu vibe coding vì:

- Bảo vệ các tệp chứa bí mật khỏi bị chỉnh sửa ngoài ý muốn.
- Tự động chạy prettier/eslint sau mỗi lần tạo mã.
- Chặn `git push --force` lên main nếu chưa được cho phép rõ ràng.
- Chèn ngữ cảnh mới vào đầu mỗi phiên.
- Ghi nhật ký hoạt động của Claude để kiểm tra.

CLAUDE.md xử lý ngữ cảnh. Hooks xử lý **hành động**. Đây là hai việc khác nhau, và nhầm lẫn chúng là lỗi phổ biến nhất của người mới.

## Hooks khác CLAUDE.md và skills như thế nào?

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

Điểm chính

`CLAUDE.md` là văn bản mà mô hình đọc khi bắt đầu phiên và cố gắng làm theo. Hook là lệnh shell do tiến trình Claude Code chạy để phản hồi một sự kiện, độc lập với mô hình. Skill là một gói hướng dẫn có tên mà Claude có thể gọi bằng công cụ Skill, giống như một hàm. Cả ba giải quyết các loại nhiệm vụ khác nhau.

</div>

Tài liệu của Anthropic giải thích rõ sự khác biệt giữa `CLAUDE.md` và hooks:

<figure>
<blockquote>
<p>Nội dung CLAUDE.md được gửi dưới dạng tin nhắn người dùng sau system prompt, không phải một phần của system prompt. Claude đọc và cố gắng làm theo, nhưng không có bảo đảm tuân thủ nghiêm ngặt.</p>
</blockquote>
<figcaption>— Anthropic, tài liệu về memory, https://code.claude.com/docs/en/memory</figcaption>
</figure>

Nói cách khác: nội dung `CLAUDE.md` đến với Claude dưới dạng tin nhắn đầu tiên từ người dùng, không phải chỉ thị hệ thống. Mô hình sẽ đọc và cố gắng làm theo. Nhưng không có gì bảo đảm, nhất là về cuối phiên dài, khi ngữ cảnh tăng lên và các hướng dẫn cũ bị đẩy ra ngoài.

Hook hoạt động khác. Nó nằm trong `settings.json`, không phụ thuộc mô hình và được tiến trình Claude Code chạy khi có sự kiện cụ thể: `PreToolUse`, `PostToolUse`, `SessionStart` và các sự kiện khác. Lệnh chạy trước khi Claude nhận kết quả và có thể chặn hành động thực sự (exit 2) hoặc sửa tệp sau khi chỉnh sửa (exit 0).

So sánh:

| Tính năng | CLAUDE.md | Hook | Skill |
|----|----|----|----|
| Nằm ở đâu | `CLAUDE.md` trong thư mục gốc dự án | `.claude/settings.json` | Thư mục `.claude/skills/` |
| Ai chạy | Mô hình đọc như một phần tin nhắn người dùng | Tiến trình Claude Code khi có sự kiện | Mô hình thông qua công cụ Skill |
| Có bảo đảm thực thi không | Không (cố gắng hết sức) | Hook command cấu hình đúng, khớp sẽ chạy mã; lỗi và hành vi exit tùy sự kiện | Qua một lần gọi mô hình (có cấp quyền, nhưng không ở cấp shell) |
| Dùng để làm gì | Ngữ cảnh dự án, giọng điệu, phong cách, định tuyến | Hạn chế, định dạng, kiểm tra một số tệp cụ thể | Quy trình nhiều bước phức tạp có tham số |
| Chạy khi nào | Mỗi tin nhắn của mô hình trong phiên | Khi có sự kiện vòng đời | Khi mô hình gọi |
| Có thể chặn hành động không | Không | Có (exit 2) | Không |

Mẫu `CLAUDE.md` đầy đủ và 6 quy tắc để duy trì tệp này nằm trong hướng dẫn [Cách cấu hình CLAUDE.md năm 2026: mẫu dùng ngay và 6 quy tắc](https://ailoot.ai/blog/vi/claude-md-template/). Tìm hiểu về skills như các năng lực có tên trong [Claude Code Skills: Cách xây dựng thư viện của riêng bạn](https://ailoot.ai/blog/vi/claude-code-skills-library/).

Hook là cấp thấp nhất: không có mô hình, không diễn giải, chỉ có shell.

Hooks là một trong các công cụ [context engineering](https://ailoot.ai/blog/vi/context-engineering/), cùng với Plan Mode và /compact.



## 4 loại hook: Command, HTTP, Prompt, Agent — nên chọn loại nào?

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

Điểm chính

Claude Code có 4 loại handler. Command là tập lệnh shell, loại phổ biến nhất. HTTP gửi yêu cầu POST đến URL của bạn, phù hợp với nhóm và tích hợp. Prompt gọi Haiku trong thời gian ngắn để kiểm tra ngữ nghĩa. Agent tạo subagent có quyền truy cập công cụ để kiểm tra sâu. Người mới cần Command và Prompt; chúng đáp ứng 90% nhiệm vụ.

</div>

| Loại | Chức năng | Khi nào nên chọn | Độ phức tạp |
|----|----|----|----|
| **Command** | Chạy lệnh shell, nhận JSON qua stdin và phản hồi bằng mã thoát cùng stdout | 90% trường hợp: kiểm tra đường dẫn, định dạng, grep nội dung, lsof, thao tác git | thấp |
| **HTTP** | Gửi yêu cầu POST đến URL của bạn với JSON body và nhận JSON phản hồi | Khi quy tắc thuộc về nhóm/CI: xác thực tập trung, bảng trạng thái dùng chung, kiểm tra từ xa | trung bình (cần máy chủ) |
| **Prompt** | Gửi prompt ngắn đến Haiku (theo mặc định) và nhận yes/no để đánh giá ngữ nghĩa | Khi grep chưa đủ: “đánh giá xem văn bản này có mang giọng điệu doanh nghiệp không” | thấp |
| **Agent** | Tạo subagent có quyền truy cập Read/Grep/Glob để thực hiện kiểm tra nhiều bước | Xác thực nặng: “tìm mọi tệp đã thay đổi và kiểm tra xem có bài kiểm tra tương ứng không” | cao |

Lỗi phổ biến nhất là cố dùng Command cho việc cần Prompt. Ví dụ, “không dùng em dash trong tệp `.md`” là tác vụ grep, nên Command rất phù hợp. Nhưng “đừng viết với giọng điệu doanh nghiệp” là vấn đề ngữ nghĩa, nên cần Prompt.

Nếu mới bắt đầu, hãy dùng Command. Cả 7 hook dùng ngay bên dưới đều là Command. HTTP/Prompt/Agent có thể dùng sau, khi bạn sẵn sàng.

## settings.json nằm ở đâu và có 4 cấp cấu hình nào?

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

Điểm chính

Claude Code tìm cấu hình ở 4 nơi: cấp doanh nghiệp (triển khai qua MDM), cấp người dùng (`~/.claude/settings.json`), cấp dự án (`.claude/settings.json` ở thư mục gốc repository) và cấp local (`.claude/settings.local.json`, không được đưa vào git). Cả 4 tệp được hợp nhất khi bắt đầu phiên, trong đó cấp thấp hơn được ưu tiên.

</div>

| Cấp | Đường dẫn | Áp dụng khi nào | Có trong git? |
|----|----|----|----|
| Doanh nghiệp | `/Library/Application Support/ClaudeCode/settings.json` (Mac), `/etc/claude-code/settings.json` (Linux) | Cho cả nhóm thông qua MDM | không |
| Người dùng | `~/.claude/settings.json` | Mọi dự án của một người dùng | không |
| Dự án | `.claude/settings.json` trong thư mục gốc repository | Dự án này, cho cả nhóm | **có** |
| Local | `.claude/settings.local.json` | Chỉ dự án này, chỉ người dùng này | **không** (trong `.gitignore`) |

Tôi giữ 7 hook mô tả bên dưới trong `.claude/settings.json` cấp **dự án**, để chúng tự động được đưa vào khi repository được clone và Claude áp dụng cùng quy tắc trong cửa sổ mới. Các hook thử nghiệm cá nhân được để trong `.claude/settings.local.json` để không ảnh hưởng đến nhóm.

Cả 4 tệp được hợp nhất khi bắt đầu phiên. Nếu quy tắc xung đột, cấp thấp hơn được ưu tiên (local trên project, project trên user).

**Cách nhanh nhất để tạo hook đầu tiên** là dùng menu tương tác `/hooks` ngay trong Claude Code. Chọn một sự kiện trong danh sách, chọn matcher (ví dụ `Bash` hoặc `Edit`), rồi dán lệnh vào. Claude Code sẽ ghi JSON hợp lệ vào `settings.json` cho bạn.

## 31 sự kiện vòng đời: người mới cần những sự kiện nào?

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

Điểm chính

Claude Code hiện có 31 sự kiện vòng đời, chia thành 3 nhịp: một lần mỗi phiên (`SessionStart`/`SessionEnd`), một lần mỗi lượt (`UserPromptSubmit`/`Stop`) và mỗi lần gọi công cụ (`PreToolUse`/`PostToolUse`). Người mới cần 4 trong số 31 sự kiện: `PreToolUse` cho các hạn chế, `PostToolUse` cho chỉnh sửa tự động, `SessionStart` cho ngữ cảnh và `Stop` cho kiểm tra cuối cùng.

</div>

<figure>
<blockquote>
<p>Sự kiện xảy ra theo ba tần suất: một lần mỗi phiên (SessionStart, SessionEnd), một lần mỗi lượt (UserPromptSubmit, Stop, StopFailure) và mỗi lần gọi công cụ trong agentic loop (PreToolUse, PostToolUse).</p>
</blockquote>
<figcaption>— Anthropic, tài liệu tham khảo hooks, https://code.claude.com/docs/en/hooks</figcaption>
</figure>

Cần hiểu ba nhịp này để tránh nhầm lẫn:

1.  **Một lần mỗi phiên** - `SessionStart`, `SessionEnd`, `Setup`. Chúng chạy một lần khi Claude Code mở và đóng. Dùng để khởi tạo ngữ cảnh và dọn tệp tạm.
2.  **Một lần mỗi lượt** - `UserPromptSubmit`, `UserPromptExpansion`, `Stop`, `StopFailure`. Chúng kích hoạt mỗi khi bạn gửi tin nhắn và mỗi khi mô hình dừng. Dùng để ghi lại cuộc trò chuyện và kiểm tra kết quả cuối cùng.
3.  **Mỗi lần gọi công cụ** - `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `PermissionDenied`, `PostToolBatch`, `SubagentStart`, `SubagentStop`. Chúng kích hoạt mỗi lần chỉnh sửa tệp, khởi chạy Bash, đọc hoặc tìm kiếm. Đây là lớp hoạt động dày đặc nhất, nơi đặt các hạn chế, định dạng và bước xác thực.

Danh sách đầy đủ 31 sự kiện còn có các sự kiện bất đồng bộ/phản ứng: `TeammateIdle`, `PreCompact`, `PostCompact`, `Elicitation`, `ElicitationResult`, `InstructionsLoaded`, `ConfigChange`, `CwdChanged`, `FileChanged`, `WorktreeCreate`, `WorktreeRemove`, `Notification`. Chúng dành cho trường hợp nâng cao; hiện tại hãy bỏ qua.

## Bài kiểm tra 5 câu hỏi: khi nào quy tắc nên nằm trong hook, khi nào giữ trong CLAUDE.md?

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

Điểm chính

Nếu một quy tắc nói về hành động (không phải ngữ cảnh), liên quan đến an toàn/tiền/dữ liệu, có thể kiểm tra bằng grep/regex, khó khắc phục nếu vi phạm và Claude đã từng vi phạm ít nhất một lần, thì quy tắc đó nên nằm trong hook. Nếu có ít hơn ba câu trả lời “có” trong năm câu, hãy giữ nó trong `CLAUDE.md`. Tôi áp dụng bài kiểm tra cho 80 dòng của mình và chuyển 5 quy tắc sang hooks.

</div>

Với tôi, cách này giống một đạo luật, không phải lời gợi ý. Tôi rút gọn lựa chọn giữa `CLAUDE.md` và hook thành bài kiểm tra đơn giản gồm năm câu hỏi. Nếu có ít nhất ba câu trả lời “có”, quy tắc được đưa vào hook. Ít hơn ba thì giữ trong văn bản.

**Bài kiểm tra “khi nào quy tắc nên nằm trong hook?”**

1.  Quy tắc này nói về hành động thay vì ngữ cảnh phải không? (“đừng chạy `rm -rf`” là hành động; “tôi có dự án TypeScript” là ngữ cảnh.)
2.  Quy tắc này liên quan đến an toàn, tiền bạc hoặc dữ liệu không? (“đừng động vào `.env`”, “đừng công bố khóa”.)
3.  Có thể kiểm tra bằng grep, regex hoặc mã thoát của lệnh shell không? (“sau khi sửa `.md`, kiểm tra để chắc chắn không có em dash”.)
4.  Nếu vi phạm quy tắc, hậu quả có khó khắc phục không? (“force push lên main” khó khắc phục; “quên dấu chấm cuối câu” thì dễ.)
5.  Bạn đã từng thấy Claude quên quy tắc này ít nhất một lần chưa?

**Ví dụ: “Đừng dùng em dash; chỉ dùng dấu gạch nối.”** Hành động? Có. An toàn? Không. Kiểm tra bằng grep? Có. Khó khắc phục? Không. Đã thấy vi phạm? Có. **3 trên 5: đưa vào hook.**

**Phản ví dụ: “Tôi có vibe coders, không phải developers; hãy viết không dùng tiếng lóng.”** Hành động? Nghiêng về ngữ cảnh hơn. An toàn? Không. Có thể kiểm tra bằng regex? Quá khó. Khó khắc phục? Không. Đã thấy vi phạm? Có. **1 trên 5: giữ trong `CLAUDE.md`.**

Năm quy tắc của tôi vượt qua bài kiểm tra. Chúng được chuyển sang hooks. 75% còn lại vẫn nằm trong tệp văn bản.

## 7 hook sẵn sàng sao chép

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

Điểm chính

Dưới đây là 7 hook dùng ngay cho `.claude/settings.json`: chặn chỉnh sửa `.env`, tự động chạy prettier, ngăn force-push lên main, kiểm tra em dash, giải phóng cổng trước `npm run dev`, tự động commit sau khi kiểm thử thành công và chèn ngữ cảnh mới khi bắt đầu phiên. Sao chép từng hook vào tệp, khởi động lại Claude Code là dùng được.

</div>

Cả 7 hook được lưu trong một tệp `.claude/settings.json` tại thư mục gốc dự án. Mỗi mục có loại sự kiện, bộ lọc công cụ (matcher) và lệnh shell. Exit 2 từ chối lời gọi PreToolUse khớp trước thực thi. Ở PostToolUse, hành động đã xảy ra; hook báo phản hồi, không hoàn tác.

<figure>
<blockquote>
<p>Với hầu hết sự kiện hook, chỉ exit code 2 mới chặn hành động. Claude Code coi exit code 1 là lỗi không chặn và tiếp tục hành động, dù theo thông lệ Unix, 1 biểu thị thất bại.</p>
</blockquote>
<figcaption>— Anthropic, tài liệu tham khảo hooks, https://code.claude.com/docs/en/hooks</figcaption>
</figure>

`exit 0` nghĩa là thành công. `exit 1` nghĩa là có lỗi, nhưng Claude vẫn tiếp tục (đây là anti-pattern đầu tiên bên dưới). `exit 2` tùy sự kiện: có thể chặn PreToolUse; PostToolUse chỉ phản hồi về hành động đã làm.

### Hook 1. Kiểm soát sửa `.env` trực tiếp (PreToolUse, exit 2)

Chặn việc chỉnh sửa trực tiếp bất kỳ tệp nào khớp với `.env`, `.env.local`, `.env.production` và các tên tương tự.

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '(^|/)\\.env(\\.|$)' && { echo 'Do not edit `.env` directly. Tell me which variable to add, and I’ll open the file myself.' >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}
```

**Cách hoạt động:** Trước mỗi `Edit`/`Write`/`MultiEdit`, hook lấy đường dẫn tệp, kiểm tra bằng grep xem có khớp mẫu `.env` không, và nếu có thì trả exit 2 cùng thông báo. Claude nhìn thấy stderr và nói với tôi: “Tôi đã thử chỉnh sửa `.env` nhưng hook chặn lại; hãy cho tôi biết cần thêm biến nào.”

### Hook 2. Tự động chạy prettier sau mỗi lần sửa mã (PostToolUse, exit 0)

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(ts|tsx|js|jsx|json)$' && npx prettier --write \"$FILE\" 2>/dev/null & exit 0"
          }
        ]
      }
    ]
  }
}
```

**Cách hoạt động:** Sau mỗi `Edit`/`Write`, hook lấy đường dẫn và nếu phần mở rộng là `.ts`/`.tsx`/`.js`/`.jsx`/`.json` thì chạy prettier trong nền bằng `&`. Hook luôn thoát với mã 0 vì đây là định dạng, không phải hạn chế. Nếu prettier thất bại cũng không sao; tệp vẫn như cũ.

Khác biệt giữa hook 1 và hook 2 là căn bản: PreToolUse + exit 2 chặn cứng **trước** hành động. PostToolUse + exit 0 chỉnh sửa mềm **sau** hành động. Đây là hai chế độ khác nhau cho những nhiệm vụ khác nhau.

### Hook 3. Chặn `git push --force` lên nhánh main (PreToolUse, exit 2)

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE 'git[[:space:]]+push' && echo \"$CMD\" | grep -qE '(\\-\\-force|\\-\\-force-with-lease|[[:space:]]-f([[:space:]]|$))' && echo \"$CMD\" | grep -qE '([[:space:]/:])main([[:space:]/:]|$)' && { echo 'Force push to main is forbidden. Create a new branch or ask me to do it manually.' >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}
```

**Cách hoạt động:** Mỗi lần khởi chạy Bash, hook kiểm tra lệnh có chứa `push`, có `--force`/`--force-with-lease`/`-f` và nhắc đến `main` hay không. Nếu cả ba điều kiện đều khớp, hook chặn lệnh bằng thông báo rõ ràng.

Trước đây tôi viết quy tắc này bằng chữ in hoa trong `CLAUDE.md`. Claude vẫn vi phạm khoảng hai tuần một lần khi tôi yêu cầu nó “force push để hoàn tác commit lỗi của tôi”. Hook bắt dạng lệnh tường minh được nêu nhưng alias, script, nhánh đích ngầm hoặc Git client khác có thể vượt kiểm chuỗi. Cưỡng chế bảo vệ main trên remote, giới hạn credential tác nhân có.

### Hook 4. Chặn em dash trong tệp văn bản (PostToolUse, exit 2)

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(md|mdx|tsx?)$' && grep -q $'\\xe2\\x80\\x94' \"$FILE\" && { echo \"File $FILE contains an em dash (U+2014). Replace it with a regular hyphen (U+002D).\" >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}
```

**Cách hoạt động:** PostToolUse exit 2 báo lỗi hậu kiểm cho Claude sau sửa; không rollback hay ngăn ghi. Nếu Claude sửa tệp mà vẫn để em dash, hook thất bại, Claude nhận phản hồi và thử lại. Sau 2–3 phiên, Claude sẽ thôi chèn ký tự này.

Lưu ý grep tìm chính xác ký tự em dash (U+2014) bằng `$'\xe2\x80\x94'`, cách viết theo cú pháp Bash để biểu diễn nó dưới dạng byte UTF-8. Nếu chỉ viết `grep -q '-'`, nó sẽ bắt cả dấu gạch nối thông thường và chạy trên mọi tệp. Một ký tự vô hình có thể phá hỏng toàn bộ logic.

### Hook 5. Giải phóng cổng 3000 trước `npm run dev` (PreToolUse, exit 0)

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE '(npm|pnpm|yarn)[[:space:]]+run[[:space:]]+dev' && { lsof -ti:3000 | xargs -r kill -9 2>/dev/null; echo 'Port 3000 freed before launch' >&2; }; exit 0"
          }
        ]
      }
    ]
  }
}
```

**Cách hoạt động:** Trước mỗi `npm run dev` (hoặc `pnpm`/`yarn`), hook diệt tiến trình đang dùng cổng 3000. Hook luôn thoát với mã 0 vì đây là bước chuẩn bị, không phải hạn chế. Cách này tránh tình huống Claude thấy “cổng đang được dùng”, tưởng mã có vấn đề rồi bắt đầu sửa, trong khi thực tế máy chủ cũ chỉ còn chạy sót lại.

### Hook 6. Tự động commit sau khi kiểm thử thành công (PostToolUse, exit 0)

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "INPUT=$(cat); CMD=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.command // empty') || exit 1; EXIT=$(printf '%s' \"$INPUT\" | jq -r '.tool_response.exit_code // empty') || exit 1; case \"$CMD\" in 'npm test'|'pnpm test'|'yarn test') ;; *) exit 0 ;; esac; [ \"$EXIT\" = \"0\" ] || exit 0; git diff --cached --quiet || { echo 'Existing staged changes: review manually' >&2; exit 0; }; git add -- src/orders.ts tests/orders.test.ts || exit 1; git diff --cached --quiet && exit 0; git commit -m 'tests: checkpoint after successful run'"
          }
        ]
      }
    ]
  }
}
```

**Cách hoạt động:** Đọc JSON stdin một lần, chỉ nhận đúng lệnh test thuần đã nêu. Chỉ tiếp khi `tool_response.exit_code` được cấp rõ là zero; bản cài không có trường đó thì bỏ commit, không đoán thành công. Thay `src/orders.ts`, `tests/orders.test.ts` bằng đúng tệp tác vụ sở hữu. Có staged thay đổi trước thì dừng để không gom việc ngoài. Add lỗi dừng, diff rỗng không commit, commit hook thường vẫn bật. Thử input đạt, lỗi và thiếu kết quả ngoại tuyến trước bật.

Phản mẫu: thêm `git commit --no-verify` vào mọi Edit. Giữ kiểm commit bật. Nếu kiểm kích tự động hóa đệ quy, thu trigger hoặc thiết kế lại kiểm đó; không mặc định bypass xác minh. Xem staged diff và ownership trước bật commit tự động.

### Hook 7. Chèn ngữ cảnh mới khi bắt đầu phiên (SessionStart với additionalContext)

```json
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo 'no-branch'); STATUS=$(git status --short 2>/dev/null | head -10); echo \"{\\\"additionalContext\\\":\\\"Current branch: $BRANCH. Uncommitted changes:\\n$STATUS\\\"}\""
          }
        ]
      }
    ]
  }
}
```

**Cách hoạt động:** Khi bắt đầu phiên, hook thu thập branch Git hiện tại và danh sách các thay đổi chưa commit, rồi trả về JSON có trường `additionalContext`. Claude Code chèn văn bản này vào system message đầu tiên, để Claude biết ngay từ đầu mình đang ở đâu và còn lại việc gì từ phiên trước.

<figure>
<blockquote>
<p>Dùng additionalContext cho thông tin Claude cần biết về trạng thái môi trường hoặc một thao tác vừa hoàn tất: trạng thái môi trường (branch hiện tại, mục tiêu triển khai, feature flag đang bật), quy tắc dự án có điều kiện (lệnh kiểm thử nào áp dụng cho tệp vừa sửa), dữ liệu bên ngoài (issue đang mở được giao cho bạn, kết quả CI gần đây).</p>
</blockquote>
<figcaption>— Anthropic, tài liệu tham khảo hooks, https://code.claude.com/docs/en/hooks</figcaption>
</figure>

Có thể mở rộng hook này để lấy commit mới nhất và mô tả của nó, trạng thái GitHub Actions, sprint hiện tại trong Linear/Jira và các issue đang mở của repository hiện tại.

## 3 cạm bẫy hook mà cứ hai người thì một người gặp

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

Điểm chính

Cạm bẫy đầu tiên: hook với exit 1 vẫn cho qua một cách im lặng, còn Claude bỏ qua lỗi. Thứ hai: công việc nặng trong hook bị dừng sau timeout 60 giây. Thứ ba: hook chỉ thấy các biến môi trường từ `~/.zshrc`, không thấy `.env` của dự án. Mỗi lỗi đều sửa được bằng một dòng, nhưng nếu không biết các cạm bẫy này, bạn có thể nghĩ rằng “hooks không hoạt động”.

</div>

### Cạm bẫy 1. Hook với exit 1 cho qua một cách im lặng

Đây là lỗi nguy hiểm nhất. Trong một tuần, tôi nghĩ bộ bảo vệ `.env` hoạt động, nhưng hóa ra trong hook tôi dùng `jq -r '.tool_input.file_path'` mà không kiểm tra null. Khi `tool_input` rỗng, jq thoát với mã 1. Claude coi exit 1 là “hook error, tiếp tục”, nên hành động vẫn diễn ra. Tôi không hiểu vì sao `.env` vẫn bị đụng đến. Chỉ khi tự chạy hook trong terminal với input rỗng, tôi mới thấy nguyên nhân.

**Cách sửa chỉ có một quy tắc:** bắt đầu mọi hook bằng cách kiểm tra null và kết thúc bằng `|| exit 0`. Khi có lỗi thoát ngoài dự kiến, hook sẽ thoát theo cách “an toàn” thay vì âm thầm cho qua.

```bash
# Safe start and end template:
FILE=$(jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
# ... your logic ...
exit 0   # or || exit 0 on one line
```

### Cạm bẫy 2. Hook chậm bị dừng do timeout

Tôi từng có hook chạy `eslint --fix` trên toàn bộ tệp sau Edit. Tệp lớn (1000+ dòng) mất 8–10 giây để xử lý. Timeout mặc định của hook trong Claude Code là 60 giây nên tôi nghĩ mình vẫn trong giới hạn. Nhưng khi có nhiều thao tác Edit trong một phút, hooks tích tụ và xếp hàng, một số bị bỏ qua. Tôi chỉ nhận ra khi nhật ký hiện “hook timed out”.

**Cách sửa:** nếu hook cần làm việc nặng, hãy chạy ngầm. Dùng `eslint --fix "$FILE" &` mà không đợi kết quả. Khi đó hook trả exit 0 trong vài mili giây, còn công việc thật vẫn chạy ở chế độ nền.

```bash
# Heavy operation in background:
npx eslint --fix "$FILE" &
exit 0
```

### Cạm bẫy 3. Hook không thể thấy các biến môi trường của dự án

Tôi nghĩ `process.env` trong hook sẽ khả dụng như trong ứng dụng Node.js. Không phải vậy. Hook chạy trong shell được tiến trình cha Claude Code khởi tạo và chỉ thấy những gì có trong `~/.zshrc` hoặc `~/.bashrc`, cùng những gì Claude Code truyền vào một cách rõ ràng. Nếu hook cần `OPENAI_API_KEY` từ `.env`, bạn phải tự đọc `.env`.

**Cách sửa:** đọc rõ ràng `.env` trong lệnh hook.

```bash
# Passing project env into hook:
cd "$PWD" && source .env && curl -H "Authorization: Bearer $OPENAI_API_KEY" ...
# Or with dotenv:
dotenv -e .env -- node my-validator.js
```

Điều này đặc biệt phiền trong monorepo, nơi mỗi package có tệp `.env` riêng. Khắc phục bằng cách truyền `PWD` và chạy rõ ràng `cd "$PWD"` ở đầu lệnh.

## Kiểm thử hooks trước khi commit như thế nào?

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

Điểm chính

Hook là một lệnh shell. Bạn có thể tự chạy nó trong terminal với stdin giả, kiểm tra mã thoát và stderr trước khi Claude Code chạy hook. Cách này tiết kiệm nhiều giờ gỡ lỗi và bảo vệ bạn khỏi những hook tự chặn hoạt động của chính nó.

</div>

**Kịch bản kiểm thử cơ bản.** Lấy lệnh hook từ JSON, mở terminal và truyền JSON mà Claude Code sẽ gửi:

```bash
# Test hook 1 (protect .env):
echo '{"tool_input":{"file_path":".env"}}' | bash -c '
FILE=$(jq -r ".tool_input.file_path // empty")
[ -z "$FILE" ] && exit 0
echo "$FILE" | grep -qE "(^|/)\.env(\.|$)" && {
  echo "BLOCKED" >&2
  exit 2
} || exit 0
'
echo "Exit code: $?"
```

Bạn sẽ thấy `BLOCKED` trong stderr và `Exit code: 2`. Nếu nhận được exit 0, regex không hoạt động; hãy sửa grep.

**Các tình huống cần kiểm tra:**

1.  **Trường hợp thành công** - tệp cần bị chặn. `.env` cho hook 1, `git push --force main` cho hook 3.
2.  **Trường hợp âm** - tệp không bị chặn. `README.md` cho hook 1, `git push origin feature-branch` cho hook 3.
3.  **Trường hợp biên** - input rỗng, giá trị null, ký tự bất ngờ. Hook phải trả exit 0 hoặc thất bại với lỗi rõ ràng, không được âm thầm cho qua.

**Nơi đặt hooks thử nghiệm.** Không đặt trong `.claude/settings.json`, mà trong `.claude/settings.local.json` (không nằm trong git). Thêm hooks mới ở đó dưới `PostToolUse`, dùng `echo` thay cho hành động thật, kiểm tra xem chúng có kích hoạt không rồi chuyển sang `settings.json` dùng trong production.

```json
// Experiment in settings.local.json - writes to /tmp/hook-debug.log:
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"[hook fired] $(date) $(jq -r '.tool_input.file_path // \"?\"')\" >> /tmp/hook-debug.log"
          }
        ]
      }
    ]
  }
}
```

Sau 5–10 lần chỉnh sửa trong Claude Code, mở `/tmp/hook-debug.log` để xem những tệp nào đã kích hoạt hook. Nếu tệp trống, matcher không phù hợp; hãy sửa regex.

## Tiếp theo là gì?

<div class="aside" data-semantic="key-takeaway" role="note" aria-label="Key takeaways">

Điểm chính

Bắt đầu với hooks PostToolUse có exit 0; chúng an toàn nhất và không chặn gì cả. Sau một tuần, thêm hook PreToolUse đầu tiên với exit 2 để bảo vệ `.env` hoặc ngăn force-push. Sau hai tuần, `CLAUDE.md` sẽ rút từ bản thỏa thuận 200 dòng thành mô tả ngữ cảnh ngắn 50–80 dòng, còn kiểm sự kiện chọn chuyển sang hook, giới hạn quyền cứng được cưỡng chế riêng bằng permissions, sandbox và bảo vệ remote.

</div>

5 bước cho tuần tới:

1.  **Grep `CLAUDE.md`** để tìm những từ như “never,” “forbidden,” “always” và “must”. Mỗi quy tắc như vậy là ứng viên cho hook. Nếu một quy tắc mang tính mệnh lệnh, tệp văn bản sẽ không thực thi nó.
2.  **Bắt đầu với PostToolUse + exit 0.** Đây là cách an toàn nhất: chúng thay đổi điều gì đó nhưng không chặn hành động nào. Prettier (hook 2), dừng máy chủ dev (hook 5), tự động commit sau khi kiểm thử (hook 6).
3.  **Chỉ khi tự tin mới thêm PreToolUse + exit 2 đầu tiên.** Ví dụ, bảo vệ `.env` (hook 1) hoặc ngăn force-push (hook 3). Đây là lệnh chặn cứng; một lỗi trong lệnh grep có thể chặn chính bạn.
4.  **Tự kiểm thử hooks trước khi commit** (xem phần bên trên). Cách này tiết kiệm nhiều giờ gỡ lỗi.
5.  **Dùng `settings.local.json` cho hooks thử nghiệm.** Thêm hooks mới vào đó và dùng `echo` thay vì hành động thật.

Sau khi cấu hình hooks, bước tiếp theo trong cụm hướng dẫn là [cách hoàn tác thay đổi trong Claude Code](https://ailoot.ai/blog/vi/claude-code-undo-changes/), phòng trường hợp hook để lọt điều gì nguy hiểm. Nếu muốn Claude nhớ cấu trúc dự án giữa các phiên, hãy xây dựng [Second Brain trong Claude Code](https://ailoot.ai/blog/vi/claude-code-second-brain/) (thư mục `business/` + kho kiến thức được liên kết). Và để đặt tên cho các skill của dự án để tái sử dụng, hãy xem [Claude Code Skills](https://ailoot.ai/blog/vi/claude-code-skills-library/).

<div class="section" role="doc-bibliography" aria-label="Sources">

### Nguồn

- [Tài liệu tham khảo Claude Code Hooks - tài liệu chính thức của Anthropic](https://code.claude.com/docs/en/hooks)
- [Tài liệu Claude Code Memory - về CLAUDE.md và system prompt](https://code.claude.com/docs/en/memory)
- [Tự động hóa quy trình làm việc bằng hooks - hướng dẫn chính thức của Anthropic kèm ví dụ](https://code.claude.com/docs/en/hooks-guide)

</div>





Nếu cần truy cập các mạng nơ-ron qua Ailoot, hãy xem [Ailoot](https://ailoot.ai/).

## Câu hỏi thường gặp

### Vì sao hook phải đọc JSON stdin chỉ một lần?

Stdin là luồng: jq đầu tiêu thụ, lần sau gặp EOF. Lưu đầu vào một lần rồi lấy trường lệnh và kết quả từ giá trị đó. Kết quả thiếu hoặc lạ coi là chưa xác minh và bỏ auto-commit.

### Hook regex ví dụ có tạo ranh giới bảo mật không?

Không. Chúng kiểm sự kiện và chuỗi nhận diện được. Shell redirection, alias, script, MCP hay đường ghi khác có thể vượt; dùng credential hạn chế, quyền tệp, sandbox và nhánh bảo vệ cho giới hạn cứng.


## Ailoot · Công ty và danh mục dự án của công ty

Ailoot dựa trên kinh nghiệm vận hành tích lũy, cơ sở khách hàng lớn, dịch vụ ổn định và việc kiểm soát KPI liên tục.

Công ty và danh mục dự án của công ty: hàng triệu khách hàng; doanh số hàng tỷ mỗi tháng.

Ailoot áp dụng kinh nghiệm vận hành này để cung cấp quyền truy cập API vào các tính năng AI trong sản phẩm của khách hàng, tác tử, ứng dụng và quy trình tự động hóa.
