làm tíai?GitHub ↗
CHƯƠNG 10 / 16

Tự động hóa bằng Hooks

Chạy kiểm tra, formatter và guardrail tại các lifecycle event của Claude Code.

16 phút đọc

Hooks là các shell command, HTTP endpoint hoặc LLM prompt do bạn định nghĩa, chạy tự động tại những điểm cụ thể trong vòng đời của Claude Code. Chúng cho bạn quyền kiểm soát mang tính deterministic (chắc chắn xảy ra) thay vì phụ thuộc vào việc model có "chịu" chạy một hành động nào đó hay không — dùng để enforce project rules, tự động hóa việc lặp lại (format, lint) và tích hợp Claude Code với công cụ sẵn có.

Hooks là gì và khi nào dùng

Khi một hook event fire và matcher khớp, Claude Code truyền JSON mô tả context về hook handler. Với command hook, input đến qua stdin; với HTTP hook, nó là body của POST request. Handler đọc input, hành động, và tùy chọn trả về một quyết định qua exit code hoặc JSON trên stdout.

Dùng hook khi bạn cần một hành động luôn xảy ra: chạy formatter sau mỗi lần edit, block một câu lệnh nguy hiểm trước khi nó chạy, gửi notification khi Claude cần input, hay inject context lúc session bắt đầu. Với quyết định cần "phán đoán" thay vì quy tắc cứng, dùng prompt-based hoặc agent-based hook (xem bên dưới).

Hook lifecycle

Hook fire tại các điểm lifecycle cụ thể. Các event chia thành ba nhịp (cadence):

  • Một lần mỗi session: SessionStart, SessionEnd
  • Một lần mỗi turn: UserPromptSubmit, Stop, StopFailure
  • Mỗi tool call trong agentic loop: PreToolUse, PostToolUse

Khi một event fire, tất cả hook khớp chạy song song, và các handler trùng lặp được dedupe tự động (command hook dedupe theo command string + args; HTTP hook theo URL).

Bảng hook events

Các event thường dùng nhất:

EventFire khi
SessionStartKhi session bắt đầu hoặc resume
SetupKhi khởi chạy với --init-only, hoặc --init/--maintenance trong chế độ -p
UserPromptSubmitKhi bạn submit một prompt, trước khi Claude xử lý
UserPromptExpansionKhi một command người dùng gõ expand thành prompt. Có thể block
PreToolUseTrước khi một tool call chạy. Có thể block
PermissionRequestKhi một permission dialog xuất hiện
PermissionDeniedKhi tool call bị auto mode classifier từ chối. Trả {retry: true} để cho model retry
PostToolUseSau khi một tool call thành công
PostToolUseFailureSau khi một tool call thất bại
PostToolBatchSau khi một batch tool call song song hoàn tất, trước model call tiếp theo
NotificationKhi Claude Code gửi notification
SubagentStartKhi một subagent được spawn
SubagentStopKhi một subagent kết thúc
TaskCreatedKhi một task được tạo qua TaskCreate
TaskCompletedKhi một task được đánh dấu hoàn thành
StopKhi Claude hoàn tất phản hồi
StopFailureKhi turn kết thúc do API error. Output và exit code bị bỏ qua
InstructionsLoadedKhi một CLAUDE.md hoặc .claude/rules/*.md được load vào context
ConfigChangeKhi một file cấu hình thay đổi trong session
CwdChangedKhi thư mục làm việc thay đổi (ví dụ khi Claude chạy cd)
FileChangedKhi một file được watch thay đổi trên disk. matcher chỉ định filename cần watch
WorktreeCreateKhi một worktree đang được tạo qua --worktree hoặc isolation: "worktree"
WorktreeRemoveKhi một worktree đang bị xóa
PreCompactTrước khi context compaction
PostCompactSau khi context compaction hoàn tất
ElicitationKhi một MCP server yêu cầu user input trong một tool call
ElicitationResultSau khi user phản hồi một MCP elicitation
SessionEndKhi một session kết thúc

Còn có TeammateIdle, MessageDisplay, và một số event khác cho các tính năng nâng cao (agent teams, streaming display). Xem Hooks reference để có danh sách đầy đủ.

Cấu hình trong settings.json

Hooks được định nghĩa trong file settings JSON, với ba tầng nesting:

  1. Chọn một hook event (như PreToolUse hoặc Stop)
  2. Thêm một matcher group để lọc khi nào fire
  3. Định nghĩa một hoặc nhiều hook handler để chạy khi khớp
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

Mỗi tên event là một key bên trong đối tượng hooks duy nhất. Nếu file settings đã có key hooks, thêm event mới như một sibling thay vì thay thế cả object.

Hook locations (scope)

Nơi bạn định nghĩa hook quyết định phạm vi của nó:

LocationScopeShare được?
~/.claude/settings.jsonTất cả project của bạnKhông, local trên máy bạn
.claude/settings.jsonMột projectCó, commit vào repo được
.claude/settings.local.jsonMột projectKhông, gitignored khi Claude tạo
Managed policy settingsToàn organizationCó, admin kiểm soát
Plugin hooks/hooks.jsonKhi plugin được bậtCó, đóng gói cùng plugin
Skill / agent frontmatterKhi component đang activeCó, định nghĩa trong file component

Enterprise admin có thể dùng allowManagedHooksOnly để chặn user/project/plugin hook.

Matcher patterns

Field matcher lọc khi nào hook fire. Cách nó được đánh giá phụ thuộc vào ký tự nó chứa:

Giá trị matcherĐánh giá nhưVí dụ
"*", "", hoặc bỏ trốngMatch tất cảfire mọi lần event xảy ra
Chỉ chữ, số, _, -, khoảng trắng, ,, |Chuỗi khớp chính xác, hoặc danh sách phân tách bởi | / ,Bash; Edit|Write; Edit, Write
Chứa bất kỳ ký tự nào khácJavaScript regular expression, unanchored^Notebook; mcp__memory__.*

Đường regex được test bằng RegExp.prototype.test, khớp bất kỳ đâu trong chuỗi. Edit.* khớp cả EditNotebookEdit; bọc bằng ^$ (^Edit$) khi cần khớp toàn chuỗi. Matcher case-sensitive.

Mỗi loại event match trên một field khác nhau:

EventMatcher lọc theoVí dụ giá trị
PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDeniedtool nameBash, Edit|Write, mcp__.*
SessionStartcách session bắt đầustartup, resume, clear, compact
SessionEndlý do session kết thúcclear, resume, logout, other, ...
Notificationloại notificationpermission_prompt, idle_prompt, ...
SubagentStart, SubagentStopagent typegeneral-purpose, Explore, Plan, ...
PreCompact, PostCompacttác nhân trigger compactionmanual, auto
ConfigChangenguồn cấu hìnhuser_settings, project_settings, skills, ...
FileChangedfilename cần watch.envrc|.env
UserPromptSubmit, Stop, PostToolBatch, CwdChanged, ...không hỗ trợ matcherluôn fire mọi lần

Các event không hỗ trợ matcher sẽ âm thầm bỏ qua field matcher nếu bạn thêm vào.

Match MCP tools

MCP server tool xuất hiện như tool thường trong tool event, theo pattern mcp__<server>__<tool>. Để match mọi tool từ một server, append .* vào prefix (bắt buộc):

  • mcp__memory__.* — mọi tool từ server memory
  • mcp__brave-search__.* — server có tên chứa dấu gạch ngang
  • mcp__.*__write.* — mọi tool bắt đầu bằng write từ mọi server

Lọc chi tiết với field if

Với tool event, bạn lọc chặt hơn bằng field if trên từng handler. if dùng cú pháp permission rule, khớp theo cả tool name lẫn arguments — nên hook process chỉ spawn khi tool call khớp:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git *)",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
          }
        ]
      }
    ]
  }
}

if chỉ giữ đúng một permission rule (không có &&, ||, hay danh sách) và chỉ đánh giá trên tool event (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied). Trên event khác, hook có if sẽ không bao giờ chạy. Với Bash pattern: leading VAR=value bị strip trước khi match; mỗi subcommand (kể cả trong $() và backticks) được kiểm tra riêng. Bộ lọc là best-effort (fail open khi không parse được) — dùng permission system để enforce cứng, không dùng hook.

Hook handler types

Mỗi object trong mảng hooks bên trong là một handler. Có năm loại:

typeChạy gì
commandChạy shell command. Nhận JSON input qua stdin, trả kết quả qua exit code + stdout
httpPOST JSON input tới một URL. Endpoint trả kết quả qua response body
mcp_toolGọi một tool trên MCP server đã kết nối. Text output xử lý như stdout của command
promptGửi prompt tới model để đánh giá single-turn, trả yes/no dạng JSON
agentSpawn subagent có thể dùng tool (Read, Grep, Glob) để verify. Experimental

Common fields (mọi loại)

FieldBắt buộcMô tả
type"command", "http", "mcp_tool", "prompt", hoặc "agent"
ifKhôngPermission rule để lọc; chỉ trên tool event
timeoutKhôngGiây trước khi hủy. Mặc định: 600 cho command/http/mcp_tool; 30 cho prompt; 60 cho agent
statusMessageKhôngMessage spinner tùy chỉnh hiển thị khi hook chạy

Command hook fields

FieldMô tả
commandShell command để chạy. Với args, đây là executable spawn trực tiếp
argsDanh sách argument. Khi có, command được resolve như executable và spawn không qua shell
asyncNếu true, chạy nền không block
shellShell dùng: "bash" hoặc "powershell"

Shell form (không có args): command truyền cho shell — hỗ trợ pipe, &&, biến. Exec form (có args): spawn trực tiếp, mỗi phần tử args là một argument y nguyên, không quoting. Dùng exec form khi tham chiếu path placeholder.

Tham chiếu script bằng path

Dùng các placeholder này để trỏ script bất kể working directory:

  • ${CLAUDE_PROJECT_DIR} — project root
  • ${CLAUDE_PLUGIN_ROOT} — thư mục cài đặt plugin
  • ${CLAUDE_PLUGIN_DATA} — thư mục dữ liệu bền của plugin
{
  "type": "command",
  "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-style.sh",
  "args": []
}

Input và output JSON

Command hook nhận JSON qua stdin và giao tiếp kết quả qua exit code, stdout, stderr. HTTP hook nhận JSON như POST body và trả kết quả qua response body.

Common input fields

Mọi event nhận các field chung sau, cùng với field riêng của từng event:

FieldMô tả
session_idĐịnh danh session hiện tại
transcript_pathĐường dẫn tới file JSON hội thoại
cwdWorking directory khi hook được gọi
permission_modePermission mode hiện tại: default, plan, acceptEdits, auto, dontAsk, bypassPermissions
hook_event_nameTên event đã fire

Ví dụ, một PreToolUse hook cho lệnh Bash nhận trên stdin:

{
  "session_id": "abc123",
  "cwd": "/home/user/my-project",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test"
  }
}

tool_nametool_input là field riêng theo event. UserPromptSubmit nhận prompt; SessionStart nhận source (startup/resume/clear/compact), v.v.

Exit codes

Exit code từ command của bạn cho Claude Code biết hành động nên tiếp tục, bị block, hay bị bỏ qua:

Exit codeÝ nghĩa
0Thành công. stdout được parse tìm JSON output (chỉ xử lý JSON khi exit 0)
2Blocking error. stdout bị bỏ qua; stderr được feed lại cho Claude như error
Bất kỳ code khácNon-blocking error (với hầu hết event). Transcript hiện <hook> hook error, thực thi tiếp tục

Lưu ý: chỉ exit 2 block hành động. Exit 1 bị coi là non-blocking error và hành động vẫn tiếp tục — dù 1 là mã lỗi Unix thông thường. Ngoại lệ: WorktreeCreate, nơi mọi exit code khác 0 đều hủy việc tạo worktree.

Với UserPromptSubmit, UserPromptExpansion, và SessionStart, stdout khi exit 0 được thêm vào context mà Claude thấy được; với các event khác, stdout chỉ ghi vào debug log.

Exit code 2 — hành vi theo event

Tác dụng của exit 2 phụ thuộc vào event, vì một số event là hành động chưa xảy ra (block được) còn số khác đã xảy ra rồi:

EventBlock được?Hành vi khi exit 2
PreToolUseBlock tool call
PermissionRequestTừ chối permission
UserPromptSubmitBlock xử lý prompt và xóa prompt
UserPromptExpansionBlock việc expansion
StopNgăn Claude dừng, tiếp tục hội thoại
SubagentStopNgăn subagent dừng
PostToolBatchDừng agentic loop trước model call tiếp theo
ConfigChangeBlock config change (trừ policy_settings)
PreCompactBlock compaction
PostToolUseKhôngHiện stderr cho Claude; tool đã chạy rồi
PostToolUseFailureKhôngHiện stderr cho Claude; tool đã fail rồi
SessionStart / Setup / SubagentStartKhôngHiện stderr cho user; session/subagent tiếp tục
Notification, SessionEnd, CwdChanged, FileChangedKhôngChỉ hiện stderr cho user
StopFailureKhôngOutput và exit code bị bỏ qua

JSON output — decision control

Exit code chỉ cho phép block hoặc im lặng. Để kiểm soát tinh hơn, exit 0 và in một JSON object ra stdout. Không trộn hai cách: nếu exit 2, JSON bị bỏ qua.

Các universal field hoạt động trên mọi event:

FieldMặc địnhMô tả
continuetrueNếu false, Claude dừng hoàn toàn. Ưu tiên hơn mọi decision field
stopReasonnoneMessage hiện cho user khi continue: false (Claude không thấy)
suppressOutputfalseNếu true, ẩn stdout của hook khỏi transcript
systemMessagenoneWarning message hiện cho user

Mỗi nhóm event dùng một pattern quyết định khác nhau:

EventsPatternKey fields
UserPromptSubmit, PostToolUse, Stop, SubagentStop, ConfigChange, PreCompacttop-level decisiondecision: "block", reason
PreToolUsehookSpecificOutputpermissionDecision (allow/deny/ask), permissionDecisionReason
PermissionRequesthookSpecificOutputdecision.behavior (allow/deny)
SessionStart, Setup, SubagentStartcontext onlyhookSpecificOutput.additionalContext

Với PreToolUse, các giá trị permissionDecision:

  • "allow" — bỏ qua prompt permission interactive (nhưng deny rule vẫn áp dụng)
  • "deny" — hủy tool call, gửi permissionDecisionReason về cho Claude
  • "ask" — hiện prompt permission như bình thường
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Use rg instead of grep for better performance"
  }
}

Với UserPromptSubmit, dùng hookSpecificOutput.additionalContext để inject text vào context của Claude:

{
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "Current branch: release-42. Deploy freeze until Friday."
  }
}

Hook và permission mode: PreToolUse hook fire trước mọi kiểm tra permission mode, kể cả bypassPermissions. Một hook trả permissionDecision: "deny" block được tool ngay cả với --dangerously-skip-permissions. Chiều ngược lại thì không: "allow" không vượt qua được deny rule. Hook có thể siết chứ không nới.

Ví dụ tự động hóa

Auto-format code sau khi edit

Chạy Prettier trên mọi file Claude edit. Dùng PostToolUse với matcher Edit|Write, trích file path bằng jq:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

Block edit vào file được bảo vệ

Ngăn Claude sửa file nhạy cảm như .env, package-lock.json, hay bất cứ gì trong .git/. Script kiểm tra path và exit 2 để block.

Lưu vào .claude/hooks/protect-files.sh:

#!/bin/bash
# protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")

for pattern in "${PROTECTED_PATTERNS[@]}"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
    exit 2
  fi
done

exit 0

Cho script quyền chạy rồi đăng ký PreToolUse hook:

chmod +x .claude/hooks/protect-files.sh
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
          }
        ]
      }
    ]
  }
}

Block lệnh Bash nguy hiểm

Dùng JSON output để deny và giải thích cho Claude:

#!/bin/bash
# .claude/hooks/block-rm.sh
COMMAND=$(jq -r '.tool_input.command')

if echo "$COMMAND" | grep -q 'rm -rf'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Destructive command blocked by hook"
    }
  }'
else
  exit 0  # không có quyết định; permission flow bình thường áp dụng
fi

Lưu ý quan trọng: exit 0 không có output không phê duyệt tool call — nó chỉ nghĩa là "hook không có ý kiến", và tool call tiếp tục qua permission flow bình thường. Hook có thể deny nhưng im lặng thì không allow.

Notification khi Claude cần input

Nhận desktop notification khi Claude chờ input. Dùng Notification event:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "notify-send 'Claude Code' 'Claude Code needs your attention'"
          }
        ]
      }
    ]
  }
}

(macOS: osascript -e 'display notification ...'; Windows: PowerShell MessageBox.) Matcher rỗng fire trên mọi loại; đặt permission_prompt hoặc idle_prompt để lọc hẹp hơn.

Log mọi lệnh Bash

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt"
          }
        ]
      }
    ]
  }
}

Re-inject context sau compaction

Dùng SessionStart với matcher compact. Mọi text ghi ra stdout được thêm vào context của Claude:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Reminder: use Bun, not npm. Run bun test before committing.'"
          }
        ]
      }
    ]
  }
}

Prompt-based hook — verify bằng phán đoán

Với quyết định cần phán đoán, dùng type: "prompt". Model (Haiku mặc định) trả yes/no dạng JSON: "ok": true cho phép tiếp tục, "ok": false thì reason được feed lại cho Claude:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}."
          }
        ]
      }
    ]
  }
}

Menu /hooks, disable, và debug

/hooks trong Claude Code để mở trình duyệt read-only cho các hook đã cấu hình — hiện mỗi event với số lượng hook, cho drill vào matcher và xem chi tiết handler cùng nguồn (User, Project, Local, Plugin, ...). Để thêm/sửa/xóa hook, edit JSON trực tiếp hoặc nhờ Claude làm.

Để xóa hook, xóa entry khỏi file settings. Để tạm tắt tất cả hook mà không xóa, đặt "disableAllHooks": true trong settings. Không có cách tắt một hook riêng lẻ.

Debug nhanh:

  • Run /hooks xác nhận hook xuất hiện đúng event; matcher case-sensitive, phải khớp chính xác tool name.
  • Test script thủ công: echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh; echo $?
  • Nếu "command not found": dùng absolute path hoặc ${CLAUDE_PROJECT_DIR}; thêm "args": [] để chuyển sang exec form.
  • Nếu "jq: command not found": cài jq hoặc dùng Python/Node để parse JSON.
  • Nếu script không chạy: chmod +x ./my-hook.sh.
  • Xem debug log đầy đủ: claude --debug-file /tmp/claude.log rồi tail -f /tmp/claude.log.

Stop hook và block cap

Stop hook fire mỗi khi Claude hoàn tất phản hồi, không chỉ lúc hoàn thành task. Claude Code cho phép tối đa 10 continuation do Stop hook kích hoạt trong một turn; sau cap này Claude dừng dù hook tiếp tục block. Script vẫn nên kiểm tra field stop_hook_active và exit sớm nếu là true:

#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0  # cho phép Claude dừng
fi
# ... phần còn lại của hook

Xem thêm

  • content/en/docs/claude-code/hooks.md — Hooks reference: full event schema, JSON output, async/HTTP/MCP tool hooks, security considerations.
  • content/en/docs/claude-code/hooks-guide.md — Automate actions with hooks: hướng dẫn quickstart với ví dụ.
  • content/en/docs/claude-code/settings.md — Cấu hình settings và thứ tự resolve file.
  • content/en/docs/claude-code/slash-commands.md — Slash command (/hooks).
  • content/en/docs/claude-code/plugins.md — Đóng gói hook trong plugin để share.
  • Chapter 09 — Permissions: cách permission rule và deny list tương tác với hook.
Nguồn của chương

Tài liệu chính thức duy nhất từ Anthropic.

Claude Code docs
Trở về mục lục