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 .env và local.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:
#!/bin/bashinput=$(cat)tool_name=$(echo "$input" | jq -r '.tool_name // ""')command=$(echo "$input" | jq -r '.tool_input.command // ""')
if [ "$tool_name" != "Bash" ]; then exit 0fi
# 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 2fi
exit 0Lớp 3: .claudeignore
Ngăn Claude Code đọc file nhạy cảm:
local.propertiesgoogle-services.json*.keystore**/security/impl/.env.env.*backend/config/secrets.jsonbackend/.envBa 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.mdClaude 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ế:
rm Claude.mdThấ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:
if echo "$command" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|...|-rf|-fr)\b'; thenRegex này cụ thể tìm rm theo sau bởi flag chứa cả r và f. 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:
# TRƯỚC: Chỉ bắt rm có flag -rfif echo "$command" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|...)\b'; then
# SAU: Bắt TẤT CẢ lệnh rmif echo "$command" | grep -qE '\brm\s+'; thenSau khi sửa, tôi test chính xác thao tác tương tự:
❯ rm Claude_CONTEXT.mdBị 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
# Hook regex — chỉ bắt recursive+forcegrep -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.
# Hook regex — bắt BẤT KỲ lệnh rm nàogrep -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/bashinput=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name // ""')command=$(echo "$input" | jq -r '.tool_input.command // ""')
if [ "$tool_name" != "Bash" ]; then exit 0fi
# Chặn TẤT CẢ hình thức xóa fileif 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 2fi
if echo "$command" | grep -qE '\b(rmdir|unlink|shred)\s+'; then echo '{"error": "BLOCKED: Xóa file không được phép."}' >&2 exit 2fi
# Chặn sudoif echo "$command" | grep -qE '^\s*sudo\s+'; then echo '{"error": "BLOCKED: sudo không được phép."}' >&2 exit 2fi
# Chặn git phá hủyif 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 2fi
# Chặn network exfiltrationif echo "$command" | grep -qE '\b(curl|wget|ssh|scp)\s+'; then echo '{"error": "BLOCKED: Lệnh network không được phép."}' >&2 exit 2fi
# Chặn thay đổi permission nguy hiểmif echo "$command" | grep -qE 'chmod\s+(777|666|\+s)'; then echo '{"error": "BLOCKED: Thay đổi permission nguy hiểm."}' >&2 exit 2fi
# Chặn kill processif echo "$command" | grep -qE '\b(kill|killall|pkill)\s+'; then echo '{"error": "BLOCKED: Kill process không được phép."}' >&2 exit 2fi
# Chặn đọc file secretif echo "$command" | grep -qE 'cat\s+.*(\.env|local\.properties|secrets|keystore|google-services)'; then echo '{"error": "BLOCKED: Truy cập file secret."}' >&2 exit 2fi
exit 0Lớ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:
# Trước mỗi phiên Claude Codegit add -A && git commit -m "checkpoint before Claude Code"
# Nếu có gì saigit checkout . # Khôi phục tất cả file đã sửagit 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.