Khám phá Learn Stream About Jokes
COHORT 2 Foundation — Claude Code — 3 buổi Zoom · 21 · 24 · 28/07, 20:00 Đăng ký →
Core

Hooks — biến quy ước thành luật

Script chạy tự động ở những điểm xác định trong phiên. Skill là lời khuyên; hook là luật. Kèm 6 hook thật, đã chạy thử.

Skill là lời khuyên — model có thể nói vòng qua nó. Hook là luật — script chạy bất kể model đang nhớ gì, còn bao nhiêu token, hay có “đồng ý” hay không.

Đó là toàn bộ lý do hook tồn tại.

Hook sống ở đâu

Bốn nơi, ưu tiên từ trên xuống:

File Phạm vi
Managed policy settings Toàn tổ chức
~/.claude/settings.json Mọi project của bạn
.claude/settings.json Một project — commit vào git
.claude/settings.local.json Một project, chỉ máy bạn — git bỏ qua

Hook nói chuyện với Claude thế nào

Vào: JSON qua stdin. Lấy đường dẫn file bằng jq -r '.tool_input.file_path'.

Ra: JSON qua stdout, và chỉ khi exit 0.

Đây là chỗ hầu hết ví dụ trên mạng sai:

exit 1 không chặn gì cả. Tài liệu ghi rõ: “Claude Code treats exit code 1 as a non-blocking error and proceeds with the action, even though 1 is the conventional Unix failure code.”

exit 2 có chặn — nhưng khi đó “Claude Code ignores stdout and any JSON in it”, chỉ đọc stderr. Nghĩa là bạn mất luôn thông điệp có cấu trúc.

Nên cách đúng để chặn một PreToolUse là: in JSON ra stdout rồi exit 0.

{"hookSpecificOutput":{
  "hookEventName":"PreToolUse",
  "permissionDecision":"deny",
  "permissionDecisionReason":"Lý do, Claude sẽ đọc câu này"
}}

⚠️ Lưu ý: mỗi sự kiện có tên trường riêng. PreToolUse dùng permissionDecision. PostToolUse dùng additionalContext. UserPromptSubmit dùng decision: "block". Không có trường nào tên block hay feedback — nếu bạn thấy chúng trong một bài blog, ví dụ đó không chạy.


Sáu hook thật

Tất cả nằm trong object "hooks" của .claude/settings.json. Mỗi hook dưới đây đã được chạy thử.

1. Chặn sửa file khi đang ở nhánh main

Claude hay refactor thẳng trên main thay vì tách nhánh.

"PreToolUse": [{
  "matcher": "Edit|MultiEdit|Write",
  "timeout": 5,
  "statusMessage": "Kiểm tra nhánh git…",
  "hooks": [{
    "type": "command",
    "command": "b=$(git branch --show-current 2>/dev/null); if [ \"$b\" = \"main\" ] || [ \"$b\" = \"master\" ]; then printf '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Đang ở nhánh %s. Tạo feature branch trước.\"}}\\n' \"$b\"; fi; exit 0"
  }]
}]

💡 Tip: matcher là regex trên tên tool, không neo hai đầu. Edit|MultiEdit|Write khớp cả ba.

2. Chặn đọc/ghi secret

"PreToolUse": [{
  "matcher": "Read|Write|Edit",
  "timeout": 3,
  "hooks": [{
    "type": "command",
    "command": "p=$(jq -r '.tool_input.file_path // empty'); if printf '%s' \"$p\" | grep -qE '(\\.env|\\.ssh/|aws/credentials)'; then printf '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Chặn truy cập secret: %s\"}}\\n' \"$p\"; fi; exit 0"
  }]
}]

Không khớp thì hook im lặng và exit 0 — Claude làm việc bình thường.

3. Tự format sau khi ghi file

"PostToolUse": [{
  "matcher": "Edit|MultiEdit|Write",
  "timeout": 15,
  "statusMessage": "Đang format…",
  "hooks": [{
    "type": "command",
    "command": "p=$(jq -r '.tool_input.file_path // empty'); case \"$p\" in *.js|*.jsx|*.ts|*.tsx) [ -f \"$p\" ] || exit 0; npx prettier --write \"$p\" >/dev/null 2>&1 && printf '{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":\"Đã format %s bằng Prettier.\"}}\\n' \"$p\" ;; esac; exit 0"
  }]
}]

4. Trả lỗi TypeScript ngược vào vòng lặp của Claude

Claude viết code chạy được nhưng sai type. tsc bắt được, và additionalContext đẩy lỗi thẳng vào lượt sau.

"PostToolUse": [{
  "matcher": "Edit|MultiEdit|Write",
  "timeout": 30,
  "statusMessage": "Kiểm tra type…",
  "hooks": [{
    "type": "command",
    "command": "p=$(jq -r '.tool_input.file_path // empty'); case \"$p\" in *.ts|*.tsx) ;; *) exit 0 ;; esac; out=$(npx tsc --noEmit 2>&1) || jq -cn --arg e \"$(printf '%s' \"$out\" | head -n 10)\" '{hookSpecificOutput:{hookEventName:\"PostToolUse\",additionalContext:(\"tsc báo lỗi type:\\n\"+$e)}}'; exit 0"
  }]
}]

💡 Tip: PostToolUse không chặn được. Nó chỉ đưa thông tin về. Muốn chặn thì phải làm ở PreToolUse.

5. Nhắc lại luật repo sau mỗi lần nén context

Phiên dài, Claude nén lịch sử, và bản tóm tắt hay quên mất CLAUDE.md.

"PostCompact": [{
  "timeout": 5,
  "hooks": [{
    "type": "command",
    "command": "[ -f CLAUDE.md ] || exit 0; jq -cn --arg r \"$(head -n 50 CLAUDE.md)\" '{hookSpecificOutput:{hookEventName:\"PostCompact\",additionalContext:(\"Context vừa nén. Nhắc lại luật repo:\\n\"+$r)}}'"
  }]
}]

6. Ping desktop khi Claude cần bạn

"Notification": [{
  "hooks": [{
    "type": "command",
    "command": "osascript -e 'display notification \"Claude Code cần bạn trả lời.\" with title \"Claude Code\"'"
  }]
}]

Windows: đổi sang powershell.exe -Command "[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code cần bạn trả lời.','Claude Code')"


Sự kiện nào chặn được

Claude Code · Hooks

Hai sự kiện chặn được.
Tám cái còn lại chỉ nhìn.

Nhầm chỗ này là hook của bạn chạy, in JSON, rồi để Claude làm điều bạn vừa cấm. Bảng dưới đây phân biệt rõ.

Hiện 13 / 13

Chặn được

2

Chỉ hai sự kiện này ngăn được một hành động. Mọi thứ khác chỉ quan sát.

PreToolUse matcher: regex tên tool

Chạy ngay trước một tool. Chặn bằng permissionDecision: "deny" trên stdout, exit 0.

Đây là nơi khoá file, chặn secret, cấm lệnh nguy hiểm.

UserPromptSubmit

Chạy khi bạn bấm Enter. Chặn bằng decision: "block".

Cũng dùng để tự chèn luật repo vào mỗi prompt.

Không chặn được — chỉ quan sát hoặc bổ sung

8
PostToolUse

Sau khi tool chạy xong. Trả thông tin về bằng additionalContext.

Format code, lint, chạy test, đẩy lỗi type ngược vào vòng lặp.

SessionStart

Lúc mở phiên hoặc --resume. Nạp env, chèn git log gần đây.

SessionEnd

Lúc đóng phiên.

PreCompact

Ngay trước khi nén lịch sử.

PostCompact

Ngay sau khi nén. Chỗ tốt nhất để nhắc lại CLAUDE.md.

Notification

Khi Claude cần bạn duyệt hoặc trả lời. Ping desktop, Slack, Discord.

Stop

Khi Claude kết thúc một lượt.

SubagentStop

Khi một subagent kết thúc.

Trường chung của mọi hook

3
timeout số giây

Quá hạn thì hook bị huỷ.

statusMessage chuỗi

Dòng chữ hiện cạnh spinner trong lúc hook chạy.

matcher regex

Lọc theo tên tool. Không neo hai đầu — Bash khớp Bash, mcp__.* khớp mọi MCP tool.


Ba lỗi hay gặp

Dùng exit 1 để chặn. Không chặn. Claude vẫn làm. Đây là lỗi phổ biến nhất, vì 1 là mã lỗi quen thuộc của Unix.

Trả {"block": true} hoặc {"feedback": "…"}. Không có hai trường này. PreToolUse dùng permissionDecision, PostToolUse dùng additionalContext, UserPromptSubmit dùng decision.

Đặt hook chặn ở PostToolUse. Lúc đó tool đã chạy xong rồi. Muốn ngăn thì phải ở PreToolUse.

💡 Tip: hook chỉ nạp lúc mở phiên. Sửa settings.json xong phải khởi động lại Claude Code, nếu không bạn đang chạy phiên bản cũ mà tưởng đã cập nhật.

0:00

Chia sẻ ảnh

Bắt đầu gõ để tìm kiếm...