Khám phá Learn Stream About Jokes
COHORT 2 Foundation — Claude Code — 3 buổi Zoom · 21 · 24 · 28/07, 20:00 Đăng ký →
Bài viết

Claude Code: chặn secret rò rỉ ở tầng hook, trước khi prompt rời máy

Một Claude Code hook (UserPromptSubmit) có thể chặn API key bị paste nhầm trước khi nó lên provider. Nhưng cách 'auto-fix' phổ biến nhất — tự ghi secret vào .env — lại là sai lầm. Vậy khi lỡ paste một key ra ngoài, việc thật sự cần làm là gì?

Công cụ & Stack

Claude Code: chặn secret rò rỉ ở tầng hook, trước khi prompt rời máy

Một hook local có thể chặn secret trước khi prompt rời khỏi máy, nhưng không có hook nào biến một key đã bị paste thành “chưa từng lộ”; remediation thật sự vẫn là rotate.

Trong bài Hook tiết kiệm token khi đọc website, mình dùng hook để đưa một quy tắc ổn định xuống tầng harness: khi Claude Code đọc website, nội dung được làm sạch trước khi đi vào context window. Lần đó mục tiêu là tiết kiệm token. Lần này mục tiêu nghiêm trọng hơn một chút: ngăn API key, private key hoặc JWT bị gửi lên provider chỉ vì mình vô tình paste nhầm vào prompt.

Tinh thần của hai việc thật ra giống nhau. Mình có thể tự nhắc “đừng bao giờ paste secret”, hoặc thêm một câu tương tự vào CLAUDE.md, nhưng trí nhớ của mình và prompt đều không phải security boundary. Nếu một quy tắc có thể kiểm tra nhanh, deterministic và áp dụng cho mọi prompt, nó hợp với hook hơn.


Claude Code chặn secret trước khi byte đầu tiên rời máy

Claude Code có event UserPromptSubmit, chạy sau khi mình nhấn gửi nhưng trước khi prompt được chuyển lên API. Hook nhận một JSON payload qua stdin, đọc trường .prompt, kiểm tra các pattern rồi trả về exit code 2 nếu muốn chặn.

Vị trí này quan trọng vì một khi secret đã đi qua network, câu chuyện không còn là “model có dùng nó hay không”. Giá trị đó có thể đã xuất hiện trong request, provider logs và context window của session. Nhờ model nhận diện rồi xóa ở lượt sau không giải quyết được phần đã xảy ra, vì secret không thể được thu hồi khỏi quá khứ của request.

Mình cũng không muốn giao bước nhận diện đầu tiên cho model. Model có thể hiểu nhiều loại credential hơn regex, nhưng nó chậm hơn, tốn token và không bảo đảm phản hồi giống nhau ở mọi lần chạy. Một nhóm regex local thường hoàn thành trong vài millisecond, nên mức latency dưới khoảng 15ms là hợp lý cho loại hook này.

Ví dụ, phần lõi có thể ngắn như sau:

prompt="$(jq -r '.prompt // ""')"

if [[ "$prompt" =~ sk-proj-[a-zA-Z0-9_-]{48,} ]]; then
  printf 'Đã chặn OpenAI project key: %s\n' 'sk-pro…7xQ2' >&2
  printf 'Hãy coi key này đã lộ và rotate ngay.\n' >&2
  exit 2
fi

Điểm mình cần từ đoạn này không phải khả năng hiểu ngữ cảnh. Mình cần một cái chốt cơ học: thấy pattern thì dừng, không tranh luận và không gửi prompt đi tiếp.


Ghi secret vào .env không phải là remediation

Có một phiên bản của ý tưởng này tự động lấy secret vừa bắt được rồi ghi nguyên giá trị vào .env. Nghe qua khá tiện: prompt bị chặn, còn key được chuyển về “đúng chỗ”. Nhưng nói thật là mình không xem đó là sửa lỗi.

Việc ghi key thật xuống .env chỉ tạo thêm một bản plaintext trên đĩa. File này có thể bị commit nhầm, được backup, đồng bộ qua cloud hoặc bị một process khác đọc. .gitignore làm giảm một rủi ro cụ thể, nhưng không biến .env thành secret manager và cũng không thay đổi trạng thái của key cũ.

Quan trọng hơn, khi một secret đã xuất hiện ngoài chỗ mình dự định lưu nó, mình không còn cơ sở chắc chắn rằng chỉ mình nhìn thấy. Có thể hook đã chặn kịp trước network, nhưng terminal history, clipboard manager, screen recording hoặc một lớp logging local vẫn là những khả năng mình chưa chắc đã kiểm soát hết. Vì vậy, mình chọn quy tắc bảo thủ hơn: đã paste nhầm thì coi như key đã burned.

Remediation (khắc phục thật sự, chứ không chỉ giấu vấn đề đi) ở đây là rotate key: vào dashboard của provider, tạo một key mới và vô hiệu hóa key cũ, để chuỗi đã lộ vĩnh viễn không còn dùng được nữa. Rotate là chữ mình dùng cho đúng thao tác “xoay vòng” đó — thay chìa khóa mới, hủy chìa khóa cũ, giống như đổi khóa cửa sau khi làm mất chìa. Hook không thể thay mình làm cho key cũ trở lại an toàn; nó chỉ có thể chặn đúng thời điểm và nhắc mình thực hiện bước quan trọng nhất này.


Hook nên báo đủ để hành động, nhưng không lặp lại secret

Khi bắt được một pattern, hook của mình làm bốn việc. Nó chặn prompt bằng exit code 2, xác định loại credential, hiển thị một bản redact chỉ còn vài ký tự đầu và cuối, rồi in thẳng hướng dẫn rotate ra stderr. Secret đầy đủ không được in lại, vì terminal output và log của hook cũng là những nơi không nên nhân bản credential.

Nếu muốn hỗ trợ bước cấu hình tiếp theo, hook có thể tạo một placeholder:

MY_API_KEY=<paste-rotated-key-here>

Đây là khác biệt nhỏ về code nhưng lớn về ý nghĩa. Placeholder dựng sẵn chỗ để mình đặt key mới sau khi rotate; nó không lưu lại key cũ đã burned. Mình vẫn phải bảo đảm .env nằm trong .gitignore, giới hạn quyền đọc phù hợp và cân nhắc dùng secret manager nếu môi trường có yêu cầu cao hơn.

Sau khi rotate, mình resubmit prompt theo cách yêu cầu code đọc credential từ environment, chẳng hạn process.env.MY_API_KEY, thay vì chép giá trị thật vào câu lệnh. Prompt chỉ cần nói secret nằm ở biến nào. Model không cần biết giá trị của nó để viết phần lớn integration code.


Regex có giới hạn, nên bypass cũng phải có chủ đích

Các pattern có prefix cố định khá dễ bắt. Anthropic key bắt đầu bằng sk-ant-api, OpenAI project key có sk-proj-, AWS access key ID có AKIA, còn private key có header rõ ràng. JWT cũng thường nhận diện được bằng ba đoạn base64url ngăn bởi dấu chấm:

sk-ant-api[0-9]{2}-[a-zA-Z0-9_-]{80,}
sk-proj-[a-zA-Z0-9_-]{48,}
AKIA[0-9A-Z]{16}
-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----
eyJhbGciOi[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+

Nhưng regex này không bắt được mọi secret. Password database, generic bearer token hoặc chuỗi entropy cao không có prefix cố định cần một lớp scan khác như gitleaks hoặc trufflehog. Ngược lại, tài liệu kỹ thuật đôi khi chứa token ví dụ đủ giống đồ thật, nên false positive là điều phải chấp nhận.

Mình vẫn muốn có bypass, nhưng bypass cần là một hành động có chủ đích, chẳng hạn tạm tắt hook cho đúng một prompt sau khi đã kiểm tra token là giả. Nếu hook âm thầm đoán quá nhiều và mình quen tay bỏ qua cảnh báo, lớp bảo vệ này sẽ mất giá trị. Một security control không cần bắt được mọi thứ mới đáng tin; nó cần rõ ràng về phần mình bắt được, phần mình bỏ sót, và cách vượt qua khi thật sự cần.


Điều hook sửa được là phản xạ của mình

Mình không gọi đây là “zero-leak auto-remediation”, vì hai chữ đó dễ tạo cảm giác rằng hook đã giải quyết xong sự cố. Nó chỉ bảo đảm rằng với request đang bị chặn, secret khớp regex chưa được gửi lên provider. Nó không chứng minh secret chưa nằm ở clipboard history, terminal log hay một nơi local khác, và chắc chắn không rotate key thay mình.

Thứ hook làm tốt là biến một nguyên tắc bảo mật thành phản xạ của hệ thống. Với Claude Code, quy tắc ổn định nằm trong harness, còn prompt được giữ cho mục tiêu và ngữ cảnh đang thay đổi. Lần trước mình áp dụng cách nghĩ này để giảm rác HTML; lần này mình dùng nó để tạo một điểm dừng trước network và một lời nhắc không thể bỏ qua: key đã paste nhầm thì rotate.

Trong workflow của bạn, loại secret nào thường có nguy cơ bị paste vào prompt nhất, và bạn đang lưu nó ở đâu sau khi rotate?


Phụ lục: cheatsheet chặn secret ở UserPromptSubmit hook

Bạn có thể paste nguyên phần cheatsheet này cho AI agent của mình để agent dựng hook, kiểm tra đường dẫn và đăng ký nó trong Claude Code.

Loại key Regex
Anthropic API key sk-ant-api[0-9]{2}-[a-zA-Z0-9_-]{80,}
OpenAI project key sk-proj-[a-zA-Z0-9_-]{48,}
AWS access key ID AKIA[0-9A-Z]{16}
Private key header -----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----
JWT eyJhbGciOi[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+

Lưu script tại ~/.claude/hooks/block-secrets.sh:

#!/usr/bin/env bash

set -u

payload="$(cat)"

if ! prompt="$(printf '%s' "$payload" | jq -r '.prompt // ""')"; then
  printf 'Không đọc được payload của UserPromptSubmit hook.\n' >&2
  exit 0
fi

types=(
  "Anthropic API key"
  "OpenAI project key"
  "AWS access key ID"
  "private key"
  "JWT"
)

patterns=(
  'sk-ant-api[0-9]{2}-[a-zA-Z0-9_-]{80,}'
  'sk-proj-[a-zA-Z0-9_-]{48,}'
  'AKIA[0-9A-Z]{16}'
  '-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----'
  'eyJhbGciOi[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+'
)

env_names=(
  "ANTHROPIC_API_KEY"
  "OPENAI_API_KEY"
  "AWS_ACCESS_KEY_ID"
  "PRIVATE_KEY"
  "JWT_TOKEN"
)

for i in "${!patterns[@]}"; do
  if [[ "$prompt" =~ ${patterns[$i]} ]]; then
    secret="${BASH_REMATCH[0]}"
    kind="${types[$i]}"
    env_key="${env_names[$i]}"

    if (( ${#secret} > 12 )); then
      redacted="${secret:0:6}...${secret: -4}"
    else
      redacted="<redacted>"
    fi

    printf 'Đã chặn %s trong prompt: %s\n' "$kind" "$redacted" >&2
    printf 'Hãy coi secret này đã lộ: rotate ngay tại dashboard của provider và vô hiệu key cũ.\n' >&2
    printf 'Sau đó dùng biến môi trường %s thay vì paste secret vào prompt.\n' "$env_key" >&2

    if [[ "${CREATE_ENV_PLACEHOLDER:-0}" == "1" ]]; then
      if [[ ! -f .env ]] || ! grep -q "^${env_key}=" .env; then
        printf '%s=<paste-rotated-key-here>\n' "$env_key" >> .env
        printf 'Đã thêm placeholder %s vào .env; secret cũ không được lưu.\n' "$env_key" >&2
      fi
    fi

    exit 2
  fi
done

exit 0

Đăng ký hook trong ~/.claude/settings.json:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "CREATE_ENV_PLACEHOLDER=1 bash ~/.claude/hooks/block-secrets.sh"
          }
        ]
      }
    ]
  }
}

Lưu ý

  • jq phải có trong PATH để script đọc trường .prompt từ JSON payload.
  • Muốn tắt việc tạo placeholder, bỏ CREATE_ENV_PLACEHOLDER=1 khỏi command.
  • Regex trên chỉ bắt các secret có prefix hoặc cấu trúc cố định.
  • Với chuỗi entropy cao không có prefix, cân nhắc thêm gitleaks hoặc trufflehog.
  • Token ví dụ trong tài liệu có thể gây false positive, nên cần một cơ chế bypass có chủ đích.
  • .env phải nằm trong .gitignore; hook chỉ ghi placeholder, không ghi secret vừa bị bắt.
  • Rotate và vô hiệu key cũ mới là remediation thật sự.

Bài liên quan

#claude-code #claude-code-hook #hook #security #secret #api-key #userpromptsubmit #rotate-key #ai-workflows

Bài viết liên quan

Khi video trở thành perception layer cho auto-edit

idea

Giá trị thật của video-analyzer không phải biến video thành thứ để search, mà thành signal có timestamp để một pipeline khác tự ra quyết định cắt. video-analyzer lo perception, Claude Code lo judgment, ffmpeg lo execution.

Nâng cấp Astro 6: cái bẫy 10MB và bài học deploy cho chắc

idea

Bản nâng cấp tưởng nửa ngày hoá ra đụng một cái blocker thật: worker phình lên 53MB, vượt giới hạn 10MB của Cloudflare. Đây là cách mình gỡ, và vì sao 'build pass' không bao giờ là 'xong'.

0:00

Chia sẻ ảnh

Bắt đầu gõ để tìm kiếm...