Lộ trình học›Claude Code›Claude Code in Action Mở bản đồ

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

Bài viếtTrung cấp30 phút đọc

Trong dev, .env chứa:

Sau bài này, bạn sẽ
  1. Cài đặt end-to-end một hook PreToolUse block Claude đọc file .env
  2. Viết Node.js script hook đầy đủ với error handling
  3. Config hook trong .claude/settings.local.json
  4. Test hook với 4 scenario khác nhau (Read direct, Grep, gián tiếp, false positive)
  5. Debug khi hook không trigger

Mở đầu: Tại sao .env là priority #1 để protect?

  • API key (Anthropic, Stripe, AWS)
  • DB password
  • JWT secret
  • Third-party credential

Nếu Claude đọc .env:

  • Token sensitive có thể lọt vào transcript → shared inadvertently
  • Claude có thể quote secret trong response → tiếp tục lan truyền
  • Compromise security khi transcript được lưu hoặc share

Quy tắc vàng: .env tuyệt đối không xuất hiện trong prompt/response của LLM. Hook là cách enforce tuyệt đối.

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:

Đọc stdin JSON
Lấy file_path (Read) hoặc path (Grep)
Match pattern /\.env(\.|$)/?
  Yes → exit 2 + stderr
  No  → exit 0

Bước 4: Exit code:

  • Block → 2
  • Allow → 0

Bước 1: Cài structure

Trong project root:

mkdir -p .claude/hooks

Directory layout sau khi xong

project/
├── .claude/
│   ├── hooks/
│   │   └── read_hook.js        ← script hook
│   └── settings.local.json     ← config
├── src/
├── package.json
└── ...

Bước 2: Viết script read_hook.js

File: .claude/hooks/read_hook.js

#!/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);
});

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

Bước 3: Config settings.local.json

File: .claude/settings.local.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read|Grep",
        "hooks": [
          {
            "type": "command",
            "command": "node /absolute/path/to/project/.claude/hooks/read_hook.js"
          }
        ]
      }
    ]
  }
}

Absolute path! Dùng đường dẫn tuyệt đối, không ./hooks/.... Lý do security (bị đề cập ở Bài 4.15).

Ví dụ:

  • 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

Tại sao settings.local.json?

  • 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)

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:

Đọc file .env của project này cho tôi xem.

Expected:

[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

Claude sẽ respond kiểu:

Tôi không thể đọc file .env — có một PreToolUse hook trong project block access đến env files. Đây là protection cho credentials. Nếu bạn cần verify env var runtime, dùng process.env.XXX thay vì đọc file.

✅ Pass.

Scenario 2: Grep trong .env

Prompt:

Search xem file .env có chứa "ANTHROPIC" không.

Expected: Grep tool bị block (hook match cả Read|Grep).

✅ Pass.

Scenario 3: Đọc file khác (không phải .env)

Prompt:

Đọc package.json của project này.

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 file environment.ts trong src/config/ (tôi biết file này public).

Expected: Hook cho phép (environment.ts không match /(^|\/)\.env(\.|$)/).

✅ Pass — regex đủ tight.

Scenario 5: Đọc .env.local, .env.production

Prompt:

Check xem .env.production có config NODE_ENV không.

Expected: Hook block — pattern match .env.production.

✅ Pass.

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:

  1. Đã restart Claude Code sau edit settings? → /exit && claude
  2. File settings.local.json đúng path? Check:
   cat .claude/settings.local.json
  1. Path absolute đúng? Test trực tiếp:
   echo '{"tool_input":{"file_path":".env"}}' | node /abs/path/read_hook.js
   echo "Exit: $?"

Expected: Exit: 2

  1. Matcher syntax đúng? (dùng Read|Grep, không phải ["Read", "Grep"])

Triệu chứng 2: Hook error, không block

Claude vẫn đọc .env, log có [read_hook] error.

Check:

  1. Script có lỗi syntax JS? Chạy: node read_hook.js với input manual
  2. Node version đủ mới? (cần ≥14 cho for await)
  3. Permission file hook? chmod 755 read_hook.js (nhiều khi không cần vì config gọi node)

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:

  1. Confirm đây là PreToolUse, không nhầm PostToolUse
  2. tool_name trong JSON match matcher? Log input ra file để verify
  3. Có hook khác override ở file settings khác?

Tactic: Log debug temporary

Thêm tạm vào hook:

require('fs').appendFileSync(
  '/tmp/hook-debug.log', 
  JSON.stringify(input) + '\n'
);

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.

Mở rộng: Block thêm folder secrets/

Bổ sung pattern:

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);
  }
}

Hook giờ block phong phú hơn. Cùng mục đích: không cho credential vào prompt.

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:

  • Không false positive quá nhiều?
  • Có case hook miss?
  • Dev workflow bị ảnh hưởng không?

Tuần 2: Rollout team

Viết doc internal:


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

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:

  • Pattern nào chặn đúng đắn
  • False positive nào
  • Bổ sung file/folder cần block

Kết quả

  • 100% dev có hook active
  • Zero incident .env content leak
  • Dev confidence dùng Claude Code tự nhiên, không lo

Case studies theo ngành

Bank — Block prod DB read

Setup: PreToolUse hook trên Bash → regex match SELECT/pg_dump targeting prod host.

  • Kết quả: Zero incident prod data access trong tool.

Health — Block PHI folder

Setup: PreToolUse trên Read/Grep → block /patients/, /ehr-exports/.

  • Kết quả: HIPAA audit pass, tool usage scaled from 5 → 30 engineer safely.

University — Block student data

Setup: Block /student-records/, /grades/.

  • Kết quả: Compliance FERPA, tool approved for instructor use.

Enterprise — Block IP protect

Setup: Block /proprietary-algorithms/, custom copyrighted codebases.

  • Kết quả: Legal approval để dev dùng Claude Code trên IP-sensitive repo.

Defense — Air-gap verify

Setup: Trong addition to hook, PreToolUse verify network egress patterns.

  • Kết quả: Deploy Claude Code + Bedrock trong classified env.

Anti-patterns

Dùng path tương đối

Settings:

"command": "node ./hooks/read_hook.js"
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

if (path.includes('env')) { exit 2 }
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

const input = JSON.parse(stdin);  // crash if invalid
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 PROJECT_ROOT = process.env.PROJECT_ROOT;  // undefined

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.

Mẹo nâng cao

Mẹo 1: Config bằng ENV var

Cho phép override pattern qua env:

const customPatterns = (process.env.CLAUDE_HOOK_BLOCK_PATTERNS || '')
  .split(',')
  .filter(Boolean)
  .map(p => new RegExp(p));

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

#!/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

Ngắn hơn Node, đủ cho simple case.

Mẹo 3: Multiple hook chain

Có thể có nhiều hook cho cùng matcher:

"PreToolUse": [
  { "matcher": "Read", "hooks": [{...}, {...}] }
]

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:

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:
  1. Read .env → Block? Có Không
  2. Grep .env → Block? Có Không
  3. Read package.json → Allow? Có Không
  4. Read environment.ts → Allow? Có Không
  5. Read .env.production → Block? Có Không

Nếu 5/5 → hook chuẩn.

Bài tập 2 (optional, 15 phút): Extend block list

Thêm pattern cho:

  • .key, .pem (SSH/cert)
  • Folder secrets/, credentials/
  • File .htpasswd
  • Any file matching id_rsa*

Test với prompt yêu cầu đọc mỗi loại. Verify block đúng.

Tóm tắt bài học

Những điều cốt lõi
  • 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

Bạn đã hoàn thành bài này chưa?Tiến độ lưu trên trình duyệt của bạn.