Learn > Claude > Claude Code: permission mode & hooks

Claude Code: permission mode & hooks

Sáu permission mode của Claude Code và classifier của auto mode; hook PreToolUse/PostToolUse — dựng 4 bước, exit code, JSON quyết định, và 2 hook thực chiến.

  • Permission mode = chốt MỘT LẦN mức tin tưởng thay vì duyệt từng thao tác. Sáu mode: manual (default — chỉ reads, còn lại hỏi), accept edits (reads + file edits + common file system bash commands), plan (read-only, chỉ đề xuất), auto (chấp nhận mọi thứ nhưng có classifier model riêng review từng action TRƯỚC khi chạy), dontAsk (chỉ tool pre-approved, còn lại auto-deny không prompt), bypassPermissions (bỏ mọi check = dangerously-skip-permissions, chỉ dùng trong container/VM cô lập). Shift+Tab cycle qua manual → accept edits → plan → auto; status bar luôn hiện mode hiện tại.
  • Auto mode: classifier gác INTENT — chặn production deploy/migration, force push, pipe code tải về vào shell, gửi sensitive data ra endpoint ngoài, phá huỷ file; cho qua local edit, cài dependency từ lock file, read-only request, push branch của chính mình. Nhưng classifier KHÔNG kiểm correctness: refactor auth mà viết auth hỏng thì vẫn cho qua (hỏng ≠ nguy hiểm) → ghép auto mode với Stop hook chạy test. Auto gác intent TRƯỚC mỗi action, Stop hook gác correctness SAU khi xong. dontAsk dành cho lần chạy không người trực (CI, scheduled job, batch đêm) — pipeline chạy tiếp thay vì treo chờ một cái duyệt không ai bấm.
  • Hook: chạy command của riêng bạn NGAY TRƯỚC (PreToolUse) hoặc NGAY SAU (PostToolUse) khi Claude gọi một tool. PreToolUse CHẶN được (cho phép, hoặc block + gửi error message ngược lại cho Claude); PostToolUse KHÔNG chặn được (tool đã chạy rồi) — chỉ làm follow-up (VD format file vừa sửa) + feedback cho Claude. matcher chỉ định tool bị nhắm (VD "Write|Edit", dấu | = OR). Khai báo trong settings.json (global ~/.claude / project / project-local) hoặc gõ /hooks. Câu chốt: Pre = kiểm soát Claude ĐƯỢC LÀM GÌ; Post = tăng cường thứ ĐÃ LÀM.
  • Dựng một hook = 4 bước: (1) chọn Pre/Post, (2) xác định tool cần canh (matcher), (3) viết command nhận JSON tool call qua standard input (stdin), (4) dùng exit code báo allow/block. JSON gồm session_id, transcript_path, hook_event_name, tool_name, tool_input. Exit code 0 = success (stdout JSON được parse); exit code 2 = blocking error ở gần như mọi event (stderr feed ngược cho Claude làm context, giải thích vì sao bị chặn); mọi exit code khác là non-blocking — ⚠️ exit 1 TRÔNG như lỗi nhưng KHÔNG chặn, Claude vẫn chạy command. Exit 2 chặn được cả Stop ("chưa xong đâu"); PostToolUse chặn thì muộn (tool đã chạy) nhưng vẫn feed text về được; vài event bỏ qua chặn hoàn toàn (Notification, SessionStart). Với 3 event SessionStart / UserPromptSubmit / UserPromptExpansion, plain text ở stdout được cộng thẳng vào context. Ví dụ chặn đọc .env (bản hiện hành của khoá) chỉ cover tool Read qua tool_input.file_path; mỗi tool có input shape khác nhau (Grep gửi pattern + path là thư mục, Bash gửi command) nên check file_path KHÔNG bắt được grep/cat .env — muốn phủ đồng đều thì dùng permissions.deny "Read(**/.env)".
  • Gotcha bảo mật hook: docs khuyến nghị dùng absolute path (KHÔNG relative) cho script hook → giảm path interception + binary planting attack. Nhưng absolute path khó share (mỗi máy để project ở thư mục khác). Cách giải: settings.example.json chứa placeholder $PWD; npm run setup chạy scripts/init-claude.js thay $PWD bằng absolute path máy bạn rồi copy & đổi tên thành settings.local.json — vừa share được (commit template) vừa giữ absolute path.
  • Hai hook thực chiến giải điểm yếu AI-assisted dev: (1) TypeScript type-checking hook (PostToolUse) chạy tsc --noEmit sau mỗi edit → capture type error → feed lại Claude để sửa call site nó quên (VD đổi signature ở schema.ts nhưng quên main.ts); typed language dùng type checker, untyped thì dùng automated tests. (2) Query duplication hook dùng Agent SDK khởi chạy một Claude THỨ HAI review file trong ./queries, phát hiện query trùng → feedback bắt Claude tái dùng hàm có sẵn (VD getPendingOrders). Query hook nặng (tốn API mỗi edit) → chỉ monitor thư mục quan trọng. Nguyên tắc: compiler/linter = deterministic gate; AI review = probabilistic gate cho việc không có ground truth.
  • Vì sao cần hook: luật viết trong CLAUDE.md là một REQUEST, không phải GUARANTEE — Claude "thường" nghe, mà trong lần chạy dài không ai canh thì "thường" là không đủ. Hook là deterministic code chạy tại một điểm cố định trong loop nên nó BẢO ĐẢM hành vi, biến luật từ "Claude thường nghe" thành "Claude không skip được".
  • PreToolUse trả quyết định bằng cách in JSON ra stdout rồi exit 0. Field permissionDecision nhận allow (cho qua) / deny (chặn) / ask (đẩy về user tự quyết); còn defer chỉ dùng cho non-interactive -p run nơi tiến trình gọi tạm dừng tool rồi resume. ⭐ Nước đi ít người biết mà mạnh hơn: updatedInput để VIẾT LẠI call thay vì chặn — VD guardrail canh tool Bash nhận diện pattern sk_live_ rồi thay secret bằng placeholder: lệnh vẫn chạy, việc vẫn xong, nhưng secret không đi qua (redact thay vì block). Catch: updatedInput thay TOÀN BỘ input object nên phải echo lại các field không đổi, không thì mất.
  • Claude Code bắn 33 hook event (docs 08/2026); nhúm hay dùng: PreToolUse (primitive cưỡng chế, dừng được việc trước khi xảy ra), PostToolUse (auto-format/lint sau tool call thành công), Stop (Claude muốn kết thúc turn — từ chối được, "chưa xong đâu"; bản song sinh SubagentStop), PreCompact/PostCompact, InstructionsLoaded (CLAUDE.md hoặc rule file được load — audit thứ gì thật sự vào context), SessionStart (mồi môi trường; source startup nếu chỉ muốn chạy lúc khởi động mới). ⚠️ Muốn bơm lại context SAU compaction thì KHÔNG dùng PostCompact — dùng SessionStart với matcher compact, đó mới là cái đưa output ngược vào hội thoại (VD in summary các file đang làm để Claude bắt nhịp thay vì bắt đầu lạnh).
  • Ngoài Pre/PostToolUse, 7 event vòng đời hay dùng nhất (không phải toàn bộ danh sách 33): Notification (Claude xin quyền dùng tool, hoặc idle 60 giây), Stop (Claude trả lời xong), SubagentStop (subagent = "Task" trong UI chạy xong), PreCompact (trước compact thủ công/tự động), UserPromptSubmit (user submit prompt, TRƯỚC khi Claude xử lý), SessionStart (bắt đầu/resume session), SessionEnd (session kết thúc). Cấu trúc stdin đổi theo 2 trục: theo loại hook (Stop chỉ có hook_event_name + stop_hook_active, không có tool_name/tool_input) và theo tool được gọi (tool_input khác nhau; PostToolUse còn có thêm tool_response). Mẹo soi input: gắn tạm hook matcher "*" với 'jq . > post-log.json' để ghi input ra file mà xem shape thật.

TL;DRPermission mode chốt MỘT LẦN mức tin tưởng thay vì duyệt từng thao tác — auto mode có classifier gác intent (chặn production deploy, force push). Hook là chỗ biến luật thành bảo đảm: PreToolUse chặn được thao tác trước khi chạy, PostToolUse chạy sau. Luật trong CLAUDE.md là một request; hook mới là guarantee.

Phần 3/7 của ghi chú khoá Claude Code in Action — đề thi thử 219 câu (chấm điểm + giải thích) nằm ở tab "Đề thi thử" trên trang tổng quan.

Permission mode — chốt MỘT LẦN Claude được chạy gì

Các kỹ thuật tự chủ ở trên chỉ có nghĩa nếu Claude không phải dừng hỏi phép mỗi thao tác. Permission mode cho bạn quyết định một lần mức tin tưởng cho cả phiên làm việc, thay vì duyệt từng prompt một. Ba mode quen thuộc (manual, accept edits, plan) lo phần việc kè kè bên cạnh; phần hands-off thật sự nằm ở mode auto.

⚡ Cập nhật 14/08/2026 — auto giờ là mode MẶC ĐỊNH, không còn manual. Khoá (và mọi tài liệu trước 08/2026) dạy rằng session mới khởi động ở Manual. Từ 14/08/2026 điều đó đã đổi: trên gói Pro, Max và Team, chạy ở terminal hoặc VS Code extension, mode khởi động sẵn (built-in default) là auto — tức classifier duyệt thay bạn ngay từ prompt đầu. Cần v2.1.228+ (macOS/Linux/WSL) hoặc v2.1.233+ (Windows native); bản cũ hơn vẫn khởi động ở Manual.

Vẫn khởi động ở default (Manual) trong các trường hợp: gói Enterprise hoặc API key của Claude Console · claude -pAgent SDK · Bedrock / Google Cloud Agent Platform / Microsoft Foundry / Claude Platform on AWS / apps gateway · settings đặt disableAutoMode: "disable" · session đầu tiên sau khi cài/nâng cấp, hoặc khi feature-flag fetching bị tắt.

⚠️ Bẫy cấu hình: giá trị "auto" đặt trong .claude/settings.json của project KHÔNG có hiệu lực (và khi đó Claude Code dùng built-in default, bỏ luôn defaultMode~/.claude/settings.json). Muốn ghim auto thì đặt ở user settings; muốn kéo về Manual cho cả máy thì "defaultMode": "default". Kèm theo: classifier call của auto mode không còn tính vào usage limit trên các gói này.

Sáu permission mode

Mỗi mode kẻ một đường khác nhau giữa chạy thẳngphải xin phép:

Mode (tên trong settings) Chạy không hỏi Phần còn lại
Manual (default) Chỉ reads Mọi thứ khác hỏi trước
Accept edits (acceptEdits) reads + file edits + common file system bash commands Còn lại hỏi. Hợp khi iterate rồi review sau
Plan (plan) reads — cộng thêm lệnh được classifier duyệt khi auto mode khả dụng (docs 08/2026) Không sửa gì — chỉ nghiên cứu & đề xuất thay đổi
Auto (auto) Chấp nhận mọi thứ, nhưng có classifier model riêng review từng action TRƯỚC khi chạy
Don't ask (dontAsk) Chỉ tool đã pre-approve Còn lại auto-deny, không prompt
Bypass permissions (bypassPermissions) Bỏ qua mọi check (tương đương cờ --dangerously-skip-permissions) CHỈ chạy trong container/VM cô lập

Cycling bằng Shift+Tab: khỏi nhớ lệnh cho từng mode — bấm Shift+Tab để luân phiên qua các mode hằng ngày. Status bar dưới màn hình luôn hiện mode đang dùng, liếc xuống là biết Claude đang được phép làm gì.

⚡ Vòng cycle đã đổi (docs 08/2026). Khoá dạy manual → accept edits → plan → auto. Bản hiện hành: đang ở auto thì phím đầu tiên nhảy về default, rồi vòng chạy defaultacceptEditsplan. Mode tuỳ chọn chèn vào SAU plan, thứ tự bypassPermissions trước, auto cuối — bật cả hai thì phải đi qua bypassPermissions mới tới auto. dontAsk không bao giờ nằm trong cycle (chỉ set bằng --permission-mode dontAsk). Nhãn status bar: ⏸ manual mode on (xám) · ⏵⏵ accept edits on · ⏸ plan mode on · ⏵⏵ auto mode on · ⏵⏵ don't ask on · ⏵⏵ bypass permissions on.

Auto mode chạy thế nào

Auto là mode hands-off: Claude tự chạy, nhưng trước mỗi action có một classifier model riêng xem xét. Classifier gác INTENT (ý định) — nó soi những nước đi leo thang vượt quá thứ bạn thật sự yêu cầu.

Kiểu hành vi classifier được thiết kế để CHẶN:

  • Production deploysmigrations.
  • Force push, hoặc pipe code tải về thẳng vào shell.
  • Gửi sensitive data ra endpoint bên ngoài.
  • Phá huỷ file đang tồn tại của phiên làm việc.

Và cho qua phần việc thường ngày: local edits trong project của bạn, cài dependency từ lock file, các read-only request, và push lên branch của chính bạn.

Cái classifier KHÔNG làm được

Classifier kiểm intent, KHÔNG kiểm correctness (tính đúng). Nó không biết code có chạy được hay không. Nhờ Claude refactor authentication mà nó viết authentication hỏng → classifier vẫn cho qua, vì hỏng thì không nguy hiểm.

→ Vì thế auto mode đi kèm một Stop hook chạy test. Hai lớp bổ trợ nhau:

Gác cái gì Gác lúc nào
Auto mode Intent — Claude đang định làm gì TRƯỚC mỗi action, trong lúc chạy
Stop hook Correctness — code có thật sự chạy không SAU khi Claude làm xong

⚠️ Freshness: guardrail của auto mode vẫn đang tiến hoá — block list & allow list đổi theo thời gian, tra docs để lấy bản hiện hành, đừng học thuộc như luật cứng.

dontAsk — cho lần chạy không người trực

Don't ask là nước đi đúng khi không có người ngồi duyệt prompt: CI pipeline, scheduled job, batch chạy đêm. Chỉ tool pre-approved được phép; ngoài danh sách là auto-deny, không hỏi. Đó chính là điểm cốt: pipeline chạy tiếp thay vì treo chờ một cái duyệt mà không ai ở đó để bấm.

Khớp mode với việc

  • Auto = mode hands-off — classifier gác intent trước mỗi action, Stop hook gác correctness sau đó.
  • Don't ask = pipeline không người trực.
  • Bypass permissions = chỉ trong container/VM cô lập.

📌 Chi tiết đã verify ở docs (rà lại 08/2026) — hữu ích khi cấu hình thật:

  • Đặt mode mặc định bằng khoá defaultMode trong settings file; default hiển thị nhãn "Manual" trong CLI/extension và nhận alias manual.
  • Tổ chức chặn được hai mode nguy hiểm: permissions.disableAutoModepermissions.disableBypassPermissionsMode = "disable" (đặt ở managed settings thì user không override được).
  • dontAsk deny cả AskUserQuestion — hợp lý, vì mode này tồn tại cho lúc không có ai trả lời.
  • bypassPermissions vẫn promptnăm chỗ, không phải hai: (1) ask rule tường minh; (2) connector tool bị tổ chức đặt ask; (3) MCP tool gắn _meta["anthropic/requiresUserInteraction"] (cần v2.1.199 trở lên); (4) xoá thư mục gốc/home (rm -rf /, rm -rf ~) — chốt an toàn cuối (circuit breaker), và từ v2.1.208 nó nổ cả khi lệnh xoá nấp trong $(...), backtick hay <(...); (5) hai chốt cross-session messaging (isolatePeerMachines, và tin nhắn inbound từ session khác khi chưa có luật crossSessionInbound nào áp vào).

Áp dụng thực tế: đây đúng là bài least-privilege quen thuộc, chỉ đổi chủ thể từ nhân sự sang agent. Cách tôi phân vai: plan khi chưa tin cách hiểu bài của Claude (bắt "xác nhận rồi mới làm" — hợp khách Nhật), accept edits cho mạch sửa nhỏ trong repo tôi review từng commit, auto khi việc rõ và tôi có test làm lưới, dontAsk cho automation chạy đêm. Ba chốt tôi ghim: (1) auto KHÔNG phải bypass — nhầm hai cái này là nhầm chí mạng: một cái có classifier gác từng action, cái kia không còn gì gác; bypass chỉ sống trong container. (2) Câu "classifier gác intent, không gác correctness"ranh giới trách nhiệm rõ nhất tôi từng thấy phát biểu về AI guardrail — nó nói thẳng rằng an toàn ≠ đúng, nên CI/test vẫn là của tôi, không giao được cho mode. (3) Cặp auto + Stop hook chính là mô hình two-gate tôi vẫn dùng trong delivery: một cửa trước (duyệt hành động) và một cửa sau (nghiệm thu kết quả) — bỏ cửa nào cũng thủng. Và nhớ: permission là thứ Claude Code cưỡng chế, còn CLAUDE.md chỉ thuyết phục model — cùng bài học đã gặp ở mục "luật cứng thì đẩy sang hook".

Hooks — chạy lệnh của bạn TRƯỚC/SAU khi Claude gọi tool

Hook cho phép bạn chạy command của riêng mình ngay trước hoặc ngay sau khi Claude định thực thi một tool. Cực hữu ích để tự động hoá workflow: chạy code formatter sau khi Claude sửa file, chạy test khi file đổi, hoặc chặn truy cập file cụ thể.

Vì sao cần hook — một câu: viết luật trong CLAUDE.md là một request (lời đề nghị), không phải guarantee (bảo đảm). Ghi "always format after editing" thì Claude thường nghe. Thường. Nhưng trong một lần chạy dài bạn không ngồi canh, "thường" là không đủ. Hook là deterministic code chạy tại một điểm cố định trong loop → nó bảo đảm hành vi thay vì hy vọng. Nó biến luật từ "Claude thường nghe" thành "Claude KHÔNG SKIP được".

Hook chen vào luồng ở đâu

Nhớ lại luồng bình thường khi bạn nói chuyện với Claude Code:

  1. Query của bạn gửi tới model kèm tool definitions.
  2. Claude quyết dùng tool → trả về một formatted response.
  3. Claude Code thực thi tool đó và trả kết quả.

Hook chen vào ngay quanh bước 3 — cho bạn chạy code just-before hoặc just-after lúc tool được thực thi. Hai loại phổ biến nhất (còn loại khác ở lesson sau):

  • PreToolUse — chạy TRƯỚC khi tool được gọi.
  • PostToolUse — chạy SAU khi tool được gọi.

Khai báo hook ở đâu

Hook định nghĩa trong các Claude settings file — đúng bộ ba vị trí quen thuộc như CLAUDE.md:

File Phạm vi Chia sẻ team?
~/.claude/settings.json Global — mọi project (máy cá nhân)
.claude/settings.json Project ✅ commit, chia sẻ team
.claude/settings.local.json Project — cá nhân ❌ không commit

Viết tay trong file, hoặc dùng lệnh /hooks ngay trong Claude Code.

⚠️ Đừng lẫn: hook khai trong settings.json, KHÁC với chỉ thị tự nhiên đặt trong CLAUDE.md. Cùng bộ ba vị trí (global / project / project-local) nhưng khác file, khác vai: CLAUDE.mdlời dặn cho model; settings.jsoncấu hình tất định cho harness.

PreToolUse — chạy TRƯỚC, chặn được

"PreToolUse": [
  {
    "matcher": "Read",
    "hooks": [
      { "type": "command", "command": "node /home/hooks/read_hook.js" }
    ]
  }
]

matcher chỉ định loại tool nào bị nhắm (ở đây là Read). Trước khi tool Read chạy, command được gọi. Command nhận được chi tiết về tool call Claude định thực hiện, rồi bạn có 2 lựa chọn cơ bản:

  • Cho phép thao tác chạy bình thường.
  • Block tool call và gửi một error message ngược lại cho Claude.

(Bản đầy đủ có 3 quyết địnhallow / deny / ask — khi hook trả JSON thay vì chỉ dùng exit code, kèm khả năng viết lại call bằng updatedInput. Xem mục "PreToolUse trả quyết định bằng JSON" phía dưới.)

PostToolUse — chạy SAU, KHÔNG chặn được

"PostToolUse": [
  {
    "matcher": "Write|Edit",
    "hooks": [
      { "type": "command", "command": "node /home/hooks/edit_hook.js" }
    ]
  }
]

matcher "Write|Edit" nhắm nhiều tool cùng lúc (Write HOẶC Edit — dấu | như regex). Vì tool call đã xảy ra rồi, PostToolUse KHÔNG block được thao tác. Nhưng nó vẫn:

  • Chạy follow-up operations (VD format lại file vừa sửa).
  • Cấp feedback thêm cho Claude về tool use đó.

Dựng một hook — 4 bước

Hook chặn/kiểm soát tool call trước hoặc sau khi nó thực thi → kiểm soát chi tiết Claude được và không được làm gì. Tạo một hook gồm 4 bước:

  1. Chọn PreToolUse hay PostToolUse — Pre ngăn được tool call chạy; Post chạy sau khi tool đã dùng.
  2. Xác định loại tool call cần canh — chỉ rõ đúng tool nào sẽ kích hoạt hook (chính là matcher).
  3. Viết một command nhận tool call — command này nhận JSON data về tool call đề xuất qua standard input (stdin).
  4. (Nếu cần) command trả feedback cho Claudeexit code của command báo Claude allow hay block thao tác.

💡 Muốn biết chính xác tool nào đang có trong setup hiện tại (để đặt matcher)? Hỏi thẳng Claude liệt kê — đặc biệt cần thiết vì danh sách tool thay đổi khi bạn thêm custom MCP server.

Dữ liệu tool call — JSON qua stdin

Khi hook command chạy, Claude gửi một khối JSON qua standard input mô tả tool call đề xuất:

{
  "session_id": "2d6a1e4d-6...",
  "transcript_path": "/Users/sg/...",
  "hook_event_name": "PreToolUse",
  "tool_name": "Read",
  "tool_input": {
    "file_path": "/code/queries/.env"
  }
}

Command đọc JSON từ stdin → parse → quyết định allow/block dựa trên tool_nametool_input. (Ví dụ trên: Claude định Read file /code/queries/.env.)

Exit code — cho hook KHÔNG nói JSON

Không phải hook nào cũng cần trả JSON; hook đơn giản thì exit code là đủ. Ba con số đáng nhớ:

Exit code Ý nghĩa
0 Success. Nếu stdout là JSON → Claude parse. Plain text thì bị bỏ qua ở đa số event, NGOẠI TRỪ SessionStart, UserPromptSubmit, UserPromptExpansion — ở ba event này plain text được cộng thẳng vào context (đây chính là thứ làm hook giữ-state chạy được)
2 Blocking error. stderr được feed ngược cho Claude làm context — đây là exit code chặn ở gần như mọi nơi
còn lại Non-blocking. stderr chỉ được log, Claude chạy tiếp

⚠️ Bẫy kinh điển — exit code 1: nhìn thì có vẻ là lỗi, nhưng nó KHÔNG chặn — Claude vẫn chạy command. Muốn dừng thật thì exit 2, không phải 1.

Vài nếp gấp nữa:

  • 2 chặn được cả Stop — đó chính là cách bạn nói với Claude "chưa xong đâu".
  • PostToolUse chạy SAU khi tool đã chạy → chặn ở đó là muộn, không cứu được tool call; nhưng vẫn feed text ngược cho Claude được.
  • Vài event bỏ qua chuyện chặn hoàn toàn (theo khoá: Notification, SessionStart) — chúng hiện stderr rồi chạy tiếp, bất kể exit code.

PreToolUse trả quyết định bằng JSON — permissionDecision

Sức mạnh thật nằm ở PreToolUse, vì nó chặn được tool call trước khi chạy. Cách "nói chuyện ngược" với Claude: in JSON ra stdout rồi exit 0. Field then chốt là permissionDecision, nhận 3 giá trị:

Giá trị Nghĩa
allow Cho call đi qua
deny Chặn call
ask Đẩy về cho user tự quyết

Về kỹ thuật còn giá trị thứ tư là defer, nhưng nó chỉ áp cho non-interactive -p run — nơi một tiến trình gọi tạm dừng tool rồi resume sau. Hiếm khi phải dùng.

Khuôn JSON:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "...",
    "updatedInput": {
      "command": "..."
    }
  }
}

⭐ Redact thay vì block — updatedInput

Để ý field updatedInput: thay vì chặn một call, bạn VIẾT LẠI nó. Đây là cách bóc một secret ra khỏi lệnh bash mà vẫn cho lệnh chạy.

⚠️ Catch: updatedInput thay TOÀN BỘ input object → phải echo lại những field bạn không đổi, không thì mất sạch.

Ví dụ thực chiến: một PreToolUse guardrail canh tool Bash. matcher chọn tool cần canh, thêm mệnh đề if (tuỳ chọn) để thu hẹp về đúng một loại lệnh. Claude được yêu cầu chạy một lệnh có secret trông như thật; hook chặn lại giữa đường, nhận diện pattern sk_live_, và thay bằng placeholder trước khi lệnh kịp thực thi.

Lệnh vẫn chạy. Việc vẫn xong. Nhưng secret không bao giờ đi qua. Đó là khác biệt giữa blockingredacting — và hook làm được điều đó mỗi lần, không sót lần nào.

Ví dụ: chặn Claude đọc file .env

Use case kinh điển: ngăn Claude đọc file nhạy cảm như .env. Bản hiện hành của khoá chỉ chặn tool Read — matcher "Read", hook đọc tool_input.file_path:

  • Path an toàn → exit 0 (cho qua).
  • Path chứa .env → ghi lý do ra stderr + exit 2 → Claude bị chặn và nhận được lời giải thích.
"PreToolUse": [
  {
    "matcher": "Read",
    "hooks": [ { "type": "command", "command": "node $PWD/hooks/read_hook.js" } ]
  }
]
// hooks/read_hook.js
process.stdin.setEncoding("utf8");
let input = "";
process.stdin.on("data", (d) => (input += d));
process.stdin.on("end", () => {
  const toolArgs = JSON.parse(input);
  const readPath = toolArgs.tool_input?.file_path || "";
  if (readPath.includes(".env")) {
    console.error("You cannot read the .env file"); // stderr -> Claude thấy
    process.exit(2);                                // exit 2 = chặn
  }
  process.exit(0);
});

⚠️ Vì sao chỉ Read? (điểm dễ nhầm — video cũ khác text mới) Video của khoá bản cũ có kèm Grep; bản text hiện hành đã bỏ, chỉ còn Read. Lý do: mỗi tool gửi input shape khác nhauRead gửi { file_path }; Grep gửi { pattern, path } với paththư mục tìm kiếm, không phải file; Bash gửi { command }. Nên một check trên file_path bắt được Read nhưng không chặn được grep API_KEY toàn project hay cat .env trong Bash. Muốn phủ đủ: hoặc viết matcher riêng cho từng tool (soi đúng field của nó), hoặc — gọn hơn — dùng permissions.deny với rule như "Read(**/.env)" (áp đồng đều trên mọi tool). Xem hooks guide.

Áp dụng thực tế: đây là lúc "hook = guardrail tất định" trở nên cụ thể: chốt chặn KHÔNG nằm ở việc dặn Claude ("đừng đọc .env") — mà ở một script bên ngoài đọc tool_input.file_path rồi exit 2. Model có thể lơ lời dặn, nhưng không vượt được exit code. Hai điểm tôi luôn soi khi review một PreToolUse guard: (1) input shape của mỗi tool khác nhau → matcher = attack surface. Một hook check file_path chỉ chặn Read; grep API_KEY toàn project (Grep dùng path = thư mục) hay cat .env (Bash dùng command) vẫn lọt. Vì thế với thứ phải chặn tuyệt đối như secret, tôi không cố ghép nhiều matcher hook (dễ sót một shape) mà dùng permissions.deny "Read(**/.env)" — rule áp đồng đều mọi tool, hook chỉ là lớp bổ trợ/feedback; (2) thông điệp stderr phải nói rõ lý do để Claude tự đổi hướng thay vì thử lại mù. Với khách Nhật, "chặn ngay tại tầng harness + log lại" là câu trả lời gọn cho "làm sao đảm bảo AI không chạm dữ liệu mật?" — không phải niềm tin, mà là cơ chế.

Ứng dụng thực tế của hook

  • Code formatting — tự format file sau khi Claude sửa.
  • Testing — tự chạy test khi file thay đổi.
  • Access control — chặn Claude đọc/sửa file cụ thể.
  • Code quality — chạy linter/type checker rồi feedback cho Claude.
  • Logging — ghi lại file nào Claude access/modify.
  • Validation — kiểm naming convention / coding standard.

⭐ Insight cốt lõi: PreToolUse = kiểm soát Claude ĐƯỢC LÀM GÌ (control what Claude can do); PostToolUse = tăng cường thứ Claude ĐÃ LÀM (enhance what Claude has done).

Áp dụng thực tế: hook là guardrail tất định — nó luôn chạy, không phụ thuộc Claude có "nhớ" hay không (khác chỉ thị mềm trong CLAUDE.md mà model có thể lơ). Cách tôi phân vai với hai loại: PreToolUse là chốt bảo vệ — vì chỉ nó block được, tôi đặt ở đây mọi thứ phải chặn tại nguồn: cấm đọc .env/secret, cấm sửa file sinh tự động (bài học repo này: đừng để build churn lọt vào commit), cấm rm -rf bậy. PostToolUse là chốt chuẩn hoá — nó không cứu được thao tác đã lỡ, nên đừng đặt "bảo vệ" ở đây; hợp nhất là auto-format/lint/typecheck + log để mọi thay đổi của Claude ra đúng chuẩn team mà không ai phải nhớ chạy tay. Về mặt Delivery, đây là "policy-as-code": thay vì viết một tài liệu "nhớ format trước khi push" rồi cầu mong đội tuân, tôi mã hoá luật vào settings.json commit chung — máy enforce, người khỏi cãi.

Gotchas — bảo mật hook & chia sẻ settings

Sau khi chạy npm run setup, trong .claude/hai file settings: settings.example.json (template, commit) và settings.local.json (sinh ra, cá nhân không commit). Vì sao tách ra? Do một khuyến nghị bảo mật hook trong docs Claude Code.

Khuyến nghị: dùng absolute path (KHÔNG dùng relative path) cho script hook. Lý do bảo mật: giảm path interceptionbinary planting attack — kẻ xấu đặt một script/binary giả trùng tên vào một thư mục (trên PATH hoặc thư mục tương đối) để hook chạy nhầm nó. Absolute path khoá cứng đúng file cần chạy.

Nhưng absolute path làm khó việc share settings.json: đường dẫn tuyệt đối tới script trên máy bạn khác máy người khác (mỗi người để project ở thư mục khác nhau).

Cách project này giải: settings.example.json đặt placeholder $PWD ở chỗ đường dẫn script. Khi chạy npm run setup: (1) cài dependencies, (2) chạy scripts/init-claude.js — script này thay $PWD bằng absolute path tới project trên máy bạn, copy settings.example.json rồi đổi tên thành settings.local.json.

→ Nhờ vậy vừa share được settings (commit template có $PWD), vừa giữ absolute path như khuyến nghị.

Áp dụng thực tế: đây là pattern "template + machine-specific generator" — commit cái khuôn (settings.example.json), sinh cái thực thi theo từng máy (settings.local.json, không commit — đúng cặp CLAUDE.local.md/settings.local.json). Điểm architect: bảo mật (absolute path) và tính chia-sẻ-được kéo ngược nhau, init-claude.js là cầu nối. Và bản thân generator ấy phải portable: khi tôi chạy trên Windows, $PWD = D:\... (dấu \ là escape trong JSON ⇒ vỡ JSON.parse) lại còn có dấu cách trong path ⇒ lệnh node <path> đứt giữa chừng — phải forward-slash + quote path mới chạy. Bài học: một khuyến nghị bảo mật đúng vẫn có thể tạo bug vận hành nếu bỏ qua khác biệt OS. Với khách Nhật, "hook script luôn absolute path" là một gạch đầu dòng supply-chain hardening cụ thể (chống binary planting), không phải khẩu hiệu.

Hai hook thực chiến — TypeScript check & chống query trùng

Hook giải được vài điểm yếu kinh điển của AI-assisted dev trên project lớn. Hai ví dụ dưới đây chính là hooks/tsc.jshooks/query_hook.js trong project thực hành.

1. TypeScript type-checking hook (PostToolUse)

Vấn đề gốc: khi Claude đổi một function signature, nó thường không cập nhật hết call site. VD nhờ thêm param verbose vào một hàm trong schema.ts → Claude sửa được definition nhưng quên call site ở main.ts → sinh type error mà Claude không bắt ngay.

Giải: một PostToolUse hook chạy TypeScript compiler sau mỗi lần sửa file:

  1. Chạy tsc --noEmit để kiểm type error.
  2. Capture lỗi tìm được.
  3. Feed lỗi ngược cho Claude ngay.
  4. Prompt Claude đi sửa các file khác (call site).

→ Áp dụng cho mọi typed language có type checker. Ngôn ngữ untyped thì thay bằng automated tests.

2. Query duplication prevention hook (Agent SDK)

Vấn đề: project lớn nhiều query → Claude đôi khi viết trùng chức năng thay vì tái dùng code có sẵn — nhất là khi task phức tạp nhiều bước mà thao tác DB chỉ là một mảnh. VD "tạo Slack integration cảnh báo đơn pending > 3 ngày" → Claude viết query mới thay vì dùng getPendingOrders() đã tồn tại.

Giải: một review process:

  1. Trigger khi Claude sửa file trong ./queries.
  2. Khởi chạy một instance Claude Code THỨ HAI (programmatically, qua Agent SDK).
  3. Nhờ instance thứ hai review thay đổi + kiểm query tương tự đã có.
  4. Nếu trùng → feed feedback cho instance gốc.
  5. Prompt Claude bỏ bản trùng, dùng lại code có sẵn.

Cân nhắc (trade-offs):

TypeScript hook Query duplication hook
Chi phí Nhẹ, chạy nhanh Nặng — mỗi lần sửa ./queries khởi chạy 1 Claude mới (tốn time + API usage)
Lời khuyên Bật thoải mái Chỉ monitor thư mục quan trọng để giảm overhead

Cả hai dùng Agent SDK để một Claude review công việc của Claude khác — "AI reviews AI".

Nguyên tắc mở rộng (áp cho project của bạn):

  • Dùng compiler/linter output làm feedback tức thì.
  • Dựng code review bằng AI instance riêng.
  • Focus monitor thư mục giá trị cao (nơi tính nhất quán quan trọng nhất).
  • Cân bằng lợi ích tự động hoá vs chi phí hiệu năng.

Áp dụng thực tế: hai hook này là hai archetype tôi luôn tách bạch. tsc.jsdeterministic gate — trong sandbox nó gọi TS compiler API (ts.createProgram + noEmit), lỗi type là ground truth, đúng/sai không cãi; loại này tôi bật rộng tay vì rẻ và chắc. query_hook.jsprobabilistic gate — nó query() qua Agent SDK để một Claude khác phán đoán trùng lặp; mạnh cho thứ không có compiler (trùng ngữ nghĩa, style), nhưng tốn tiền + latency + có thể sai, nên trong sandbox nó bị process.exit(0) tắt sẵn cho khỏi đốt credit. Quy tắc của tôi: việc nào có ground truth (type/test/lint) thì dùng cổng tất định; việc mờ mới nhờ AI, và luôn giới hạn ở thư mục critical để chi phí không phình. Với Delivery, đây là "shift-left": bắt lỗi ngay lúc sửa (call site lệch, query trùng) thay vì để lộ ở review/CI muộn hơn.

Còn hook nào nữa? Toàn bộ event + mẹo soi input

Pre/PostToolUse chỉ là hai loại phổ biến. Claude Code bắn 33 hook event (đối chiếu docs 08/2026) trong một session — không cần thuộc hết. Có một nhúm nhỏ bạn dùng đi dùng lại, và chúng nằm đúng những điểm trong agentic loop mà bạn muốn chen vào: session bắt đầu → prompt vào → tool được gọi → turn kết thúc.

Hook event Chạy khi
PreToolUse TRƯỚC một tool call — primitive để cưỡng chế, loại duy nhất dừng được việc trước khi nó xảy ra
PostToolUse SAU một tool call thành công — chỗ đặt auto-format / auto-lint
Stop Claude muốn kết thúc turn — bạn từ chối được, nói "chưa xong đâu" nếu một điều kiện chưa thoả. Có bản song sinh SubagentStop cho lúc sub-agent xong
PreCompact / PostCompact Trướcsau compaction
InstructionsLoaded Một CLAUDE.md / rule file được load — tiện để audit xem thứ gì THẬT SỰ vào context
SessionStart Đầu session, để mồi môi trường. Dùng source startup nếu chỉ muốn chạy lúc khởi động mới
Notification Claude Code gửi notification — khi cần xin quyền dùng tool, hoặc sau khi idle 60 giây
UserPromptSubmit User submit prompt, TRƯỚC khi Claude xử lý
SessionEnd Một session kết thúc

⚠️ Chỗ vấp nhiều người: muốn bơm lại context SAU compaction thì ĐỪNG dùng PostCompact — dùng SessionStart với matcher compact. Đó mới là cái thật sự đưa output của nó ngược vào hội thoại.

Chỗ rối: cấu trúc stdin đổi theo 2 trục:

  1. Theo loại hook (PreToolUse, PostToolUse, Notification, Stop…) — mỗi loại field khác nhau.
  2. Theo tool được gọi (chỉ với Pre/Post) — tool_input khác nhau tuỳ tool.

VD một PostToolUse canh TodoWrite (tool Claude dùng để quản to-do) — có tool_name, tool_input.todos, và thêm tool_response:

{
  "hook_event_name": "PostToolUse",
  "tool_name": "TodoWrite",
  "tool_input": { "todos": [{ "content": "write a readme", "status": "pending", "id": "1" }] },
  "tool_response": { "oldTodos": [], "newTodos": [{ "content": "write a readme", "status": "pending", "id": "1" }] }
}

Còn một Stop hook thì không có tool_name/tool_input, chỉ:

{ "hook_event_name": "Stop", "stop_hook_active": false }

→ Shape khác nhau nhiều nên bạn có thể không biết trước command sẽ nhận cấu trúc gì.

Mẹo: hook "soi input". Gắn tạm một hook matcher: "*" chỉ để ghi input ra file:

"PostToolUse": [
  { "matcher": "*", "hooks": [ { "type": "command", "command": "jq . > post-log.json" } ] }
]

Nó ghi nguyên input vào post-log.json → mở ra xem chính xác dữ liệu nào được feed vào → viết command đúng field. (Chính là các dòng pre-log.json/post-log.json trong settings.example.json của sandbox.)

Giữ state qua một lần compact

Một pattern nữa đáng dựng sẵn. Khi Claude compact một hội thoại dài, nó đánh rơi rất nhiều chi tiết. Một SessionStart hook với matcher compact chạy ngay sau compaction — cho nó in ra một summary ngắn về những file bạn đang làm. Summary đó đi ngược vào context, nên Claude bắt nhịp tiếp thay vì bắt đầu lạnh.

(Ghép với bảng exit code ở trên mới thấy hết: SessionStart là một trong ba event mà plain text ở stdout được cộng vào context — chính vì thế hook giữ-state chỉ cần echo một đoạn text, khỏi cần JSON.)

Chốt lại phần hook

Hook biến một luật Claude thường theo thành luật luôn theo. Đừng dừng ở auto-format: guard tool bằng PreToolUse, gác cửa turn bằng Stop, và giữ state qua compact. Dựng thì tốn công lúc đầu, nhưng hoàn vốn ngay lần đầu nó bắt được một thứ trong lần chạy bạn còn chẳng ngồi xem.

Áp dụng thực tế: mấy event ngoài-tool này mở ra tự động hoá cả vòng đời phiên, không chỉ tool call: SessionStart nạp context/env đầu phiên; UserPromptSubmit chèn context hoặc chặn prompt trước khi Claude đọc; Stop ping tôi / chạy check cuối khi Claude xong; PreCompact lưu state trước khi lịch sử bị nén; Notification báo desktop khi Claude đang chờ tôi; SubagentStop gom kết quả Task. Nguyên tắc tôi ghim: chọn event HẸP nhất cho đúng tín hiệu cần (đừng nhét mọi thứ vào PostToolUse matcher *). Và kỷ luật quan trọng nhất bài này dạy: đừng đoán schema — log nó ra rồi đọc. Cùng phản xạ debug với bài "JSON qua stdin", nay tổng quát hoá: hợp đồng input là theo-từng-event, nên jq . > log.json quan sát trước, code sau là bước bắt buộc, không phải tuỳ chọn.

Ba thứ ở bản cập nhật này tôi thấy đáng giá nhất: (1) câu "request, not guarantee" — nó đặt tên đúng cho thứ tôi vẫn cãi nhau khi review process: luật nằm trong tài liệu là kỳ vọng, luật nằm trong code chạy mới là kiểm soát; y hệt lý do checklist dán tường thua một CI gate. (2) Redact thay vì block (updatedInput) là thay đổi tư duy guardrail: guardrail không nhất thiết phải là "cấm" — nó có thể là "vẫn cho làm, nhưng làm phiên bản an toàn", nên không cản trở việc; đây đúng thứ tôi cần cho log/secret trong môi trường khách Nhật, nơi chặn thẳng thì dev sẽ tìm đường đi vòng. Nhớ cái bẫy updatedInput thay cả object — quên echo lại field cũ là mất tham số, hỏng lệnh. (3) exit 1 KHÔNG chặn là loại bug im lặng nguy hiểm nhất: hook trông như đang gác mà thực ra cửa vẫn mở — muốn chặn thì exit 2. Còn SessionStart matcher compact (không phải PostCompact) là mảnh ghép cuối cho các phiên chạy dài: nén xong vẫn nhớ mình đang sửa file nào.


Phần tiếp theo: Claude Code: routine, headless & Agent SDK

Nguồn: Claude Code in Action (Anthropic Academy) — Copyright Anthropic. Phần đề thi thử cho khoá này nằm ở tab "Đề thi thử" trên trang tổng quan khoá.

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

Hook trong Claude Code là gì và chen vào luồng ở đâu?
Hook cho phép bạn chạy command của riêng mình ngay trước hoặc ngay sau khi Claude định gọi một tool. Luồng bình thường: query của bạn gửi tới model kèm tool definitions, Claude quyết dùng tool (trả một formatted response), rồi Claude Code thực thi tool và trả kết quả. Hook chen vào ngay quanh bước thực thi tool đó — cho bạn chạy code just-before (PreToolUse) hoặc just-after (PostToolUse). Rất hữu ích để format code sau khi sửa, chạy test khi file đổi, hoặc chặn truy cập file cụ thể.
PreToolUse khác PostToolUse thế nào, loại nào chặn được thao tác?
PreToolUse chạy TRƯỚC khi tool được gọi và CHẶN được: command của bạn nhận chi tiết về tool call Claude định làm, rồi bạn cho phép thao tác chạy bình thường, hoặc block nó và gửi một error message ngược lại cho Claude. PostToolUse chạy SAU khi tool đã thực thi nên KHÔNG chặn được nữa (thao tác đã xảy ra) — nhưng nó vẫn chạy được follow-up (VD format file vừa sửa) và cấp thêm feedback cho Claude. Câu chốt: PreToolUse kiểm soát Claude ĐƯỢC LÀM GÌ; PostToolUse tăng cường thứ Claude ĐÃ LÀM.
Khai báo hook ở đâu, và matcher để làm gì?
Hook định nghĩa trong các Claude settings file, đúng bộ ba vị trí như CLAUDE.md: ~/.claude/settings.json (global, mọi project), .claude/settings.json (project, commit & chia sẻ team), và .claude/settings.local.json (cá nhân, không commit). Bạn viết tay hoặc dùng lệnh /hooks ngay trong Claude Code. Trong mỗi hook, matcher chỉ định loại tool nào bị nhắm — ví dụ "Read" nhắm tool Read, còn "Write|Edit" dùng dấu | kiểu regex để nhắm cả Write lẫn Edit.
Hook thường dùng vào những việc gì?
Sáu ứng dụng điển hình: code formatting (tự format file sau khi Claude sửa), testing (tự chạy test khi file đổi), access control (chặn Claude đọc/sửa file cụ thể), code quality (chạy linter/type checker rồi feedback cho Claude), logging (ghi lại file nào Claude chạm/sửa), và validation (kiểm naming convention / coding standard). Access control nên đặt ở PreToolUse vì chỉ loại đó chặn được; formatting/logging hợp với PostToolUse vì chạy theo sau.
Dựng một hook gồm những bước nào?
Bốn bước. (1) Chọn PreToolUse hay PostToolUse — Pre ngăn được tool call chạy, Post chạy sau khi tool đã dùng. (2) Xác định loại tool call cần canh — chỉ rõ đúng tool nào kích hoạt hook (chính là matcher). (3) Viết một command nhận tool call — command này nhận JSON data về tool call đề xuất qua standard input (stdin). (4) Nếu cần, command trả feedback cho Claude — exit code của command báo Claude allow hay block thao tác.
Hook command nhận dữ liệu về tool call như thế nào?
Khi hook command chạy, Claude gửi một khối JSON qua standard input (stdin) mô tả tool call đề xuất, gồm các field session_id, transcript_path, hook_event_name (VD "PreToolUse"), tool_name (VD "Read") và tool_input (VD { "file_path": "/code/queries/.env" }). Command đọc JSON từ stdin, parse nó, rồi quyết định allow hay block dựa trên tool_name và tool_input.
Exit code trong hook nghĩa là gì?
Ba con số đáng nhớ. Exit 0 = success: nếu stdout là JSON thì Claude parse nó; plain text bị bỏ qua ở đa số event, ngoại trừ SessionStart, UserPromptSubmit và UserPromptExpansion — ở ba event đó plain text được cộng thẳng vào context. Exit 2 = blocking error: stderr được feed ngược cho Claude làm context, và đây là exit code chặn ở gần như mọi nơi, kể cả chặn Stop để nói "chưa xong đâu". Mọi exit code khác là non-blocking: stderr chỉ được log và Claude chạy tiếp. ⚠️ Bẫy hay dính: exit 1 trông như lỗi nhưng KHÔNG chặn — Claude vẫn chạy command; muốn dừng thật thì exit 2. Riêng PostToolUse thì chặn là muộn (tool đã chạy) dù vẫn feed text về Claude được, và vài event bỏ qua chặn hoàn toàn như Notification với SessionStart.
PreToolUse hook trả quyết định cho Claude bằng cách nào?
In JSON ra stdout rồi exit 0. Field then chốt là permissionDecision trong hookSpecificOutput, nhận ba giá trị: allow (cho call đi qua), deny (chặn call), ask (đẩy về cho user tự quyết); kèm permissionDecisionReason để giải thích. Còn một giá trị thứ tư là defer nhưng chỉ áp cho non-interactive -p run, nơi một tiến trình gọi tạm dừng tool rồi resume sau — hiếm khi phải dùng.
Hook có thể sửa lệnh của Claude thay vì chặn không?
Có, bằng field updatedInput trong hookSpecificOutput — thay vì block một call, bạn viết lại nó. Đây là cách bóc một secret ra khỏi lệnh bash mà vẫn cho lệnh chạy: một PreToolUse guardrail canh tool Bash nhận diện pattern sk_live_ rồi thay bằng placeholder trước khi lệnh kịp thực thi. Lệnh vẫn chạy, việc vẫn xong, nhưng secret không bao giờ đi qua — khác biệt giữa blocking và redacting. Catch quan trọng: updatedInput thay TOÀN BỘ input object, nên phải echo lại những field bạn không đổi, không thì mất chúng.
Làm sao giữ được context sau khi Claude compact hội thoại?
Dùng SessionStart với matcher compact, KHÔNG dùng PostCompact — SessionStart mới là event thật sự đưa output của hook ngược vào hội thoại. Cho hook in ra một summary ngắn về những file bạn đang làm; summary đó vào context nên Claude bắt nhịp tiếp thay vì bắt đầu lạnh. Nó chạy được vì SessionStart là một trong ba event mà plain text ở stdout được cộng vào context.
Hook chặn Read .env có chặn luôn được Grep và Bash không?
Không — bản hiện hành của bài chỉ chặn tool Read (kiểm tool_input.file_path). Lý do: mỗi tool gửi input shape khác nhau. Read gửi { file_path }; Grep gửi { pattern, path } với path là THƯ MỤC tìm kiếm chứ không phải file; Bash gửi { command }. Nên một check trên file_path bắt được Read nhưng không chặn được grep API_KEY toàn project hay cat .env trong Bash. Muốn phủ đủ, hoặc viết matcher riêng cho từng tool và soi đúng field của nó, hoặc dùng permissions.deny với rule như "Read(**/.env)" — loại rule này áp đồng đều trên mọi tool nên gọn và chắc hơn.
Vì sao sau npm run setup lại có hai file settings trong .claude?
Vì một khuyến nghị bảo mật hook của Claude Code: nên dùng absolute path (không dùng relative path) cho script hook, để giảm path interception và binary planting attack. Nhưng absolute path lại khó share vì đường dẫn tuyệt đối trên mỗi máy khác nhau. Nên project giữ settings.example.json (template, có commit và chia sẻ) chứa placeholder $PWD; khi chạy npm run setup, ngoài việc cài dependencies nó còn chạy scripts/init-claude.js để thay $PWD bằng absolute path trên máy bạn, rồi copy và đổi tên thành settings.local.json (file cá nhân, không commit). Nhờ vậy vừa share được settings vừa giữ absolute path.
Vì sao nên dùng absolute path thay vì relative path cho script hook?
Vì lý do bảo mật: absolute path khoá cứng đúng file cần chạy, giảm rủi ro path interception và binary planting attack — tức kẻ xấu đặt một script hay binary giả trùng tên vào một thư mục (trên PATH hoặc thư mục tương đối) để hook vô tình chạy nhầm nó. Đánh đổi là absolute path khó chia sẻ giữa các máy, nên mới cần cơ chế $PWD + init-claude.js để sinh path đúng theo từng máy.
TypeScript type-checking hook giải quyết vấn đề gì và làm thế nào?
Vấn đề: khi Claude đổi một function signature, nó thường không cập nhật hết call site — ví dụ thêm param verbose vào một hàm trong schema.ts thì Claude sửa được definition nhưng quên chỗ gọi ở main.ts, tạo type error. Giải: một PostToolUse hook chạy trình biên dịch TypeScript (tsc --noEmit) sau mỗi lần sửa file: nó kiểm type error, capture lỗi, feed lỗi ngược lại cho Claude ngay, và prompt Claude đi sửa các file khác. Cách này áp dụng cho mọi typed language có type checker; với ngôn ngữ untyped thì thay bằng automated tests.
Query duplication prevention hook hoạt động thế nào, và dùng gì?
Nó dùng Claude Agent SDK để triển khai một review process. Khi Claude sửa file trong thư mục ./queries, hook khởi chạy một instance Claude Code THỨ HAI (programmatically), nhờ instance đó review thay đổi và kiểm xem có query tương tự đã tồn tại chưa (VD getPendingOrders). Nếu phát hiện trùng, nó feed feedback về instance gốc và prompt Claude bỏ bản trùng, dùng lại code có sẵn. Đây là mô hình "AI review AI". Nhược điểm: nặng — mỗi lần sửa ./queries tốn thêm thời gian và API usage, nên chỉ nên monitor các thư mục quan trọng.
Vì sao nên chỉ monitor thư mục quan trọng với query duplication hook?
Vì query hook khởi chạy một instance Claude riêng cho mỗi lần review, tốn thêm thời gian và chi phí API mỗi lần có edit trong thư mục được canh. Lợi ích là codebase sạch hơn, ít trùng lặp; nhưng để cân bằng lợi ích tự động hoá với chi phí hiệu năng, chỉ nên áp nó cho các thư mục giá trị cao nơi tính nhất quán quan trọng nhất (như ./queries), thay vì canh toàn bộ project. Khác với nó, TypeScript hook nhẹ và chạy nhanh nên bật thoải mái hơn.
Ngoài PreToolUse và PostToolUse, Claude Code còn những hook event nào?
Docs 08/2026 liệt kê 33 hook event, nhưng nhúm bạn thực sự đụng tới là bảy event phủ vòng đời phiên làm việc. Notification chạy khi Claude Code gửi notification — lúc cần xin quyền dùng tool, hoặc sau khi idle 60 giây. Stop chạy khi Claude trả lời xong. SubagentStop chạy khi một subagent (hiện là "Task" trong UI) xong. PreCompact chạy trước một thao tác compact (thủ công hoặc tự động). UserPromptSubmit chạy khi user submit prompt, trước khi Claude xử lý. SessionStart chạy khi bắt đầu hoặc resume một session, và SessionEnd khi session kết thúc.
Vì sao viết hook lại khó vì cấu trúc stdin thay đổi?
Vì input đổi theo hai trục. Một, theo loại hook: PreToolUse, PostToolUse, Notification, Stop… mỗi loại có field khác nhau — ví dụ một Stop hook chỉ có hook_event_name và stop_hook_active, KHÔNG có tool_name hay tool_input; còn một PostToolUse thì có tool_name, tool_input và thêm cả tool_response. Hai, với Pre/PostToolUse thì tool_input còn khác nhau tuỳ tool được gọi (VD TodoWrite gửi tool_input.todos). Vì vậy bạn thường không biết trước command của mình sẽ nhận đúng cấu trúc gì.
Làm sao biết chính xác dữ liệu mà hook của mình sẽ nhận?
Dùng một "hook soi input": gắn tạm một hook với matcher "*" và command 'jq . > post-log.json'. Nó ghi nguyên khối input mà hook nhận vào file post-log.json, để bạn mở ra xem chính xác shape dữ liệu rồi mới viết command inspect đúng field. Đây là kỷ luật "quan sát trước, code sau" — đừng đoán schema. (Chính là các dòng pre-log.json/post-log.json trong settings.example.json của project thực hành.)
Claude Code có mấy permission mode và khác nhau chỗ nào?
Sáu mode, mỗi mode kẻ một đường khác nhau giữa "chạy thẳng" và "phải xin phép". Manual (default) chỉ đọc file, mọi thứ khác hỏi trước. Accept edits chạy luôn reads, file edits và các lệnh bash file system thông dụng — hợp khi bạn iterate rồi review sau. Plan chỉ đọc, không sửa gì, chỉ nghiên cứu và đề xuất thay đổi. Auto chấp nhận mọi thứ nhưng có một classifier model riêng review từng action trước khi nó chạy. Don't ask (dontAsk) chỉ cho phép tool đã pre-approve, còn lại auto-deny không prompt. Bypass permissions bỏ qua mọi kiểm tra (tương đương cờ dangerously-skip-permissions) và chỉ nên chạy trong container hoặc VM cô lập. Bấm Shift+Tab để luân phiên giữa các mode; status bar dưới màn hình luôn hiện mode đang dùng. Lưu ý bản 08/2026: đang ở auto thì phím đầu nhảy về default, rồi vòng chạy default → acceptEdits → plan, còn dontAsk không nằm trong cycle.
Mode mặc định của Claude Code là gì?
Từ 14/08/2026, mode khởi động sẵn trên gói Pro, Max và Team (chạy ở terminal hoặc VS Code extension) là auto mode — classifier duyệt hành động thay bạn ngay từ prompt đầu, thay cho Manual như trước. Cần Claude Code v2.1.228 trở lên (v2.1.233 trên Windows native). Vẫn khởi động ở Manual (config value default) nếu: dùng gói Enterprise hoặc API key của Claude Console, chạy claude -p hoặc Agent SDK, chạy trên Bedrock / Google Cloud Agent Platform / Microsoft Foundry, hoặc settings đặt disableAutoMode. Đổi lại bằng khoá permissions.defaultMode trong user settings — lưu ý giá trị "auto" đặt ở project settings không có hiệu lực.
Auto mode của Claude Code an toàn tới đâu — classifier chặn được gì?
Auto là mode hands-off: Claude tự chạy, nhưng trước mỗi action có một classifier model riêng xem xét. Classifier gác INTENT, tức soi những nước đi leo thang vượt quá thứ bạn thật sự yêu cầu: production deploy và migration, force push hoặc pipe code tải về thẳng vào shell, gửi sensitive data ra endpoint bên ngoài, phá huỷ file. Nó cho qua phần việc thường ngày: local edit trong project, cài dependency từ lock file, các read-only request, và push lên branch của chính bạn. Guardrail này vẫn đang tiến hoá nên tra docs để lấy block list/allow list hiện hành.
Vì sao auto mode phải đi kèm một Stop hook chạy test?
Vì classifier kiểm intent chứ không kiểm correctness — nó không biết code có chạy được không. Nhờ Claude refactor authentication mà nó viết authentication hỏng thì classifier vẫn cho qua, bởi hỏng không phải là nguy hiểm. Hai lớp bổ trợ nhau: auto mode gác Claude ĐỊNH LÀM GÌ, trước mỗi action, trong lúc chạy; Stop hook xác nhận code có THẬT SỰ chạy, sau khi Claude làm xong. Một cửa trước, một cửa sau — bỏ cửa nào cũng thủng.
Chạy Claude Code không có người ngồi duyệt (CI, cron) thì dùng mode nào?
Dùng dontAsk (Don't ask). Đây là nước đi đúng cho CI pipeline, scheduled job và batch chạy đêm: chỉ tool đã pre-approve được phép, mọi thứ ngoài danh sách bị auto-deny mà không prompt. Đó chính là điểm cốt — pipeline chạy tiếp thay vì treo chờ một cái duyệt mà không ai ở đó để bấm. Đừng thay bằng bypassPermissions cho "tiện": bypass bỏ mọi kiểm tra và chỉ an toàn trong container/VM cô lập.