Trong dev, .env chứa:
- Cài đặt end-to-end một hook PreToolUse block Claude đọc file .env
- Viết Node.js script hook đầy đủ với error handling
- Config hook trong .claude/settings.local.json
- Test hook với 4 scenario khác nhau (Read direct, Grep, gián tiếp, false positive)
- Debug khi hook không trigger
Thiết kế (nhắc lại từ Bài 4.13)
Bước 1: Pre? → Yes, cần block trước khi tool đọc.
Bước 2: Matcher? → Read|Grep (2 tool có thể truy cập file content).
Bước 3: Logic:
Bước 4: Exit code:
- Block → 2
- Allow → 0
Đọc stdin JSON
Lấy file_path (Read) hoặc path (Grep)
Match pattern /\.env(\.|$)/?
Yes → exit 2 + stderr
No → exit 0Bước 1: Cài structure
Trong project root:
Directory layout sau khi xong
project/ ├── .claude/ │ ├── hooks/ │ │ └── read_hook.js ← script hook │ └── settings.local.json ← config ├── src/ ├── package.json └── ...
mkdir -p .claude/hooksBước 2: Viết script read_hook.js
File: .claude/hooks/read_hook.js
Phân tích script
- Shebang #!/usr/bin/env node — cho phép chạy trực tiếp nếu bạn chmod +x, nhưng config dưới sẽ gọi node ...
- Stdin reading — loop for await ... chunks, tập trung buffer
- Parse defensive — try/catch, fall back exit 1 nếu JSON invalid
- Path extraction — optional chaining cho cả file_path (Read/Edit) và path (Grep)
- Regex tight — match .env theo đầu dòng hoặc sau /, optional suffix
- Exit codes — 2 block, 0 allow, 1 error
#!/usr/bin/env node
/**
* PreToolUse hook: Block Claude from reading .env files.
* Matchers this runs on: Read, Grep
*/
async function main() {
// 1. Đọc toàn bộ stdin (JSON do Claude Code push)
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
let input;
try {
input = JSON.parse(Buffer.concat(chunks).toString());
} catch (err) {
console.error("[read_hook] Invalid JSON from stdin:", err.message);
process.exit(1); // hook error, không block
}
// 2. Extract file path (defensive)
const path =
input.tool_input?.file_path ||
input.tool_input?.path ||
'';
// 3. Match .env pattern
// Match: .env, .env.local, .env.production, .env.test, etc.
// Không match: environment.ts, env-config.ts
const ENV_PATTERN = /(^|\/)\.env(\.[a-zA-Z0-9_-]+)?$/;
if (ENV_PATTERN.test(path)) {
console.error(
`[read_hook] Blocked access to env file: ${path}\n` +
`Use process.env at runtime instead.`
);
process.exit(2); // BLOCK
}
// 4. Allow
process.exit(0);
}
main().catch(err => {
console.error("[read_hook] Unexpected error:", err);
process.exit(1);
});Bước 3: Config settings.local.json
File: .claude/settings.local.json
Ví dụ:
Tại sao settings.local.json?
- macOS: /Users/john/Projects/uigen/.claude/hooks/read_hook.js
- Linux: /home/john/projects/uigen/.claude/hooks/read_hook.js
- Windows WSL: /mnt/c/Users/john/projects/uigen/.claude/hooks/read_hook.js
- settings.local.json không commit (.gitignoreed mặc định)
- Path tuyệt đối của bạn khác path teammate → commit sẽ gây conflict
- Bài 4.15 dạy cách share hook với team (dùng $PWD + init script)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Grep",
"hooks": [
{
"type": "command",
"command": "node /absolute/path/to/project/.claude/hooks/read_hook.js"
}
]
}
]
}
}Bước 4: Restart Claude Code
Claude Code đọc settings chỉ lúc start. Phải restart:
/exit
claudeBước 5: Test với 4 scenario
Scenario 1: Đọc .env trực tiếp
Prompt:
Expected:
Đọc file .env của project này cho tôi xem.Scenario 1: Đọc .env trực tiếp
Claude sẽ respond kiểu:
✅ Pass.
Scenario 2: Grep trong .env
Prompt:
[Claude attempts] Read tool với file_path=".env"
[Hook triggers] exit 2, stderr: "Blocked access to env file: .env"
[Claude receives error] Claude giải thích không thể đọc do hook blockScenario 2: Grep trong .env
Expected: Grep tool bị block (hook match cả Read|Grep).
✅ Pass.
Scenario 3: Đọc file khác (không phải .env)
Prompt:
Search xem file .env có chứa "ANTHROPIC" không.Scenario 3: Đọc file khác (không phải .env)
Expected: Hook cho phép (package.json không match pattern).
Claude đọc bình thường, trả về nội dung.
✅ Pass.
Scenario 4: File có tên gần giống (false positive test)
Prompt:
Đọc package.json của project này.Scenario 4: File có tên gần giống (false positive test)
Expected: Hook cho phép (environment.ts không match /(^|\/)\.env(\.|$)/).
✅ Pass — regex đủ tight.
Scenario 5: Đọc .env.local, .env.production
Prompt:
Đọc file environment.ts trong src/config/ (tôi biết file này public).Scenario 5: Đọc .env.local, .env.production
Expected: Hook block — pattern match .env.production.
✅ Pass.
Check xem .env.production có config NODE_ENV không.Debug khi hook không hoạt động
Triệu chứng 1: Hook không trigger
Claude đọc .env bình thường, không có error.
Check:
- Đã restart Claude Code sau edit settings? → /exit && claude
- File settings.local.json đúng path? Check:
- Path absolute đúng? Test trực tiếp:
cat .claude/settings.local.jsonTriệu chứng 1: Hook không trigger
Expected: Exit: 2
Triệu chứng 2: Hook error, không block
Claude vẫn đọc .env, log có [read_hook] error.
Check:
Triệu chứng 3: Hook chạy nhưng không block
Exit code đúng là 2 nhưng Claude vẫn đọc?
Check:
Tactic: Log debug temporary
Thêm tạm vào hook:
- Matcher syntax đúng? (dùng Read|Grep, không phải ["Read", "Grep"])
- Script có lỗi syntax JS? Chạy: node read_hook.js với input manual
- Node version đủ mới? (cần ≥14 cho for await)
- Permission file hook? chmod 755 read_hook.js (nhiều khi không cần vì config gọi node)
- Confirm đây là PreToolUse, không nhầm PostToolUse
- tool_name trong JSON match matcher? Log input ra file để verify
- Có hook khác override ở file settings khác?
echo '{"tool_input":{"file_path":".env"}}' | node /abs/path/read_hook.js
echo "Exit: $?"Tactic: Log debug temporary
Trigger prompt → xem /tmp/hook-debug.log. Biết chính xác hook nhận gì.
Xóa dòng này sau khi debug xong.
require('fs').appendFileSync(
'/tmp/hook-debug.log',
JSON.stringify(input) + '\n'
);Mở rộng: Block thêm folder secrets/
Bổ sung pattern:
Hook giờ block phong phú hơn. Cùng mục đích: không cho credential vào prompt.
const BLOCKED_PATTERNS = [
/(^|\/)\.env(\.[a-zA-Z0-9_-]+)?$/, // .env files
/\/secrets\//, // secrets/ folder
/\/credentials\.json$/, // credentials.json
/\.key$/, // *.key (SSH keys, etc.)
/\.pem$/, // *.pem cert
];
for (const pat of BLOCKED_PATTERNS) {
if (pat.test(path)) {
console.error(`[read_hook] Blocked access to sensitive file: ${path}`);
process.exit(2);
}
}Ví dụ thực chiến: Team rollout
Tuần 1: Pilot 1 dev
Dev A setup hook trên máy mình. Dùng 1 tuần, verify:
Tuần 2: Rollout team
Viết doc internal:
init-claude.sh tự generate settings.local.json với path đúng cho máy từng dev (xem Bài 4.15).
Tuần 3: Monitor + iterate
Mỗi dev ghi log file hooks trigger — review cuối tuần:
Kết quả
- Không false positive quá nhiều?
- Có case hook miss?
- Dev workflow bị ảnh hưởng không?
- Pattern nào chặn đúng đắn
- False positive nào
- Bổ sung file/folder cần block
- 100% dev có hook active
- Zero incident .env content leak
- Dev confidence dùng Claude Code tự nhiên, không lo
# Setup security hook cho Claude Code
1. Copy hooks/ từ repo template
2. Run ./scripts/init-claude.sh
3. Restart Claude Code
4. Verify: bạn không đọc được .envCase studies theo ngành
💰 Bank — Block prod DB read
Setup: PreToolUse hook trên Bash → regex match SELECT/pg_dump targeting prod host.
🏥 Health — Block PHI folder
Setup: PreToolUse trên Read/Grep → block /patients/, /ehr-exports/.
🎓 University — Block student data
Setup: Block /student-records/, /grades/.
🏭 Enterprise — Block IP protect
Setup: Block /proprietary-algorithms/, custom copyrighted codebases.
🔐 Defense — Air-gap verify
Setup: Trong addition to hook, PreToolUse verify network egress patterns.
- Kết quả: Zero incident prod data access trong tool.
- Kết quả: HIPAA audit pass, tool usage scaled from 5 → 30 engineer safely.
- Kết quả: Compliance FERPA, tool approved for instructor use.
- Kết quả: Legal approval để dev dùng Claude Code trên IP-sensitive repo.
- Kết quả: Deploy Claude Code + Bedrock trong classified env.
Anti-patterns
❌ Dùng path tương đối
Settings:
Hậu quả: Hook chỉ chạy nếu Claude Code start từ project root. Chạy từ sub-dir → path fail.
Cách đúng: Absolute path, hoặc $PWD/... với setup script (Bài 4.15).
❌ Pattern block quá rộng
"command": "node ./hooks/read_hook.js"❌ Pattern block quá rộng
Hậu quả: Block luôn environment.ts, event-handler.ts (có "env").
Cách đúng: Regex tight ^\.env hoặc (^|\/)\.env(\.|$)/.
❌ Không handle exception
if (path.includes('env')) { exit 2 }❌ Không handle exception
Hậu quả: Hook crash, Claude continue. Security gap.
Cách đúng: Try/catch, exit code phù hợp.
❌ Hook depend vào env variable không set trong session
const input = JSON.parse(stdin); // crash if invalid❌ Hook depend vào env variable không set trong session
Cách đúng: Hard code hoặc resolve từ file path.
❌ Dùng settings.json (shared) cho absolute path cá nhân
Hậu quả: Commit vào Git → teammate pull, path sai.
Cách đúng: Absolute path trong settings.local.json (không commit). Hoặc dùng $PWD trick Bài 4.15.
const PROJECT_ROOT = process.env.PROJECT_ROOT; // undefinedMẹo nâng cao
Mẹo 1: Config bằng ENV var
Cho phép override pattern qua env:
Dev có use case riêng → set env var, không phải edit script.
Mẹo 2: Hook viết bằng shell cho đơn giản
const customPatterns = (process.env.CLAUDE_HOOK_BLOCK_PATTERNS || '')
.split(',')
.filter(Boolean)
.map(p => new RegExp(p));Mẹo 2: Hook viết bằng shell cho đơn giản
Ngắn hơn Node, đủ cho simple case.
Mẹo 3: Multiple hook chain
Có thể có nhiều hook cho cùng matcher:
#!/bin/bash
read -r -d '' input
path=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // ""')
if echo "$path" | grep -qE '(^|/)\.env(\.|$)'; then
echo "Blocked: $path" >&2
exit 2
fi
exit 0Mẹo 3: Multiple hook chain
Chạy tuần tự. Nếu hook đầu exit 2 → stop, không chạy hook tiếp.
Mẹo 4: Hook conditional bằng file glob
Hook chỉ active trong một số folder:
"PreToolUse": [
{ "matcher": "Read", "hooks": [{...}, {...}] }
]Mẹo 4: Hook conditional bằng file glob
if (!path.startsWith(process.cwd() + '/src/')) {
process.exit(0); // not in src, allow
}
// ... strict check cho srcÁp dụng ngay
Bài tập 1: Cài hook .env (20 phút)
Bước 1: Tạo script đúng như trên.
Bước 2: Config settings.local.json với absolute path máy bạn.
Bước 3: Restart Claude Code.
Bước 4: Test 5 scenario:
Nếu 5/5 → hook chuẩn.
Bài tập 2 (optional, 15 phút): Extend block list
Thêm pattern cho:
Test với prompt yêu cầu đọc mỗi loại. Verify block đúng.
- Read .env → Block? ☐ Có ☐ Không
- Grep .env → Block? ☐ Có ☐ Không
- Read package.json → Allow? ☐ Có ☐ Không
- Read environment.ts → Allow? ☐ Có ☐ Không
- Read .env.production → Block? ☐ Có ☐ Không
- .key, .pem (SSH/cert)
- Folder secrets/, credentials/
- File .htpasswd
- Any file matching id_rsa*
Tóm tắt bài học
🎯 Implement hook = script shell + config JSON — không phức tạp sau khi setup lần đầu.
🎯 Absolute path là yêu cầu bảo mật — đừng dùng relative.
🎯 Test nhiều scenario — true positive, true negative, false positive.
🎯 Debug bằng log JSON vào /tmp — khi hook không làm như mong đợi.
🎯 Exit 2 = block + stderr message — Claude nhận message, giải thích cho user.
- Claude Code hooks examples
- MITRE ATT&CK T1574.007 — Path interception
- OWASP Binary Planting
- Bài 4.15 — Gotchas + absolute path trick