SSD Nodes Learn 🎉 VPS từ $5.50/tháng
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-15

Claude Code Hooks: Event, stdin và exit code 2

Tìm hiểu hook của Claude Code nằm ở đâu, event nào kích hoạt, vì sao exit code 2 hủy tool call và rủi ro bảo mật khi chạy shell command.

Hook của Claude Code là gì

Hook của Claude Code là các shell command mà Claude Code tự chạy tại những thời điểm cố định trong vòng đời của chính nó. Đây là điểm khác biệt hoàn toàn giữa hook và rules file. Chỉ thị trong CLAUDE.md là lời khuyên, và model sẽ cân nhắc nó cùng với mọi nội dung khác trong context. Hook là code, và nó chạy dù model có đồng ý hay không. Nếu agent liên tục bỏ qua formatter dù bạn đã nhắc hai lần, bạn không cần viết chỉ thị cứng rắn hơn. Bạn cần một hook.

Cơ chế này khá đơn giản. Bạn đăng ký một command trong settings file dưới tên của một event. Khi event đó xảy ra, Claude Code chạy command của bạn và ghi dữ liệu của event vào standard input (stdin) dưới dạng JSON (JavaScript object notation). Command của bạn đọc dữ liệu đó, thực hiện công việc, rồi trả về exit status. Exit 2 từ hook PreToolUse sẽ hủy tool call trước khi nó chạy. Mọi nội dung script ghi vào standard error (stderr) sẽ được gửi lại cho model làm lý do.

Tên event và tên field trong phần này lấy từ tài liệu tham khảo về hooks của Claude Code, được kiểm tra vào tháng 8 năm 2026 với release 2.1.232. Giao diện này thay đổi nhanh, vì vậy hãy kiểm tra tài liệu tham khảo cho version bạn đang dùng trước khi sao chép JSON từ bất kỳ bài blog nào, kể cả bài này. In phiên bản của bạn bằng claude --version.

Vị trí lưu cấu hình hook

Một hook là một khối JSON trong file settings. Có 6 vị trí có thể chứa hook, và phạm vi của file cũng là phạm vi của hook.

  • ~/.claude/settings.json: áp dụng cho mọi project trên máy của bạn, không áp dụng cho máy của người khác.
  • .claude/settings.json: áp dụng cho một project, được commit vào repository, nên mọi người clone project đó đều nhận được hook.
  • .claude/settings.local.json: áp dụng cho một project, chỉ trên máy của bạn.
  • Managed policy settings: áp dụng trên toàn tổ chức và do administrator thiết lập.
  • hooks/hooks.json bên trong một plugin, có hiệu lực khi plugin đó được enable.
  • Frontmatter của skill hoặc subagent, có hiệu lực khi component đó đang active.

Các entry của hook trong những file này được merge thay vì ghi đè lẫn nhau. File settings của project thêm các hook của nó vào các hook trong user settings, thay vì thay thế chúng. Vì vậy, một event có thể chứa nhiều hook từ nhiều file. Đặt "disableAllHooks": true sẽ tắt chúng, ngoại trừ một trường hợp: các hook từ managed policy settings vẫn tiếp tục chạy, trừ khi setting đó cũng được áp dụng trong managed settings.

Chạy /hooks trong một session để liệt kê mọi hook đang được đăng ký, nhóm theo event, kèm source file và matcher của từng hook. Menu này chỉ đọc, nên bạn thay đổi hook bằng cách chỉnh sửa file settings. File watcher thường tự nhận thay đổi mà không cần restart.

Các sự kiện hook nào tồn tại trong Claude Code

Bản phát hành 2.1.232 liệt kê ba mươi mốt sự kiện, từ SessionStart đến SessionEnd, bao quát compaction, subagent, worktree và file cấu hình. Khi quản trị server, bạn chỉ cần dùng một số sự kiện trong đó.

  • PreToolUse: trước khi tool call được thực thi. Đây là sự kiện có thể chặn.
  • PostToolUse: sau khi tool call thành công. PostToolUseFailure được kích hoạt khi tool call thất bại, vì vậy hook phải theo dõi mọi kết quả cần dùng cả hai sự kiện.
  • PermissionRequest: khi tool call cần quyết định cấp quyền, tức thời điểm approval prompt sẽ xuất hiện.
  • UserPromptSubmit: khi bạn gửi prompt, trước khi Claude xử lý prompt đó. Mọi nội dung hook này ghi vào stdout sẽ được thêm vào context của model.
  • SessionStartSessionEnd: lần lượt ở mỗi đầu phiên. SessionStart cũng được kích hoạt sau compaction, với giá trị matcher là compact.
  • Stop: khi Claude hoàn tất phản hồi. Sự kiện này chạy một lần cho mỗi lượt, không phải một lần cho mỗi task hoàn tất.

Mỗi nhóm có một matcher quyết định những lần xuất hiện nào sẽ chạy hook. Với các sự kiện của tool, nó lọc theo tên tool, nên "Edit|Write" chỉ được kích hoạt khi chỉnh sửa file và không chạy trong trường hợp nào khác. Matcher phân biệt chữ hoa, chữ thường. Matcher rỗng sẽ được kích hoạt cho mọi lần xuất hiện. Các tool từ server MCP (model context protocol) có tên là mcp__<server>__<tool>, nên matcher "mcp__github__.*" chỉ bắt các tool của một server và không ảnh hưởng đến các server khác.

Các hook Stop có một điểm cần lưu ý trước khi bạn viết hook. Một hook Stop chặn sẽ yêu cầu model tiếp tục xử lý, và Claude Code sẽ ghi đè hook sau tám lần chặn liên tiếp. Hãy đọc trường stop_hook_active từ input của hook và thoát với mã 0 khi giá trị này là true; nếu không, hook sẽ lặp cho đến khi đạt giới hạn đó.

Hook nhận gì qua stdin

Khi Claude sắp chạy npm test, một hook PreToolUse trên Bash sẽ đọc dữ liệu sau từ stdin:

{
  "session_id": "abc123",
  "cwd": "/home/deploy/myproject",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test"
  }
}

Mọi event đều có session_id, cwd, permission_mode, transcript_pathhook_event_name. Các event của tool bổ sung tool_name, tool_inputtool_use_id. Những event khác có các trường riêng: UserPromptSubmit nhận nội dung prompt, còn SessionStart nhận một source của startup, resume, clear, compact hoặc fork.

jq là cách thông thường để đọc dữ liệu này trong shell script, nhưng image server tối giản không có sẵn công cụ đó. Trước tiên, hãy cài đặt bằng sudo apt install -y jq trên Ubuntu và Debian.

Tác động của exit status đến tool call đang thực hiện

Có 3 kết quả.

  • Exit 0 có nghĩa là hook không phản đối. Với PreToolUse, điều đó không đồng nghĩa với việc đã được phê duyệt và luồng cấp quyền thông thường vẫn tiếp tục. Với UserPromptSubmitSessionStart, stdout được thêm vào context của model.
  • Exit 2 chặn hành động đối với những event có thể bị chặn, trong đó có PreToolUse, và stderr trở thành lý do được hiển thị cho model. Với những event không thể bị chặn, chẳng hạn PostToolUse, yêu cầu chặn bị bỏ qua, nhưng stderr vẫn được gửi đến model dưới dạng feedback.
  • Mọi exit code khác đều là lỗi không chặn. Hành động vẫn tiếp tục. Transcript hiển thị thông báo lỗi của hook, kèm dòng đầu tiên của stderr sau phần text Failed with non-blocking status code:.

Nếu cần xử lý khác ngoài chặn hoặc không phản hồi, hãy dùng exit 0 và in một JSON object ra stdout. Hook PreToolUse đưa ra quyết định bằng permissionDecision:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Database drops go through a migration, not through the agent."
  }
}

"allow" bỏ qua prompt tương tác, "deny" hủy call và gửi lý do cho model, còn "ask" hiển thị prompt như bình thường. Mỗi hook nên dùng một kiểu duy nhất. Nếu trộn exit 2 với quyết định JSON trên stdout, bạn sẽ nhận được một kết quả phải tra cứu mới biết ý nghĩa.

Khi nhiều hook khớp với cùng một event, chúng chạy song song và hook nào cũng chạy đến khi hoàn tất. deny từ một hook không dừng các hook cùng cấp, vì vậy hook ghi log vẫn ghi dòng của nó trong khi hook guardrail từ chối cùng call đó. Sau đó Claude Code gộp các câu trả lời và giữ lại kết quả hạn chế nhất theo thứ tự deny, defer, ask, allow.

Ví dụ 1: chặn một lệnh có thể phá hủy trước khi lệnh chạy

Lưu nội dung này thành .claude/hooks/block-destructive.sh trong project của bạn:

#!/bin/bash
# Deny a Bash tool call whose command matches a banned pattern.
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

for pattern in 'rm -rf /' 'mkfs' 'dd if=' 'DROP TABLE'; do
  if printf '%s' "$COMMAND" | grep -qiF -- "$pattern"; then
    echo "Blocked by policy: the command matches '$pattern'. A human runs this one." >&2
    exit 2
  fi
done

exit 0

Cấp quyền thực thi cho file, rồi đăng ký file đó tại PreToolUse trong .claude/settings.json:

chmod +x .claude/hooks/block-destructive.sh
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.sh",
            "timeout": 10,
            "statusMessage": "Checking the command against policy"
          }
        ]
      }
    ]
  }
}

Chạy thử script thủ công trước khi tin dùng, vì hook bị crash khi xử lý input của chính nó sẽ chuyển sang trạng thái cho phép:

echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /var/lib/postgresql"}}' \
  | .claude/hooks/block-destructive.sh
echo $?

Bạn sẽ thấy dòng Blocked by policy: trên stderr và exit code là 2. Cấp cho nó một lệnh vô hại như ls -la; bạn sẽ không thấy output nào và exit code là 0. Trong một session, lời gọi bị từ chối sẽ xuất hiện trong transcript cùng với message của bạn làm lý do, và model sẽ đọc message đó rồi điều chỉnh.

Một đặc tính khiến việc này đáng làm: hook PreToolUse chạy trước bước kiểm tra permission mode, trong mọi permission mode, nên lệnh từ chối vẫn bị chặn ngay cả khi dùng bypassPermissions. Vì vậy hook hữu ích khi dùng cùng chế độ tự động và các thiết lập quyền của Claude Code, trong đó các prompt bị giảm nhưng hook vẫn chạy.

Cần hiểu đúng giới hạn của cách này. So khớp mẫu trên chuỗi lệnh là một guardrail để ngăn agent bất cẩn, không phải ranh giới bảo vệ trước một agent biết cách né tránh, vì cùng một lệnh có thể được viết theo dạng mà grep không nhận ra. Các quy tắc bắt buộc phải được đặt trong hệ thống permission và trong account mà process chạy dưới đó.

Ví dụ 2: format và lint sau mỗi lần chỉnh sửa

PostToolUse với matcher Edit|Write sẽ chạy sau mọi tool chỉnh sửa file. Lưu nội dung sau thành .claude/hooks/after-edit.sh:

#!/bin/bash
# Format the edited file, then report lint failures back to the model.
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0

case "$FILE" in
  *.py)
    ruff format "$FILE" >/dev/null 2>&1
    if ! ruff check "$FILE" >&2; then
      exit 2
    fi
    ;;
  *.sh)
    if ! shellcheck "$FILE" >&2; then
      exit 2
    fi
    ;;
esac

exit 0
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/after-edit.sh",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

Yêu cầu Claude thêm một function thụt lề sai vào file Python, rồi mở file đó. File sẽ được format lại. Đây là cách kiểm tra hook đã chạy, vì hook chạy thành công sẽ không hiển thị gì trong cuộc hội thoại.

Exit 2 ở đây không hoàn tác thay đổi. PostToolUse chạy sau khi tool đã thực thi, nên thay đổi vẫn được ghi vào disk. Tác dụng của exit 2 là đưa output của ruff check đến model dưới dạng feedback, để model sửa lỗi vừa tạo thay vì chuyển sang bước tiếp theo. Đây là khác biệt giữa lỗi lint chỉ được phát hiện lúc commit và lỗi được agent tự sửa ngay trong cùng turn.

Ở đây có 2 giới hạn của matcher cần lưu ý. Edit|Write không thấy các file được thay đổi bằng shell command, trong khi Claude thường ghi file qua Bash nên khoảng trống này có thể xảy ra. Để bao phủ theo từng lần gọi, hãy match cả Bash và cho script liệt kê các file đã thay đổi bằng git status --porcelain. Để bao phủ một lần cho mỗi turn, hãy đặt bước scan vào hook Stop thay vào đó.

Ví dụ 3: ghi log mọi lần gọi tool để audit

Matcher rỗng trên PostToolUse chạy với mọi tool. Gửi bản ghi vào system journal thay vì ghi vào file trong thư mục home giúp agent không thể truy cập bản ghi đó từ shell của chính nó:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "jq -c '{time: now|todate, session: .session_id, cwd: .cwd, tool: .tool_name, input: .tool_input}' | logger -t claude-code -p local0.info"
          }
        ]
      }
    ]
  }
}

Đọc lại bằng journalctl -t claude-code -o cat | tail -n 5. Bạn sẽ thấy mỗi lần gọi tool tương ứng với một dòng JSON, trong đó dòng mới nhất nằm cuối. Nếu không có dòng nào, hook chưa chạy; phần khắc phục sự cố bên dưới sẽ giải thích cách kiểm tra.

Thêm cùng block này dưới PostToolUseFailure để ghi nhận các lần gọi bị lỗi, vì PostToolUse chỉ chạy khi thành công và một command bị lỗi thường mới là thông tin cần xem. Lý do dùng logger thay vì append vào file trong thư mục home là vấn đề ownership: hook chạy với cùng user như shell của agent, nên mọi thứ user đó có thể append thì cũng có thể truncate. Journal được ghi bởi systemd-journald bằng account riêng của nó.

Thời gian hook được phép chạy

ChartDefault hook timeout in seconds, by hook type and event
The data behind this chart
[
  {
    "label": "command, http or mcp_tool hook",
    "default_timeout_seconds": 600
  },
  {
    "label": "agent hook",
    "default_timeout_seconds": 60
  },
  {
    "label": "prompt hook",
    "default_timeout_seconds": 30
  },
  {
    "label": "command hook on UserPromptSubmit",
    "default_timeout_seconds": 30
  },
  {
    "label": "command hook on MessageDisplay",
    "default_timeout_seconds": 10
  },
  {
    "label": "any hook on SessionEnd",
    "default_timeout_seconds": 1.5
  }
]

Mặc định, command hook được chạy trong 600 giây, tức 10 phút. Một số event sẽ giảm mạnh thời gian này. Các hook SessionEnd dùng chung ngân sách 1.5 giây, nên việc dọn dẹp cuối session phải hoàn tất nhanh. Tuy nhiên, nếu đặt timeout dài hơn cho hook, ngân sách dùng chung cũng tăng tương ứng, tối đa 60 giây.

Hook chạm timeout sẽ bị hủy và không đưa ra quyết định nào. Với guardrail PreToolUse, điều đó có nghĩa là hook không chặn thao tác: tool call tiếp tục đi vào luồng kiểm tra permission thông thường. Vì vậy, hãy giữ các script guardrail ngắn và gọn. Với các tác vụ chậm mà không cần chờ, chẳng hạn gửi log đến nơi khác, hãy đặt "async": true để hook chạy background mà không giữ tool call lại.

Hook, rules file, skill và MCP server

Bốn khái niệm này thường bị nhầm lẫn vì chúng đều thay đổi cách agent hoạt động. Chỉ một trong số đó không còn là đề xuất.

Rules file (`CLAUDE.md hoặc file nằm dưới .claude/rules/`) là văn bản được nạp vào context của model. Nó định hình hành vi nhưng không thực thi điều gì. Khi đặt cạnh một cuộc hội thoại dài, một diff lớn và một yêu cầu mới từ user, một dòng trong rules file có thể bị bỏ qua. Đây là cơ chế thông thường khiến agent bỏ qua các instruction bạn đã ghi lại.

Skill là một thư mục chứa instruction và script. Model sẽ nạp skill khi đánh giá rằng skill đó phù hợp. Chính việc đánh giá này là mục đích của skill, đồng thời cũng là giới hạn của nó: model vẫn là bên quyết định. Bạn có thể thấy rõ cả hai mặt này trong một skill như Ponytail, skill hướng agent đến thay đổi nhỏ nhất nhưng vẫn hoạt động, vì nó định hình cách tiếp cận toàn bộ task theo cách mà hook không thể làm được, nhưng chỉ hoạt động khi model chọn nạp nó.

MCP (model context protocol) server cung cấp cho model các tool mới để gọi. Nó mở rộng phạm vi tài nguyên mà agent có thể truy cập. Nó không khiến agent tự động sử dụng bất kỳ tài nguyên nào, và đây cũng là một process riêng mà bạn phải vận hành, tức là một công việc riêng: xem cách chạy MCP server trên VPS.

Hook là cơ chế duy nhất trong bốn cơ chế này chạy mà không cần model lựa chọn. Dùng rules file cho một tùy chọn và skill cho một quy trình mà model nên tuân theo khi phù hợp. Dùng hook cho bước bắt buộc phải chạy mỗi lần hoặc cho việc tuyệt đối không được xảy ra. Phần so sánh sâu hơn, bao gồm cả trường hợp skill phù hợp hơn rules file, nằm trong bài so sánh skill, MCP và rules file.

Plugin là cách đóng gói, không phải cơ chế thứ năm. Nó gộp hook và skill thành một đơn vị có thể cài đặt, giúp team triển khai cùng một guardrail trên mọi máy: xem cách Claude Code plugin hoạt động.

Quyết định bảo mật trên VPS dùng chung

Hook là đoạn code do agent kích hoạt và chạy dưới user đã khởi động Claude Code. Hook kế thừa môi trường và quyền truy cập file của user đó. Trên laptop, đây là vấn đề về quy trình làm việc. Trên VPS nơi agent chạy không cần giám sát, đây là vấn đề bảo mật với 4 khía cạnh thực tế.

Hook trong một repository là code do bạn không viết. .claude/settings.json được commit, vì vậy việc clone một repository rồi bắt đầu session bên trong đó có thể đăng ký các hook đi kèm repository. Claude Code yêu cầu xác nhận trong hộp thoại workspace trust trước khi chạy project hook cho thư mục đó. Vì vậy, chấp nhận trust là thời điểm bạn quyết định chạy các hook này. Trước tiên, hãy đọc block hooks.

Hook nhìn thấy toàn bộ tool input. Một audit hook ghi log tool_input sẽ ghi mọi argument của mọi command vào file, bao gồm cả token vô tình xuất hiện trên command line. Log đó sau đó cần được bảo vệ như chính secret, và đây là một phần của vấn đề rộng hơn: giữ secret ngoài tầm với của AI agent.

Hook có thể ghi dữ liệu vào context của model. Bất kỳ nội dung nào hook SessionStart hoặc UserPromptSubmit ghi ra stdout đều được thêm vào conversation. Một hook đưa text từ nguồn bên ngoài, issue tracker hoặc log file vào, đang chuyển text không đáng tin cậy cho model như thể chính bạn đã nhập text đó. Hãy xem stdout này là input, không phải output.

Privilege mới là cơ chế kiểm soát thực sự. Chạy agent dưới một user riêng không có privilege, chỉ cấp các rule sudo cần thiết. Một lệnh deny PreToolUse vẫn đáng cấu hình, và theo thiết kế đây là cơ chế best effort: tài liệu tham chiếu cũng nói như vậy về filter if, đồng thời yêu cầu dùng permission system khi cần deny tuyệt đối. Permission rule và account mà process chạy dưới đó là các thành phần vẫn có hiệu lực khi hệ thống chịu áp lực.

Có một thuộc tính đúng trong mọi cấu hình. Hook PreToolUse chạy trước bước kiểm tra permission mode trong mọi permission mode, vì vậy hook trả về deny sẽ chặn tool ngay cả khi dùng bypassPermissions. Hook có thể siết chặt những gì permission rule cho phép. Hook không thể nới lỏng các quyền đó.

Vì sao hook của tôi không chạy?

Hãy kiểm tra theo thứ tự này. Mỗi bước mô tả triệu chứng bạn sẽ thực sự thấy.

  • Chạy /hooks và kiểm tra xem hook có xuất hiện dưới event bạn mong đợi không. Hook không xuất hiện trong menu thường có nghĩa là file settings bị lỗi cú pháp JSON, vì JSON không cho phép dấu phẩy ở cuối và comment, hoặc file không nằm trong một trong sáu vị trí ở trên.
  • So khớp matcher chính xác với tên tool. Matcher phân biệt chữ hoa chữ thường, nên "bash" không bao giờ khớp với tool Bash.
  • Chạy script thủ công với input mẫu như trong ví dụ 1 ở trên. Exit code ngoài dự kiến là lỗi trong script của bạn. Claude Code báo đây là lỗi hook, không phải quyết định.
  • Thông báo jq: command not found có nghĩa là máy đó thiếu jq. command not found đối với script của bạn có nghĩa là path không được resolve, vì vậy hãy dùng ${CLAUDE_PROJECT_DIR} hoặc path tuyệt đối. Nếu script hoàn toàn không chạy, có thể script chưa có quyền thực thi.
  • Hook in ra JSON hợp lệ nhưng không có gì xảy ra. Hook dạng shell chạy qua sh -c. Nếu shell profile in banner, banner đó sẽ được thêm vào trước JSON. Stdout không còn bắt đầu bằng {, nên Claude Code đọc toàn bộ nội dung dưới dạng plain text và bỏ qua quyết định. Khi exit 0, không có thông tin nào được báo ở nơi khác ngoài debug log. Bọc mọi echo trong profile để chúng chỉ chạy trong shell tương tác.
  • Vẫn chưa xử lý được: khởi động session bằng claude --debug-file /tmp/claude.log rồi chạy tail -f /tmp/claude.log trong terminal thứ hai. Debug log ghi lại hook nào đã khớp, exit code mà từng hook trả về, cùng mọi nội dung chúng ghi vào stdout và stderr.

FAQ

Sự khác nhau giữa hook của Claude Code và chỉ thị trong CLAUDE.md là gì?

Chỉ thị CLAUDE.md là văn bản trong context của model, nên phải cạnh tranh sự chú ý với cuộc hội thoại và yêu cầu hiện tại; model có thể cân nhắc chỉ thị đó cùng với các nội dung này. Hook là một shell command được Claude Code chạy tại một thời điểm cố định trong vòng đời của nó, nên được thực thi mỗi khi event tương ứng xảy ra, bất kể model đã quyết định gì. Dùng chỉ thị cho một tùy chọn ưu tiên. Dùng hook cho một bước bắt buộc phải luôn thực hiện hoặc một hành động tuyệt đối không được thực hiện.

Làm cách nào để ngăn Claude Code chạy một shell command cụ thể?

Đăng ký hook PreToolUse với matcher Bash đọc command từ .tool_input.command, ghi lý do vào stderr rồi thoát với mã 2. Claude Code hủy lệnh gọi và hiển thị lý do của bạn cho model. Việc này xảy ra trước bước kiểm tra permission mode, nên lệnh từ chối vẫn có hiệu lực ngay cả trong mode bypassPermissions. So khớp mẫu trên chuỗi command chỉ là một biện pháp guardrail, không phải ranh giới bảo mật, vì cùng một command có thể được viết theo cách mà mẫu không nhận ra. Do đó, hãy kết hợp với permission rule và tài khoản không có quyền nâng cao.

Hook của tôi in ra JSON hợp lệ nhưng không có gì xảy ra. Tại sao?

Nguyên nhân phổ biến nhất là shell profile. Hook không có trường args sẽ chạy thông qua sh -c. Một số profile in banner mỗi khi mở shell, khiến banner được ghi vào stdout trước JSON. Vì output không còn bắt đầu bằng {, Claude Code xem toàn bộ output là plain text và bỏ qua quyết định. Khi tiến trình thoát với mã 0, transcript cũng không ghi nhận gì. Hãy đặt mọi echo trong profile sau một kiểm tra interactive shell, rồi xác nhận đã sửa bằng cách đọc debug log từ claude --debug-file /tmp/claude.log.

Chạy hook của Claude Code trên shared server có an toàn không?

Hook chạy với tư cách user đã khởi động Claude Code và có quyền truy cập file của user đó, nên hook có thể thực hiện mọi việc mà tài khoản này được phép thực hiện. Hai thói quen sau bao quát phần lớn rủi ro: chạy agent bằng một tài khoản riêng không có quyền nâng cao, với policy sudo hạn chế; đồng thời đọc block hooks của mọi repository trước khi chấp nhận hộp thoại workspace trust, vì project hook nằm trong .claude/settings.json. Đặt "disableAllHooks": true trong file settings khi bạn không muốn chạy bất kỳ hook nào trong số đó.