Bỏ qua điều hướng

Công khaiDeep dive

Guardrail trong Claude Code và AgentKit (1/2): cơ chế hook và các lớp cấu hình

Phần 1/2: lời dặn trong prompt chỉ là gợi ý. Guardrail thật là hook chạy trong harness của Claude Code, chặn tool call bằng exit 2. Cơ chế hook, bảy nhóm guardrail của AgentKit và các lớp settings.

15 phút đọc · v1.0.0 · Cập nhật 09/06/2026

EN · Guardrails in Claude Code and AgentKit (1/2): the hook mechanism and config layers · Đọc bản tiếng Anh

Bản tương tác

Bài có kèm một trang tương tác: sơ đồ, tình huống và session minh hoạ. Link mở được tạo mới mỗi lần bấm.

Tóm tắt nhanh: Lời dặn trong prompt chỉ là gợi ý, model có thể quên hoặc lệch. Guardrail (hàng rào an toàn) thật là hook chạy trong harness của Claude Code, kiểm từng tool call trước khi nó chạy và chặn bằng exit 2. Bài này giải thích cơ chế đó, rồi mổ từng lớp chặn trong AgentKit: lớp nào chặn bằng code, lớp nào chỉ là lời dặn, cùng những chỗ hở bạn nên kiểm trước khi tin.

Khi agent có quyền đọc file, chạy shell, sửa code và mở PR, một dòng dặn trong prompt là chưa đủ. Cần một lớp kiểm tra đứng trước lúc tool chạy.

Công khai: mình là maintainer của AgentKit và có quan hệ affiliate: link AgentKit trong bài là link affiliate, mua qua link này bạn được giảm 30%.

Ghi chú: bản beta hiện tại của AgentKit (từ 3.0.0-beta.35, 21/10/2026) đã đổi format tên skill từ ak:<tên> sang ak-<tên>, nên lệnh trong bài đã cập nhật theo tên mới: Claude Code gõ /ak-plan, Codex gõ $ak-plan. Nếu bạn còn dùng bản cũ hơn, đổi ak- thành ak: là ra lệnh tương ứng.

AgentKit là tên mới của ClaudeKit. Phần cấu hình trong bài là ảnh chụp theo bản claudekit-engineer@2.19.1 lúc mình viết (tháng 6/2026), nên tên file (.ck.json, .ckignore), tên hook, key và biến môi trường (CK_*) được giữ nguyên như bản đó. Lúc rà lại với AgentKit 3.0.0-beta.1 (tháng 10/2026), lệnh đã chuyển sang tiền tố /ak:, .ck.json và CK_SIMPLIFY_DISABLED vẫn được đọc (tài liệu chuyển đổi có nhắc tuỳ chọn chuyển cấu hình sang config.yaml, nhưng hook trong bản cài vẫn đọc .ck.json), còn hook workflow-artifact-gate không có trong bản cài mới. Phần về gate này giữ lại để bạn hiểu cách kiểm artifact, nhưng hãy kiểm thư mục .claude/hooks/ trên máy trước khi bật. Mình sẽ giải thích cơ chế chung của Claude Code trước, rồi mới đi vào từng lớp chặn của AgentKit.

Một việc nhỏ, nhiều quyền quá rộng

Bạn giao cho agent một việc nhỏ: sửa phần kiểm tra dữ liệu đầu vào trong một module. Nó muốn chắc hơn nên quét thêm vài thư mục, mở một file cấu hình nhạy cảm, rồi tiện tay gom luôn vài chỗ dọn dẹp không liên quan vào cùng một PR.

Nhìn qua thì mọi thứ vẫn ổn, test vẫn đạt. Nhưng ngữ cảnh đã lẫn file rác, secret có thể đã đi vào transcript (toàn bộ bản ghi hội thoại giữa bạn và agent), còn việc duyệt PR thì bị loãng vì phần cần sửa và phần "tiện tay" nằm chung một chỗ.

So sánh hai trường hợp:

Hành động của agentKhông có guardrailCó guardrail
Glob quá rộngNgữ cảnh lẫn file rácscout-block trả exit 2
Đọc .envSecret vào bản ghi hội thoạiprivacy-block hỏi người dùng
Dọn dẹp ngoài phạm viDuyệt PR bị loãngQuy tắc và hard-gate nhắc quay lại việc chính
Test đạt nên làm tiếpThiếu bước kiểm phạm visimplify-gate hoặc chốt duyệt kiểm lại diff trước khi phát hành

Agent không tự nhiên cẩn thận hơn. Chỉ là trước khi nó làm gì đó, có một lớp đứng ra hỏi: việc này có nên được phép chạy không?

Ý chính của cả bài: hook là lớp chặn thật. Guardrail không làm model thông minh hơn, nó kiểm tra hành động trước khi tool chạy. Quy tắc, hard-gate và guard skill là các lớp định hướng thêm, có ích nhưng vẫn phụ thuộc vào việc model hoặc người dùng có làm theo hay gọi tới hay không.

Guardrail là gì trong agentic coding

Guardrail là lớp đứng giữa agent và hành động: việc rủi ro thì chặn, việc đáng nghi thì cảnh báo. Nó giống lan can ở mép cầu thang: không làm bạn đi giỏi hơn, nhưng giảm khả năng một bước hụt thành tai nạn. Khác với lời dặn trong prompt, guardrail chạy ở tầng điều phối tool, không phụ thuộc model có "nhớ" hay không.

Phân biệt guardrail và lời dặn

GuardrailLời dặn
Chạy ở đâuTrong harness, ngoài modelTrong ngữ cảnh, model đọc
Ép buộcCode chặn thậtModel tự nguyện làm theo
Ngữ cảnh dàiVẫn chạyDễ quên hoặc trượt
Model hiểu lệchVẫn chặn (nếu hook không crash)Lệch theo model
Sửa đổiPhải sửa code hoặc cấu hìnhSửa chữ là xong

Guardrail hoạt động thế nào

Harness là lớp runtime bọc quanh model. Model chỉ quyết định "tôi muốn đọc file này" hay "tôi muốn chạy lệnh này". Harness mới là bên nhận tool call, quản quyền, gọi hook, cho tool chạy hoặc chặn, rồi trả kết quả lại cho model. Hook là một script nhỏ mà harness tự gọi ở các thời điểm cố định trong vòng đời một tool call (lúc nhận prompt, trước khi tool chạy, sau khi tool chạy) để xen vào kiểm tra. Vì mọi hành động thật đều đi qua harness, đây là chỗ gắn guardrail.

Đọc thêm phần nền: Harness Engineering là gì? của Duy /zuey/.

Hook nằm ở đâu trong vòng đời Claude Code

Hook không nằm trong model. Nó nằm trong vòng đời của Claude Code: settings.json quyết định hook nào được gọi; .ck.json cùng biến môi trường quyết định hook đó làm gì khi chạy. Kết quả hook trả về có thể cho chạy, chặn, chèn thêm ngữ cảnh, hoặc ghi trạng thái và file kết quả sau khi tool hay session kết thúc. Chi tiết về hook của AgentKit có thể xem thêm ở VividKit Guides.

Một tool call đi qua guardrail

Điểm mấu chốt: hook chạy trước tool trả về một exit code. exit 0 cho tool chạy, exit 2 chặn và đẩy lý do về cho model. Ba điểm chèn chính có tên kỹ thuật tương ứng: UserPromptSubmit (lúc nhận prompt), PreToolUse (trước tool), PostToolUse (sau tool). Riêng Stop và SubagentStop chạy khi session hoặc subagent kết thúc.

Một tool call đi qua guardrail: User gửi prompt qua hook UserPromptSubmit tới Model; Model gửi tool call tới hook PreToolUse; PreToolUse trả exit code rẽ bốn nhánh: exit 0 cho tool chạy, exit 1 hoặc mã khác và hook crash vẫn cho tool chạy (bẫy), exit 2 hoặc JSON deny chặn và đẩy stderr về model; sau tool là PostToolUse, cuối session là Stop và SubagentStop
Chỉ exit 2 hoặc JSON deny mới chặn thật. exit 1 và hook crash đều để tool chạy tiếp.

Exit code là con số một tiến trình trả về khi kết thúc. Theo tài liệu hook của Claude Code, Claude Code đọc con số đó để quyết định tool có chạy hay không:

Exit codeTool có chạy khôngChuyện gì xảy ra
exit 0CóThành công. Nội dung stdout được đọc như kết quả JSON.
exit 2Không, bị chặnBỏ stdout, đẩy nội dung stderr về cho model làm thông báo lỗi.
exit 1 hoặc mã khácCóLỗi nhưng không chặn. Lỗi được báo ra, tool vẫn chạy tiếp.

Trong các mã thoát, chỉ exit 2 mới chặn thật. Hook muốn ép chính sách bằng mã thoát phải dùng đúng exit 2. Dùng nhầm exit 1 theo thói quen Unix thì guard tưởng đang bật mà thực ra vẫn mở. Cách chặn còn lại là exit 0 kèm JSON trên stdout: với PreToolUse, trả hookSpecificOutput.permissionDecision là "deny" cũng chặn tool, lý do ghi trong permissionDecisionReason (PreToolUse decision control).

Hai kiểu chặn

Chặn cứng bằng code. Hook trả exit 2, hoặc trả quyết định deny qua JSON, thì tool không chạy. Nhưng nếu hook crash, Claude Code vẫn cho tool chạy tiếp, gọi là fail-open. Một lỗi trong hook sẽ làm lớp chặn đó tắt âm thầm.

Chặn mềm bằng chữ. Thêm chữ vào ngữ cảnh (quy tắc, hard-gate) và model phải tự làm theo. Mạnh hay yếu tuỳ vào model.

Tách guardrail thành nhiều lớp đem lại mấy lợi ích:

  • Giữ ngữ cảnh sạch và giữ secret ngoài bản ghi hội thoại.
  • Giữ nhịp làm việc: lập kế hoạch, rút gọn, rồi duyệt.
  • Chặn sớm trước khi rủi ro lan ra, khi agent định đọc .env, quét quá rộng hoặc kéo phần dọn dẹp ngoài phạm vi vào PR.
  • Các lớp bù cho nhau: hook có thể fail-open, quy tắc có thể bị model bỏ qua, guard skill có thể không được gọi. Nhiều lớp giúp bớt phụ thuộc vào một điểm chặn duy nhất.

Bảy nhóm guardrail trong AgentKit

Sau một lần cài mới, lớp chặn truy cập file của AgentKit nằm ở các hook PreToolUse như scout-block và privacy-block. Nếu session Claude Code đang chạy ở chế độ bỏ qua hộp hỏi quyền, các hook này vẫn là lớp chặn chính. Nhưng hook của AgentKit là fail-open: crash hoặc bị tắt thì tool call có thể đi tiếp.

NhómCơ chếVí dụMức ép buộc
Chặn file và đường dẫnPreToolUsescout-block, privacy-blockCode, fail-open
Chặn sai bướcUserPromptSubmitsimplify-gateCode
Thêm ngữ cảnhUserPromptSubmitdev-rules-reminderCode
Giữ tên rõ nghĩaPre, Post, Stopdescriptive-nameCode, chỉ nhắc
Hard-gate trong skillXML trong markdown<HARD-GATE>Lời dặn
Quy tắc dạng lời dặnCLAUDE.mdreview-auditLời dặn
Guard skillNgười dùng gọiak-security, ak-shipNgười dùng

Mỗi nhóm có mục riêng ở Phần 2: chặn file và đường dẫn ở mục scout-block và privacy-block, chặn sai bước ở mục simplify-gate và workflow-artifact-gate, hard-gate ở mục hard-gate XML, thêm ngữ cảnh và quy tắc ở mục dev-rules-reminder, guard skill ở mục cuối của các lớp chặn. descriptive-name chỉ có trong bảng hook.

Chín tình huống quen thuộc

Tình huốngLớp xử lý
Đọc node_modules/react/scout-block, exit 2
Đọc .env để kiểm keyprivacy-block, exit 2 kèm hộp hỏi phê duyệt
Glob **/*.ts ở thư mục gốcscout-block, chặn mẫu quá rộng
Prompt đòi phát hành khi diff đã phình tosimplify-gate, nếu gate.enabled=true
Bắt đầu code khi chưa có kế hoạch hoặc chưa duyệtHARD-GATE của ak-cook
Đổi ngưỡng mà người dùng đã chốtQuy tắc review-audit: hỏi lại trước khi đổi
File mới có tên mơ hồdescriptive-name, PreToolUse(Write)
Kế hoạch dùng link hoặc chữ sai định dạngplan-format-kanban, PostToolUse(Edit/Write/MultiEdit)
Hai thành viên trong nhóm agent cùng sửa một fileQuy tắc team-coordination, chỉ trong Agent Team

Settings và các lớp cấu hình

Đây là nơi kiểm xem hook có chạy hay không. Hai lớp cấu hình quyết định lớp chặn nào chạy: settings.json gắn hook vào vòng đời của Claude Code, còn .ck.json bật tắt từng hook và chỉnh ngưỡng.

text
# Phạm vi toàn cục
~/.claude/settings.json
~/.claude/.ck.json
~/.claude/hooks/*.cjs

# Phạm vi dự án
.claude/settings.json
.claude/.ck.json
.claude/hooks/*.cjs

settings.json: hook nào chạy ở mốc nào

Đoạn dưới chỉ minh hoạ một lát cắt PreToolUse của bản cài mới: AgentKit cung cấp scout-block và privacy-block, rồi gắn chúng vào vòng đời để Claude Code gọi trước khi tool chạy. Cấu hình hook đầy đủ trên máy còn nhiều sự kiện khác; nơi cần xem là settings.json của Claude Code.

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Glob|Grep|Read|Edit|Write",
        "hooks": [
          { "type": "command", "command": "node \".claude/hooks/scout-block.cjs\"" },
          { "type": "command", "command": "node \".claude/hooks/privacy-block.cjs\"" }
        ]
      }
    ]
  }
}

settings.json là bản đồ vòng đời: khi người dùng gửi prompt thì hook nào chạy, trước khi tool chạy thì hook nào được gọi, sau khi tool xong thì hook nào xử lý tiếp. Với bản cài toàn cục, CLI đổi lệnh dùng đường dẫn tương đối trong mẫu thành dạng node "$HOME/.claude/hooks/scout-block.cjs". Với bản cài theo dự án, đường dẫn thường giữ dạng .claude/hooks/....

Đọc trạng thái hook theo ba lớp

Đừng gom mọi thứ thành "mặc định bật" hay "mặc định tắt". Trạng thái của một hook có ba lớp khác nhau:

LớpNghĩa khi kiểm
Có file scriptFile .cjs nằm trong .claude/hooks/. Có file chưa có nghĩa là hook đang chạy.
Đã gắnsettings.json đã gắn script vào một sự kiện trong vòng đời của Claude Code. Chưa gắn thì Claude Code không tự gọi hook đó.
Cờ runtimeKhi hook đã được gọi, code bên trong đọc .ck.json, DEFAULT_CONFIG hoặc biến môi trường để quyết định có chạy tiếp không. Vài lớp chặn còn có công tắc con như privacyBlock hoặc simplify.gate.enabled.

Bảng dưới là ảnh chụp danh sách hook theo bản claudekit-engineer@2.19.1. Cột cuối là cách kích hoạt từng hook để tự kiểm. Lúc mình viết bài, hai hook task-completed-handler và teammate-idle-handler đang được lên kế hoạch bỏ ở bản kế tiếp, nên đừng coi chúng là hook lâu dài.

HookSự kiệnTrạng thái sau khi càiCách kích hoạt để kiểm
session-initSessionStartĐã gắn, cờ true. Chuẩn bị dự án và biến môi trường khi khởi động, resume, clear, compactMở, resume, clear hoặc compact một session (SessionStart(startup|resume|clear|compact))
usage-quota-cache-refreshSessionStart, UserPromptSubmit, PostToolUseĐã gắn, không đọc cờ riêng, cứ được gắn là chạy. Lưu cache lượng dùngMở session, gửi prompt mới, hoặc cập nhật Task/Todo
simplify-gateUserPromptSubmitScript được gọi (cờ true), nhưng simplify.gate.enabled mặc định false nên chưa chặnGửi prompt có ý định ship, merge, pr, deploy, publish khi diff đủ lớn
dev-rules-reminderUserPromptSubmitĐã gắn, cờ true. Chèn quy tắc phát triển và ngữ cảnhGửi prompt mới. Quy tắc được chèn theo TTL
subagent-initSubagentStartĐã gắn, cờ true. Chèn ngữ cảnh cho subagentKhởi động subagent qua Task hoặc luồng agent
descriptive-namePreToolUse(Write)Đã gắn, cờ true. Nhắc đặt tên file, script rõ nghĩaĐể Claude định tạo file bằng Write
scout-blockPreToolUse trên Bash, Glob, Grep, Read, Edit, WriteĐã gắn, cờ true. Chặn thư mục nặng và glob quá rộngĐể Claude đọc node_modules, dist hoặc glob quá rộng
privacy-blockPreToolUse trên Bash, Glob, Grep, Read, Edit, WriteĐã gắn, cờ true, thêm privacyBlock là true. Chặn đường dẫn chứa secret, hỏi phê duyệtĐể Claude đọc .env, file key, đường dẫn chứa secret
plan-format-kanbanPostToolUse(Edit/Write/MultiEdit)Đã gắn, không đọc cờ riêng. Cảnh báo kế hoạch sai định dạngĐể Claude sửa hoặc ghi một file kế hoạch
session-statePostToolUse, SubagentStop, StopĐã gắn, cờ true. Ghi trạng thái session và việcCập nhật Task/Todo, kết thúc subagent hoặc kết thúc một lượt
cook-after-plan-reminderSubagentStop(Plan)Đã gắn. Không có key riêng trong DEFAULT_CONFIG.hooks, nhưng isHookEnabled() chỉ tắt khi cờ là false, nên thiếu key vẫn bậtĐể subagent Plan kết thúc
workflow-artifact-gateChưa gắnCờ false. Phải tự bật: gắn hook và bật cờ, cấu hình gateGắn vào UserPromptSubmit hoặc PreToolUse(Bash), hoặc chạy script với --stage. Bản cài mới chưa tự kích hoạt chế độ hook
task-completed-handlerTaskCompletedDùng cho Agent Teams khi một việc hoàn tất. Đang được lên kế hoạch bỏHoàn tất một việc trong Agent Teams
teammate-idle-handlerTeammateIdleDùng cho Agent Teams khi một thành viên rảnh việc. Đang được lên kế hoạch bỏĐể một thành viên trong Agent Teams hết việc và chuyển sang chờ
team-context-injectChưa gắnThiếu key nghĩa là bật. Chỉ có ý nghĩa khi lớp workflow hoặc nhóm agent gọi tớiGắn vào SubagentStart, rồi khởi động subagent của nhóm. Script chỉ có tác dụng khi agent id thuộc nhóm
usage-context-awarenessChưa gắnCờ true. Lớp bọc cũ quanh hook làm mới cache lượng dùng, không có trong mẫu settings.jsonGắn vào sự kiện mong muốn, hoặc dùng hook làm mới cache đang gắn sẵn

Điểm dễ sót: isHookEnabled() chỉ tắt khi hooks.<name> là false, key thiếu thường được xem là bật. Nhưng vài lớp chặn còn có công tắc riêng: privacy-block vẫn đọc key cũ privacyBlock=false, còn simplify-gate có simplify.gate.enabled và biến môi trường CK_SIMPLIFY_DISABLED=1.

Tự bật tắt và kiểm tra hook

Muốn chỉnh nhanh, sửa .ck.json ở phạm vi cần tác động. Cấu hình dự án được ưu tiên hơn cấu hình toàn cục khi key được đặt rõ; key thiếu thì kế thừa hoặc dùng giá trị mặc định. Với hook đã gắn trong settings.json, đặt false là tắt, đặt true là bật lại sau khi phạm vi cao hơn đã tắt. Tên hook cũng chính là key trong hooks.<name>.

json
// .claude/.ck.json hoặc ~/.claude/.ck.json
{
  "hooks": {
    "scout-block": false,
    "privacy-block": true
  },
  "privacyBlock": true,
  "simplify": {
    "gate": {
      "enabled": true
    }
  }
}
Muốn làmChỉnh ở đâuLưu ý
Tắt một hook đã gắn.ck.json: {"hooks":{"scout-block":false}}Script vẫn còn, nhưng lúc chạy sẽ cho qua.
Bật lại hook đã bị tắt ở cấu hình toàn cục.ck.json của dự án: {"hooks":{"scout-block":true}}Key đặt rõ ở dự án ghi đè cấu hình toàn cục.
Tắt lớp chặn secret.ck.json: {"hooks":{"privacy-block":false}}, hoặc key cũ {"privacyBlock":false}privacyBlock=false là key cũ nhưng vẫn có hiệu lực.
Bật simplify-gate chặn thật.ck.json: { "hooks": { "simplify-gate": true }, "simplify": { "gate": { "enabled": true } } }Thiếu simplify.gate.enabled=true thì gate chưa bật chế độ chặn.
Tắt simplify-gate theo session hoặc phạm visettings.json: { "env": { "CK_SIMPLIFY_DISABLED": "1" } }Biến môi trường mạnh hơn cấu hình gate.
Bật workflow-artifact-gatesettings.json: gắn vào UserPromptSubmit và PreToolUse(Bash); .ck.json: {"hooks":{"workflow-artifact-gate":true}}Bản cài mới chưa gắn hook này, nên chỉ sửa .ck.json là chưa đủ.

Muốn biết hook có được kích hoạt không, hãy nhìn sự kiện trong vòng đời chứ đừng nhìn tên file. Cùng một script chỉ chạy khi sự kiện và matcher trong settings.json khớp với hành động hiện tại. Hook chưa gắn thì cần gắn trước, hoặc gọi thẳng qua CLI nếu script hỗ trợ.

json
// ~/.claude/settings.json hoặc .claude/settings.json
{
  "env": {
    "CK_SIMPLIFY_DISABLED": "1"
  }
}

Đọc tiếp Phần 2: từng lớp chặn, chỗ hở và cách dùng: mổ từng lớp guardrail của AgentKit, lớp nào chặn bằng code, lớp nào chỉ là lời dặn.

Dùng bài này trong agent của bạn

Lấy API key

Chép đoạn dưới, dán vào Claude Code, Codex hay agent bạn đang dùng. Agent đọc bài rồi đối chiếu với repo đang mở.

Dán vào agent
Đọc bài "Guardrail trong Claude Code và AgentKit (1/2): cơ chế hook và các lớp cấu hình" trên modernEngineer rồi áp dụng vào repo này.
1. Gọi tool content_get của MCP server modernengineer với locale "vi", slug "guard-rails-in-claude-code-and-claudekit". Nếu chưa nối MCP hoặc bị từ chối quyền, đọc thẳng trang https://modernengineer.dev/vi/library/guard-rails-in-claude-code-and-claudekit.
2. Tóm tắt các bước chính của bài, chỉ ra bước nào áp dụng được cho repo này.
3. Đề xuất thay đổi cụ thể và hỏi tôi trước khi sửa file.
Nối MCP cho Claude Code (một lần)
terminal
claude mcp add --transport http modernengineer \
  https://api.modernengineer.dev/mcp \
  --header "Authorization: Bearer <API key>"
Cách nối Codex, Cursor và client khác

Bài mở cho mọi người: chưa nối MCP thì agent vẫn đọc được thẳng từ link bài.

Phiên bản
v1.0.0
Cập nhật
09/06/2026
Giấy phép
Bảo lưu mọi quyền
Slug
guard-rails-in-claude-code-and-claudekit