SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-22

Claude Code Hooks: Cách hoạt động và cấu hình

Tìm hiểu Claude Code hooks 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 lifecycle của chính nó. Đây là điểm khác biệt hoàn toàn giữa hook và rules file. Instruction trong CLAUDE.md chỉ là hướng dẫ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à luôn chạy, bất kể model có đồng ý hay không. Nếu agent liên tục bỏ qua formatter dù bạn đã nhắc 2 lần, bạn không cần instruction 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 một event name. 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ừ một hook PreToolUse sẽ hủy tool call trước khi tool chạy. Mọi nội dung script ghi vào standard error (stderr) sẽ được chuyển lại cho model làm lý do.

Các event name và field name 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 tương ứng với 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 thông tin của version đang dùng bằng claude --version.

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

Hook là một block 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: mọi project trên máy của bạn, chỉ áp dụng cho máy này.
  • .claude/settings.json: 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: một project, chỉ áp dụng 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 plugin, có hiệu lực khi plugin đó được bật.
  • Frontmatter của skill hoặc subagent, có hiệu lực khi component đó đang hoạt động.

Các entry hook từ những file này được merge thay vì ghi đè lẫn nhau. File settings của project thêm hook 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. Thiết lập "disableAllHooks": true sẽ tắt các hook, trừ một trường hợp: hook từ managed policy settings vẫn tiếp tục chạy, trừ khi thiết lập đó 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ý, được nhóm theo event, kèm source file và matcher của từng hook. Menu này chỉ đọc, nên bạn phải chỉnh sửa hook trong file settings. File watcher thường tự nhận thay đổi mà không cần restart.

Các event hook của Claude Code gồm những gì

Bản Release 2.1.232 liệt kê ba mươi mốt event, từ SessionStart đến SessionEnd, bao phủ compaction, subagent, worktree và file cấu hình. Khi làm việc với server, bạn thường chỉ dùng một số event trong đó.

  • PreToolUse: chạy trước khi một tool call được thực thi. Đây là event có thể chặn thao tác.
  • PostToolUse: chạy sau khi một tool call thành công. PostToolUseFailure chạy thay thế khi tool call thất bại, vì vậy hook cần thấy mọi kết quả phải dùng cả hai event.
  • PermissionRequest: chạy khi một tool call cần quyết định quyền, tức thời điểm prompt yêu cầu phê duyệt xuất hiện.
  • UserPromptSubmit: chạy khi bạn gửi prompt, trước khi Claude xử lý prompt đó. Mọi nội dung hook này in ra stdout sẽ được thêm vào context của model.
  • SessionStartSessionEnd: chạy ở mỗi đầu và cuối session. SessionStart cũng chạy sau compaction, với giá trị matcher là compact.
  • Stop: chạy khi Claude hoàn tất phản hồi. Event 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 occurrence nào sẽ chạy hook. Với các event của tool, matcher lọc theo tên tool, vì vậy "Edit|Write" chỉ chạy khi chỉnh sửa file và không chạy trong trường hợp khác. Matcher phân biệt chữ hoa chữ thường. Matcher rỗng sẽ chạy cho mọi occurrence. Tool từ server MCP (model context protocol) có tên dạng mcp__<server>__<tool>, vì vậy matcher "mcp__github__.*" sẽ bắt các tool của một server và bỏ qua tool của các server khác.

Stop hook có một điểm cần lưu ý trước khi bạn viết hook. Một hook Stop chặn thao tác 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 field stop_hook_active từ input của hook và thoát với mã 0 khi field này có giá trị true. Nếu không, hook sẽ lặp cho đến khi đạt giới hạn đó.

Dữ liệu hook nhận qua stdin

Khi Claude sắp chạy npm test, 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 tool event bổ sung tool_name, tool_inputtool_use_id. Những event khác có các field riêng: UserPromptSubmit nhận phần text prompt, còn SessionStart nhận một source gồm 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 các image server tối giản không có sẵn nó. Trước tiên, hãy cài bằng sudo apt install -y jq trên Ubuntu và Debian.

Exit status tác động thế nào đến tool call đang thực hiện

Có 3 kết quả.

  • Exit 0 nghĩa là hook không phản đối. Với PreToolUse, điều này 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 action đối với các event có thể bị chặn, trong đó có PreToolUse, và stderr trở thành lý do được hiển thị cho model. Với các event không thể bị chặn, chẳng hạn PostToolUse, yêu cầu chặn sẽ bị bỏ qua, nhưng stderr vẫn được gửi đến model dưới dạng feedback.
  • Bất kỳ exit code nào khác đều là lỗi không chặn. Action 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 văn bản Failed with non-blocking status code:.

Nếu cần xử lý nhiều hơn việc chặn hoặc không làm gì, hãy dùng exit 0 và in một JSON object ra stdout. Một hook PreToolUse 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 interactive prompt, "deny" hủy call và gửi lý do cho model, còn "ask" hiển thị prompt như bình thường. Hãy chọn một kiểu cho mỗi hook. Nếu trộn exit 2 với JSON decision trên stdout, bạn sẽ nhận được kết quả phải tra cứu mới biết ý nghĩa.

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

Ví dụ 1: chặn một lệnh phá hoại 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 đó trên 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"
          }
        ]
      }
    ]
  }
}

Tự chạy thử script trước khi tin tưởng nó, vì một 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 script 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ần gọi bị từ chối sẽ xuất hiện trong transcript cùng với thông báo của bạn làm lý do. Model sẽ đọc thông báo đó và tự đ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. Vì vậy, lệnh deny vẫn có hiệu lực ngay cả với bypassPermissions. Đây là lý do hook hữu ích khi dùng cùng chế độ tự động và các thiết lập permission của Claude Code, trong đó các prompt bị giảm bớt 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 pattern trên chuỗi lệnh chỉ là một guardrail để ngăn agent bất cẩn. Nó không phải là ranh giới bảo vệ trước một agent biết cách lách, vì cùng một lệnh có thể được viết theo dạng mà grep không nhìn thấy. Các quy tắc bắt buộc phải đặt trong permission system và trong account mà process chạy dưới quyền đó.

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 này 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 được trả về đã được format. Đâ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 trò chuyện.

Lệnh 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 trong cả hai trường hợp. exit 2 giúp đưa output của ruff check trở lại model dưới dạng feedback, để model sửa lỗi vừa tạo thay vì tiếp tục. Đâ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 lượt.

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 là có thật. Để 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 lượt, hãy đặt bước scan trong hook Stop thay vào đó.

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

Matcher trố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 một 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, dòng mới nhất nằm cuối. Nếu không có dòng nào, hook đã không chạy. Phần xử lý sự cố bên dưới sẽ hướng dẫn cách kiểm tra.

Thêm block tương tự bên 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 thất bại thường mới là thông tin đáng quan tâm. Lý do dùng logger thay vì append vào một file trong thư mục home là vấn đề quyền sở hữu: hook chạy dưới cùng user với shell của agent, nên mọi thứ user đó có quyền append thì cũng có thể truncate. Journal được systemd-journald ghi dưới account riêng của nó.

Thời gian một 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
  }
]

Theo mặc định, command hook được chạy trong 600 giây, tức 10 phút. Một số event giới hạn thời gian này rất mạnh. Các hook SessionEnd dùng chung ngân sách 1.5 giây, nên việc cleanup cuối session phải hoàn tất nhanh. Tuy nhiên, đặt timeout dài hơn cho hook sẽ tăng ngân sách dùng chung 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 theo permission flow thông thường. Vì vậy, hãy giữ các guardrail script ở mức nhỏ. Với công việc chạy 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 làm tool call bị chờ.

Hooks, rules file, skill và MCP server

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

Rules file (`CLAUDE.md hoặc một file bên 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 được điều gì. Khi đặt cạnh một cuộc hội thoại dài, một diff lớn và yêu cầu mới từ user, một dòng trong đó 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 mà model nạp khi đánh giá skill đó phù hợp. Việc đánh giá này chính là mục đích của skill, đồng thời cũng là giới hạn của nó: model vẫn là thành phần quyết định. Có thể thấy cả hai mặt trong một skill như Ponytail, giúp agent hướng đến thay đổi nhỏ nhất nhưng vẫn hoạt động, vì nó định hình cách xử lý toàn bộ task theo cách mà hook không thể làm được, và 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 mà agent có thể truy cập. Nó không khiến agent tự động sử dụng bất kỳ thứ gì, đồng thời đây là một process riêng mà bạn phải vận hành, bản thân việc đó cũng là một công việc riêng: xem cách chạy MCP server trên VPS.

Hook là thành phần duy nhất trong bốn thành phần này chạy mà không cần model lựa chọn. Dùng rules file cho một preference và dùng skill cho một procedure mà model phải làm theo khi phù hợp. Dùng hook cho bước bắt buộc phải xảy ra mỗi lần, hoặc cho việc tuyệt đối không được xảy ra. Phần so sánh chi tiết hơn, bao gồm trường hợp skill phù hợp hơn rules file, có 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ó gom các hook và skill vào một unit có thể cài đặt, nhờ đó team có thể triển khai cùng một guardrail lên mọi máy: xem cách Claude Code plugin hoạt động.

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

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

Hook trong repository là code bạn không tự viết. .claude/settings.json được commit, vì vậy việc clone repository rồi bắt đầu session bên trong đó có thể đăng ký các hook đi kèm repository. Claude Code đặt các hook cấp project sau workspace trust dialog của thư mục đó. Điều này có nghĩa là chấp nhận trust là thời điểm bạn quyết định chạy chúng. Đọc block hooks trước.

Hook 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ệ giống như secret, thuộc cùng vấn đề rộng hơn là 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 in ra stdout đều được thêm vào cuộc hội thoại. Hook lấy text từ bên ngoài, issue tracker hoặc log file rồi pipe vào sẽ đưa text không đáng tin cậy cho model như thể chính bạn đã nhập. Hook chuyển tiếp ghi chú từ một Claude Code session khác trên cùng VPS cũng làm điều tương tự. Output của một agent không đáng tin hơn issue tracker. Hãy xem stdout đó là input, không phải output.

Quyền là cơ chế kiểm soát thực sự. Chạy agent bằng một user riêng, không có đặc quyền, chỉ với các rule sudo cần thiết. Một lệnh deny PreToolUse vẫn hữu ích và được thiết kế theo hướng best effort. Tài liệu tham chiếu cũng nói như vậy về filter if và yêu cầu bạn dùng permission system khi cần deny cứng. Permission rules và account mà process chạy dưới đó là những phần vẫn có hiệu lực khi có sự cố.

Có một thuộc tính đúng trong mọi cấu hình. PreToolUse hooks chạy trước permission-mode check 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 rules cho phép. Chúng 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 nêu triệu chứng bạn sẽ thực sự thấy.

  • Chạy /hooks và kiểm tra hook có xuất hiện dưới event bạn mong đợi hay không. Hook không có trong menu thường có nghĩa là file settings bị lỗi cú pháp JSON, vì không được dùng dấu phẩy cuối hoặc comment, hoặc file không nằm ở một trong 6 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 không đúng với dự kiến là lỗi trong script. Claude Code báo đây là hook error thay vì một decision.
  • 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, nên 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 executable.
  • 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. Khi đó 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 decision. Với exit 0, không có thông tin nào được báo ngoài debug log. Hãy bọc mọi echo trong profile để chúng chỉ chạy trong interactive shell.
  • Nếu vẫn chưa xử lý được, hãy bắt đầu session bằng claude --debug-file /tmp/claude.log và chạy tail -f /tmp/claude.log trong terminal thứ hai. Debug log ghi lại những hook đã 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

Khác biệt giữa hook của Claude Code và chỉ dẫn trong CLAUDE.md là gì?

Chỉ dẫn 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à request hiện tại; model có thể cân nhắc chỉ dẫn đó cùng với chúng. Hook là một shell command mà Claude Code chạy tại một thời điểm cố định trong vòng đời, nên nó được thực thi mỗi khi sự kiện đó xảy ra, bất kể model đã quyết định gì. Dùng chỉ dẫn 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 xảy ra hoặc một hành động tuyệt đối không được xảy ra.

Làm thế 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ời gọi và hiển thị lý do của bạn cho model. Việc này xảy ra trước khi kiểm tra permission-mode, nên lệnh deny vẫn có hiệu lực trong mode bypassPermissions. So khớp pattern trên chuỗi command chỉ là một lớp bảo vệ, 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à pattern không bắt được. Vì vậy, hãy kết hợp với permission rules và một account không có đặc quyền.

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

Nguyên nhân thường gặp nhất là shell profile. Hook không có trường args sẽ chạy qua sh -c. Một số profile in banner mỗi khi khởi động shell, khiến banner xuất hiện trên stdout trước JSON của bạn. Vì output không còn bắt đầu bằng {, Claude Code coi toàn bộ output là plain text và bỏ qua quyết định. Khi process thoát với mã 0, transcript cũng không hiển thị gì. Hãy bọc mọi echo trong profile bằng 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 dưới account đã khởi động Claude Code và có quyền truy cập file của account đó, nên hook có thể thực hiện mọi thao tác mà account này được phép thực hiện. Hai thói quen sau đủ để xử lý phần lớn rủi ro: chạy agent bằng một account chuyên dụng không có đặc quyền với chính sách sudo giới hạn, và đọc block hooks của mọi repository trước khi chấp nhận workspace trust dialog, vì project hook được cung cấp trong .claude/settings.json. Đặt "disableAllHooks": true trong settings file nếu bạn không muốn chạy bất kỳ hook nào trong số đó.