Implement hook đầu tiên — Block .env access

5 — HooksTrung cấp30 phút

Trong dev, .env chứa:

Bạn sẽ học được
  • 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 0

Bướ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/hooks

Bướ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
claude

Bướ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 block

Scenario 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.json

Triệ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 .env

Case 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;  // undefined

Mẹ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 0

Mẹ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.

Tài liệu tham khảo
  • Claude Code hooks examples
  • MITRE ATT&CK T1574.007 — Path interception
  • OWASP Binary Planting
  • Bài 4.15 — Gotchas + absolute path trick
Nội dung này có hữu ích không?