Tôi nghĩ mình đã an toàn. Ba lớp phòng thủ. Deny list. Hook script với regex validation. File .claudeignore. Tất cả đã config, tất cả đang chạy.

Rồi tôi gõ rm Claude.md và nhìn nó biến mất.

Đây là câu chuyện về cách tôi nhận ra rằng số lượng lớp bảo mật không quan trọng nếu bất kỳ lớp nào cũng có lỗ hổng — và sự khác biệt chỉ một ký tự trong cấu hình đã thay đổi tất cả.


Bối cảnh: Tại sao tôi cần bảo mật

Tôi làm việc trên project Kotlin Multiplatform — shared business logic cho cả Android và iOS, cùng backend Node.js xử lý API và real-time services. Gần đây tôi bắt đầu dùng Claude Code để tăng tốc phát triển — refactor shared module, generate platform-specific implementation, scaffolding API endpoint. Năng suất tăng rõ rệt.

Nhưng vấn đề là: project của tôi có file nhạy cảm khắp nơi. API key trong .envlocal.properties. Firebase credentials trong google-services.json. JWT signing secret trong folder config/ của backend. Database connection string. Logic mã hóa độc quyền trong shared KMP module. Tôi không thể giao toàn bộ codebase cho công cụ AI mà không có biện pháp bảo vệ.

Nên tôi xây dựng thứ mà tôi nghĩ là hệ thống phòng thủ không thể xuyên thủng.


Hệ thống “không thể xuyên thủng”

Lớp 1: Deny List (settings.json)

Tôi cấu hình hệ thống permission của Claude Code để chặn các lệnh nguy hiểm:

{
"permissions": {
"deny": [
"Bash(rm -rf *)",
"Bash(rm -fr *)",
"Bash(sudo *)",
"Bash(git push --force *)",
"Bash(git reset --hard *)"
]
}
}

Lớp 2: Hook Script chặn trước khi thực thi

Script bash chạy trước mỗi command, kiểm tra pattern nguy hiểm bằng regex:

.claude/hooks/block-dangerous-commands.sh
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name // ""')
command=$(echo "$input" | jq -r '.tool_input.command // ""')
if [ "$tool_name" != "Bash" ]; then
exit 0
fi
# Kiểm tra rm -rf (với các biến thể flag)
if echo "$command" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|...|-rf|-fr)\b'; then
echo '{"error": "BLOCKED: rm -rf is not allowed."}' >&2
exit 2
fi
exit 0

Lớp 3: .claudeignore

Ngăn Claude Code đọc file nhạy cảm:

local.properties
google-services.json
*.keystore
**/security/impl/
.env
.env.*
backend/config/secrets.json
backend/.env

Ba lớp. Tôi cảm thấy bất khả xâm phạm.


Bài test phá vỡ tất cả

Trong một phiên refactoring bình thường, tôi yêu cầu Claude Code dọn dẹp file không dùng. Tại một thời điểm, nó quyết định một file markdown không cần thiết nữa:

❯ rm Claude.md

Claude Code hỏi quyền chạy rm /Users/ethannguyen/Data/WorkspaceAIAuto/UIProject/CLAUDE.md. Tôi bấm Yes.

File biến mất. Không có cảnh báo từ deny list. Không có block từ hook script. Không có bảo vệ từ .claudeignore. Cả ba lớp thất bại đồng thời.

Phản ứng của tôi: Khoan, gì cơ? Tôi có BA lớp bảo mật. Sao lại thế?


Tại sao cả ba lớp đều thất bại

Để tôi đi qua từng lớp và giải thích chính xác tại sao nó không bắt được.

Lớp 1 thất bại: Deny List quá cụ thể

Rule deny của tôi:

"Bash(rm -rf *)"

Command thực tế:

Terminal window
rm Claude.md

Thấy vấn đề chưa? Deny list chỉ match rm -rf — xóa đệ quy kèm force. Một lệnh rm đơn giản không có flag? Lọt qua ngon lành. Pattern Bash(rm -rf *) tìm chuỗi literal rm -rf theo sau là bất kỳ thứ gì. Command rm Claude.md không chứa -rf ở đâu cả.

Sai lầm: Tôi bảo vệ chống kịch bản thảm khốc (rm -rf /) mà để ngỏ cửa cho việc xóa file thường ngày.

Lớp 2 thất bại: Hook Regex cũng hẹp như vậy

Regex trong hook script:

Terminal window
if echo "$command" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|...|-rf|-fr)\b'; then

Regex này cụ thể tìm rm theo sau bởi flag chứa cả rf. Command rm Claude.md không có flag gì — chỉ rm theo sau là tên file. Regex không match.

Sai lầm: Cùng nguyên nhân gốc. Tôi thiết kế hook để bắt các biến thể rm -rf, không phải bản thân lệnh rm.

Lớp 3 không liên quan

.claudeignore ngăn Claude Code đọc file. Nó hoàn toàn không có tác dụng với việc xóa file. File CLAUDE.md thậm chí không nằm trong danh sách ignore — nhưng kể cả có thì .claudeignore cũng không ngăn được việc xóa.

Sai lầm: Hiểu nhầm .claudeignore thực sự bảo vệ cái gì. Nó là rào cản đọc, không phải rào cản ghi/xóa.

Yếu tố con người

Và rồi có tôi. Claude Code đã hỏi quyền. Nó hiển thị chính xác command: Bash(rm /Users/ethannguyen/.../CLAUDE.md). Tôi bấm Yes mà không xử lý hết thông tin mình đang đồng ý.

Trong phiên refactoring dài với hàng chục lần hỏi quyền, sự mệt mỏi duyệt (approval fatigue) xuất hiện. Bạn bắt đầu bấm Yes tự động. Đây chính xác là tình huống mà cấu hình bảo mật cần cứu bạn khỏi chính bạn.


Cách sửa: Một ký tự thay đổi tất cả

Cách sửa đơn giản đến ngượng. Trong deny list:

"Bash(rm -rf *)"
"Bash(rm *)"

Đó là tất cả. Đổi rm -rf thành chỉ rm. Giờ deny rule match bất kỳ command nào bắt đầu bằng rm, bất kể flag gì.

Hook script cập nhật theo cùng nguyên tắc:

Terminal window
# TRƯỚC: Chỉ bắt rm có flag -rf
if echo "$command" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|...)\b'; then
# SAU: Bắt TẤT CẢ lệnh rm
if echo "$command" | grep -qE '\brm\s+'; then

Sau khi sửa, tôi test chính xác thao tác tương tự:

❯ rm Claude_CONTEXT.md

Bị chặn. Dòng lỗi đỏ nói lên tất cả: “Error: Permission to use Bash with command rm has been denied.”

Claude Code sau đó tìm kiếm file, xác nhận nó không tồn tại trong project, và stop hook ngăn mọi nỗ lực tiếp theo. Hệ thống hoạt động đúng như thiết kế.


So sánh toàn diện: Trước vs. Sau

❌ TRƯỚC (Có lỗ hổng)

{
"permissions": {
"deny": [
"Bash(rm -rf *)",
"Bash(rm -fr *)",
"Bash(rm -r *)",
"Bash(sudo *)"
]
}
}

Chặn được: rm -rf anything, rm -r folder/, sudo commands

Bỏ lọt: rm file.txt, rm *.kt, rm -f file.txt, unlink file

Terminal window
# Hook regex — chỉ bắt recursive+force
grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|...|-rf|-fr)\b'

✅ SAU (An toàn)

{
"permissions": {
"deny": [
"Bash(rm *)",
"Bash(rmdir *)",
"Bash(unlink *)",
"Bash(shred *)",
"Bash(sudo *)",
"Bash(git push --force *)",
"Bash(git push -f *)",
"Bash(git reset --hard *)",
"Bash(git clean *)",
"Bash(curl *)",
"Bash(wget *)",
"Bash(kill *)"
]
}
}

Chặn được: TẤT CẢ lệnh xóa file, bất kể flag hay argument.

Terminal window
# Hook regex — bắt BẤT KỲ lệnh rm nào
grep -qE '\brm\s+'

Nguy hiểm thực tế: Phiên Refactoring

Đây là lý do vấn đề này quan trọng hơn test case của tôi. Trong quá trình refactoring, Claude Code có thể quyết định xóa file vì những lý do nghe rất hợp lý:

  • “File này không được sử dụng, xóa đi.” — Có thể không dùng trong module Claude thấy, nhưng được tham chiếu ở nơi khác.
  • “Xóa implementation cũ trước khi tạo mới.” — Nếu implementation mới lỗi thì sao? Cái cũ đã mất.
  • “Dọn dẹp file được generate.” — Claude nhận nhầm file viết tay thành file generated.
  • “File này có lỗi compile, xóa đi.” — Lỗi có thể do thiếu dependency, không phải file xấu.

Trong mọi trường hợp, Claude Code tin rằng nó đang giúp ích. Và trong phiên làm việc dài với approval fatigue, bạn có thể để nó xảy ra.

Cách tiếp cận đúng: Claude Code không bao giờ được phép xóa file. Nếu file cần xóa, bạn tự làm trong terminal. Đây là cánh cửa một chiều mà AI không nên có quyền mở.


Bộ cấu hình phòng thủ hoàn chỉnh

Đây là cấu hình cuối cùng, đã test thực tế:

.claude/settings.json

{
"permissions": {
"allow": [
"Edit",
"MultiEdit",
"Read",
"Bash(./gradlew *)",
"Bash(cd *)",
"Bash(ls *)",
"Bash(cat *)",
"Bash(mkdir *)",
"Bash(cp *)",
"Bash(mv *)",
"Bash(find *)",
"Bash(grep *)",
"Bash(git status)",
"Bash(git diff*)",
"Bash(git log*)",
"Bash(git add *)",
"Bash(git commit *)"
],
"deny": [
"Bash(rm *)",
"Bash(rmdir *)",
"Bash(unlink *)",
"Bash(shred *)",
"Bash(sudo *)",
"Bash(chmod 777 *)",
"Bash(chmod +s *)",
"Bash(mkfs *)",
"Bash(dd *)",
"Bash(git push --force *)",
"Bash(git push -f *)",
"Bash(git reset --hard *)",
"Bash(git clean *)",
"Bash(curl *)",
"Bash(wget *)",
"Bash(ssh *)",
"Bash(scp *)",
"Bash(kill *)",
"Bash(pkill *)",
"Bash(killall *)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/block-dangerous-commands.sh"
}
]
}
]
}
}

.claude/hooks/block-dangerous-commands.sh

#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name // ""')
command=$(echo "$input" | jq -r '.tool_input.command // ""')
if [ "$tool_name" != "Bash" ]; then
exit 0
fi
# Chặn TẤT CẢ hình thức xóa file
if echo "$command" | grep -qE '\brm\s+'; then
echo '{"error": "BLOCKED: rm không được phép. Xóa file thủ công trong terminal."}' >&2
exit 2
fi
if echo "$command" | grep -qE '\b(rmdir|unlink|shred)\s+'; then
echo '{"error": "BLOCKED: Xóa file không được phép."}' >&2
exit 2
fi
# Chặn sudo
if echo "$command" | grep -qE '^\s*sudo\s+'; then
echo '{"error": "BLOCKED: sudo không được phép."}' >&2
exit 2
fi
# Chặn git phá hủy
if echo "$command" | grep -qE 'git\s+(push\s+.*(-f|--force)|reset\s+--hard|clean\s+-[a-z]*f)'; then
echo '{"error": "BLOCKED: Thao tác git phá hủy."}' >&2
exit 2
fi
# Chặn network exfiltration
if echo "$command" | grep -qE '\b(curl|wget|ssh|scp)\s+'; then
echo '{"error": "BLOCKED: Lệnh network không được phép."}' >&2
exit 2
fi
# Chặn thay đổi permission nguy hiểm
if echo "$command" | grep -qE 'chmod\s+(777|666|\+s)'; then
echo '{"error": "BLOCKED: Thay đổi permission nguy hiểm."}' >&2
exit 2
fi
# Chặn kill process
if echo "$command" | grep -qE '\b(kill|killall|pkill)\s+'; then
echo '{"error": "BLOCKED: Kill process không được phép."}' >&2
exit 2
fi
# Chặn đọc file secret
if echo "$command" | grep -qE 'cat\s+.*(\.env|local\.properties|secrets|keystore|google-services)'; then
echo '{"error": "BLOCKED: Truy cập file secret."}' >&2
exit 2
fi
exit 0

Lớp thứ tư: Git

Không cấu hình nào hoàn hảo. Luôn có git làm lưới an toàn cuối cùng:

Terminal window
# Trước mỗi phiên Claude Code
git add -A && git commit -m "checkpoint before Claude Code"
# Nếu có gì sai
git checkout . # Khôi phục tất cả file đã sửa
git checkout -- file # Khôi phục file cụ thể

Bài học rút ra

1. Bảo mật chỉ mạnh bằng cấu hình yếu nhất. Ba lớp có cùng lỗ hổng bằng không có bảo vệ cho attack vector đó.

2. Test threat model thực tế, không chỉ worst case. Tôi test rm -rf / (thảm khốc) nhưng không test rm file.txt (phổ biến). Case phổ biến mới là thứ thực sự gây hại.

3. Ưu tiên deny rule rộng hơn cụ thể. Bash(rm *) luôn an toàn hơn Bash(rm -rf *). Bạn luôn có thể whitelist pattern an toàn cụ thể nếu cần.

4. Approval fatigue là thật. Trong phiên dài, bạn sẽ bấm Yes mà không đọc. Cấu hình của bạn cần bảo vệ bạn khỏi điều này.

5. AI tool sẽ xóa file với thiện chí. Trong quá trình refactoring, Claude Code thực sự tin nó đang giúp ích khi xóa file “không dùng”. Vấn đề là nó không phải lúc nào cũng có đầy đủ context về thứ gì thực sự không dùng.

6. Git là lưới an toàn thực sự. Cấu hình ngăn tai nạn. Git phục hồi từ tai nạn. Luôn commit trước khi để AI tool chỉnh sửa codebase.


Chuyện này xảy ra trên project production thật. File tôi mất có thể khôi phục được (nó là file markdown, và tôi có nội dung trong lịch sử conversation). Nhưng nó có thể là file Kotlin shared module quan trọng, Node.js route handler, hay cấu hình Gradle. Bài học tốn của tôi vài phút phục hồi. Của bạn có thể tốn nhiều hơn.

Nếu bạn đang dùng Claude Code trên project production, dành 10 phút để cấu hình deny list và hook đúng cách. Và test chúng — không chỉ với rm -rf, mà cả rm yourfile.txt.


Muốn tìm hiểu sâu hơn? Khóa học Claude Code Mastery bao gồm tất cả và hơn thế — 16 phase, 64 module, từ nền tảng đến multi-agent workflow tự động. Phase 1-3 miễn phí.

Nhận Claude Code Cheat Sheet miễn phí — 50+ command trong một file PDF — khi đăng ký newsletter.