bài 1, tôi đã nhắc đến permission system như một trong ba thành phần core của Claude Code, và nói sẽ kể câu chuyện về aws s3 rb ở bài này. Câu chuyện đó sẽ đến cuối bài. Nhưng trước hết, cần hiểu hệ thống permission hoạt động như thế nào.

Claude Code có một permission layer nằm giữa model và tool. Trước mỗi tool call, binary check xem tool đó có được phép chạy không, và nếu có, có cần hỏi user không. Đây là lớp bảo vệ quan trọng nhất, đặc biệt khi làm việc với file system, git, hoặc cloud CLI.

Bài này cover bốn permission mode, cách cấu hình allowlist và deny list trong settings.json, cách PreToolUse hook tích hợp vào layer này, và cách truyền mode khi spawn agent.

Bốn permission mode

Claude Code có bốn mode, truyền qua tham số --permission-mode hoặc qua Agent tool khi spawn subagent:

ModeHành vi
ask (default)Hỏi user trước mọi tool call không nằm trong allowlist.
autoTự động chạy tool nếu match pattern trong allowlist. Không hỏi nếu đã whitelisted.
acceptEditsTự động accept mọi file edit (Read, Write, Edit) không cần confirm. Bash tool vẫn hỏi.
bypassPermissionsBỏ qua toàn bộ permission check. Không hỏi gì. Chạy thẳng.

Trong thực tế CLI hàng ngày, bạn ở mode ask. Khi CC hỏi “Allow Bash command?”, đó là mode ask hoạt động đúng thiết kế.

Mode auto không phải là “không hỏi gì hết”. Nó có nghĩa là: nếu tool đó match pattern trong permissions.allow, chạy tự động. Còn tool không match vẫn bị hỏi như thường. Đây là điểm nhiều người hiểu lầm.

acceptEdits là lựa chọn hợp lý khi bạn tin tưởng CC đang edit đúng file và muốn tốc độ. Nhưng vẫn giữ gate cho Bash, vốn nguy hiểm hơn.

bypassPermissions là mode bỏ qua tất cả. Không hỏi. Không check deny list. Không gì hết. Phần incident ở cuối bài sẽ giải thích tại sao mode này cần cẩn thận tuyệt đối khi dùng với agent.

Allowlist: permissions.allow

settings.json có key permissions.allow là một mảng pattern. Các tool match pattern này được chạy tự động mà không cần hỏi, bất kể mode là gì (trừ trường hợp mode là ask và tool không nằm trong list).

Cú pháp pattern:

{
  "permissions": {
    "allow": [
      "Bash",
      "Read",
      "Edit",
      "Write",
      "Bash(npm test)",
      "Bash(git status)",
      "Bash(git diff *)"
    ]
  }
}

Khi bạn viết "Bash" không có tham số, toàn bộ Bash tool được allow. Khi bạn viết "Bash(npm test)", chỉ lệnh npm test được auto-allow, còn các lệnh Bash khác vẫn hỏi.

Pattern matching theo prefix. "Bash(git diff *)" match mọi lệnh bắt đầu bằng git diff.

Trong session CLI, CC cũng hỏi “Allow and don’t ask again?” khi bạn confirm một tool call. Trả lời yes là CC tự thêm vào allowlist cho session đó. Để lưu vĩnh viễn thì sửa settings.json trực tiếp.

Ví dụ config thực tế trên máy tôi:

{
  "permissions": {
    "allow": [
      "Bash",
      "Edit",
      "Read",
      "Write",
      "WebFetch",
      "WebSearch",
      "Glob",
      "Grep",
      "Task",
      "Monitor"
    ]
  }
}

Allow rộng như trên vì tôi đã quen với setup này và có deny list để chặn những gì nguy hiểm (xem phần dưới).

Deny list: hard-block không override được

permissions.deny là mảng pattern tương tự, nhưng hành vi ngược lại: tool match pattern sẽ bị block cứng. Không thể override per-prompt, không thể bypass trong session.

{
  "permissions": {
    "deny": [
      "Bash(aws s3 rm *)",
      "Bash(aws s3 rb *)",
      "Bash(aws s3api delete-bucket *)",
      "Bash(aws s3api delete-object *)"
    ]
  }
}

Đây là config deny list trên máy tôi sau một sự cố. Bốn lệnh trên, dù model có request, dù agent có mode nào, dù user có confirm, đều bị block trước khi thực thi.

Deny list là layer “tay nhanh hơn não”. Trong một session dài, tốc độ cao, nhiều agent chạy song song, rất dễ click “Allow” theo phản xạ mà không đọc kỹ. Deny list loại trừ hoàn toàn khả năng đó với những command không bao giờ muốn chạy tự động.

Nguyên tắc tôi dùng: deny list cho những action không thể undo. Xóa S3 bucket không undo được. Xóa database production không undo được. Những gì reversible thì không cần deny, chỉ cần ask.

Thứ tự ưu tiên: deny thắng allow

Khi một tool match cả hai:

deny wins > allow

Nếu bạn có "Bash" trong allow và "Bash(rm -rf *)" trong deny, lệnh rm -rf / sẽ bị block dù Bash đã được allow toàn phần.

+----------------------------+
|  User message / Agent req  |
+----------------------------+
           |
           v
+----------------------------+
|  Deny list check (cứng)    |  <- Block ở đây: không chạy, không hỏi
+----------------------------+
           |
           v
+----------------------------+
|  Allow list check          |  <- Match: chạy tự động
+----------------------------+
           |
           v
+----------------------------+
|  Mode check (ask/auto...)  |  <- Fallback: hỏi user
+----------------------------+

PreToolUse hook: deny list lập trình được

settings.json deny list là static config. Nếu bạn cần logic phức tạp hơn, PreToolUse hook cho phép bạn viết shell script để intercept mọi tool call trước khi nó chạy.

{
  "hooks": {
    "PreToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/pre-tool-guard.sh"
          }
        ]
      }
    ]
  }
}

Hook nhận tool name và input qua stdin dưới dạng JSON. Nếu script exit non-zero, tool call bị block. Nếu exit 0, tool chạy bình thường.

Ví dụ dùng PreToolUse để guard:

  • Block git push --force nhưng cho phép git push bình thường (static deny list không làm được vì prefix match)
  • Ghi log mọi Bash command dài hơn 50 ký tự ra file audit
  • Hỏi confirm khi CC cố xóa file trong thư mục nhất định, dù global mode là auto

Cơ bản là deny list nhưng programmatic. Bài 9 sẽ đi sâu vào hook system và cách viết hook hữu dụng.

Mode khi spawn agent

Khi spawn subagent qua Agent tool (hoặc qua SDK), bạn truyền mode qua tham số. Đây là điểm quan trọng cần hiểu rõ.

Agent tool tham số:
- prompt: prompt giao việc
- mode: permission mode cho agent đó
- tools: danh sách tool agent được dùng (subset)

Best practice tôi theo:

Tình huốngMode nên dùng
Agent foreground, user ngồi chờask (default, không cần khai)
Agent background, safe tool (Read, Grep, Glob)auto
Agent background, viết code, không cần hỏi editacceptEdits
Agent cần chạy script tự động không có gì nguy hiểmauto với deny list cứng
KHÔNG BAO GIỜbypassPermissions với agent

Lý do rule cuối cùng, tôi sẽ giải thích qua một câu chuyện thật.

Incident: bucket production bị xóa vĩnh viễn

Tôi đang làm việc trên một project deploy phức tạp. Có nhiều bước: build, upload artifact, update infrastructure. Để nhanh, tôi spawn một agent và truyền mode: "bypassPermissions" vì không muốn agent hỏi từng bước nhỏ.

Agent được giao task: dọn dẹp các object cũ trong S3, sau đó update stack. Prompt khá cụ thể. Nhưng trong quá trình thực thi, agent tự suy luận rằng bucket cũ không còn cần thiết và chạy aws s3 rb để xóa luôn bucket đó.

Với bypassPermissions, không có gate nào check lại. Không có “Are you sure?”. Lệnh chạy thẳng.

Bucket đó là production bucket. Có data của khách hàng, có artifact từ nhiều tháng trước. Xóa S3 bucket là permanent: không Recycle Bin, không undo, không restore từ console. Nếu không có backup ngoài S3, mọi thứ trong đó mất vĩnh viễn.

May mắn là project đó có backup strategy riêng nên không mất data thực sự. Nhưng restore mất mấy tiếng, và tôi mất thêm một buổi để audit toàn bộ xem còn gì bị ảnh hưởng không.

Bài học từ incident đó:

1. Never bypassPermissions với agent. Dù task có vẻ rõ ràng, agent suy luận theo cách của nó. bypassPermissions nghĩa là agent có thể làm bất cứ thứ gì mà không có gate nào.

2. Deny list là layer độc lập với mode. Nếu hồi đó tôi đã có deny list cho aws s3 rb, bucket đó không bị xóa dù agent ở mode nào. Deny list không check mode, nó check pattern và block cứng.

3. Phân biệt “không muốn bị hỏi” với “không cần gate”. Không muốn bị hỏi từng bước nhỏ thì dùng auto với allowlist cụ thể cho những tool safe. Không cần gate là một assumption nguy hiểm khi làm việc với destructive command.

4. Background agent cần deny list cẩn thận hơn foreground agent. Khi agent chạy background, bạn không xem live output. Khi nó đến bước destructive, bạn không có cơ hội can thiệp kịp thời.

Từ hôm đó, settings.json của tôi luôn có deny list cho mọi S3 delete command. Không phải vì tôi không tin CC, mà vì tôi không tin phản xạ của bản thân khi đang vội.

Tóm tắt và bài tiếp theo

  • Bốn mode: ask (default, hỏi mọi thứ không trong allow), auto (chạy nếu match allowlist), acceptEdits (auto-accept file edit, còn Bash vẫn hỏi), bypassPermissions (bỏ qua tất cả).
  • Allowlist permissions.allow whitelists tool pattern để auto-run. Deny list permissions.deny hard-block tool pattern, deny thắng allow.
  • PreToolUse hook cho phép viết logic block/allow lập trình được, phức tạp hơn static pattern.
  • Khi spawn agent: foreground dùng ask, background với safe tool dùng auto, không bao giờ dùng bypassPermissions.
  • Deny list là layer chống “tay nhanh hơn não”. Cấu hình cho những action không undo được, đặc biệt cloud destructive command.

Bài 5 chuyển sang chủ đề khác: compaction và prompt cache. Khi session dài đến gần giới hạn context window, CC tự động nén conversation lại. Hiểu cơ chế đó giúp bạn tránh mất context quan trọng và giảm chi phí token đáng kể.


Bài thuộc series Claude Code từ zero. Series plan tại bài giới thiệu.