วิธีใช้งาน Claude Code Hooks และการตั้งค่าคำสั่ง Shell
ทำความเข้าใจการทำงานของ Claude Code Hooks ที่รันคำสั่งอัตโนมัติโดยไม่ขึ้นกับโมเดล พร้อมวิธีจัดการ exit code 2 เพื่อยกเลิก tool call และการรับส่งข้อมูลผ่าน stdin
Hook ของ Claude Code คืออะไร
Hook ของ Claude Code คือคำสั่ง shell ที่ Claude Code เรียกใช้งานด้วยตัวเอง ณ จุดที่กำหนดไว้ในวงจรการทำงานของมัน นี่คือความแตกต่างทั้งหมดระหว่าง hook กับไฟล์กฎ (rules file) คำสั่งใน CLAUDE.md เป็นเพียงคำแนะนำ ซึ่งโมเดลจะนำไปพิจารณาเทียบกับข้อมูลอื่นทั้งหมดในบริบท แต่ hook คือโค้ดที่จะถูกเรียกใช้งานไม่ว่าโมเดลจะเห็นด้วยหรือไม่ก็ตาม หากเอเจนต์ของคุณยังคงข้ามการจัดรูปแบบโค้ด (formatter) ที่คุณระบุไว้ถึงสองครั้ง คุณไม่จำเป็นต้องให้คำสั่งที่เข้มงวดขึ้น แต่คุณต้องการ hook
กลไกนี้มีขนาดเล็ก คุณเพียงลงทะเบียนคำสั่งไว้ในไฟล์การตั้งค่าภายใต้ชื่อเหตุการณ์ เมื่อเหตุการณ์นั้นเกิดขึ้น Claude Code จะรันคำสั่งของคุณและเขียนข้อมูลเหตุการณ์ลงใน standard input (stdin) ในรูปแบบ JSON (JavaScript object notation) คำสั่งของคุณจะอ่านข้อมูลนั้น ดำเนินการตามที่ต้องการ และตอบกลับด้วยสถานะการออก (exit status) การออกด้วยสถานะ 2 จาก hook PreToolUse จะเป็นการยกเลิกการเรียกใช้เครื่องมือ (tool call) ก่อนที่จะเริ่มทำงาน และสิ่งที่สคริปต์ของคุณเขียนลงใน standard error (stderr) จะถูกส่งกลับไปยังโมเดลเพื่อใช้เป็นเหตุผล
ชื่อเหตุการณ์และชื่อฟิลด์ในที่นี้อ้างอิงจาก hooks reference ของ Claude Code ซึ่งตรวจสอบเมื่อเดือนสิงหาคม 2026 เทียบกับรุ่น 2.1.232 พื้นที่ส่วนนี้มีการเปลี่ยนแปลงอย่างรวดเร็ว ดังนั้นโปรดตรวจสอบข้อมูลอ้างอิงสำหรับเวอร์ชันของคุณก่อนที่จะคัดลอก JSON จากบล็อกโพสต์ใดๆ รวมถึงโพสต์นี้ด้วย คุณสามารถพิมพ์ข้อมูลของคุณได้ด้วย claude --version
ตำแหน่งการตั้งค่า hook
Hook คือบล็อก JSON ในไฟล์การตั้งค่า โดยสามารถเก็บไว้ได้ใน 6 ตำแหน่ง และขอบเขตของไฟล์จะเป็นขอบเขตของ hook นั้นด้วย
~/.claude/settings.json: ทุกโปรเจกต์บนเครื่องของคุณ และไม่มีผลกับเครื่องอื่น.claude/settings.json: หนึ่งโปรเจกต์ โดย commit ลงใน repository เพื่อให้ทุกคนที่ clone ไปได้รับ hook นี้ด้วย.claude/settings.local.json: หนึ่งโปรเจกต์ เฉพาะบนเครื่องของคุณเท่านั้น- การตั้งค่า Managed policy: ระดับองค์กร กำหนดโดยผู้ดูแลระบบ
hooks/hooks.jsonภายในปลั๊กอิน จะทำงานขณะที่ปลั๊กอินนั้นเปิดใช้งานอยู่- Frontmatter ของ Skill หรือ subagent จะทำงานขณะที่คอมโพเนนต์นั้นเปิดใช้งานอยู่
รายการ hook จากไฟล์เหล่านี้จะรวมเข้าด้วยกันแทนที่จะเขียนทับกัน ไฟล์การตั้งค่าโปรเจกต์จะเพิ่ม hook ของตนเข้าไปรวมกับ hook ในการตั้งค่าผู้ใช้ แทนที่จะแทนที่รายการเดิม ดังนั้นเหตุการณ์เดียวจึงสามารถมี hook หลายรายการจากหลายไฟล์ได้ การตั้งค่า "disableAllHooks": true จะเป็นการปิดการทำงานของ hook ทั้งหมด ยกเว้นกรณีเดียวคือ hook จากการตั้งค่า Managed policy จะยังคงทำงานต่อไป เว้นแต่ว่าจะมีการกำหนดการตั้งค่านั้นไว้ใน Managed settings ด้วยเช่นกัน
เรียกใช้ /hooks ภายในเซสชันเพื่อแสดงรายการ hook ทั้งหมดที่ลงทะเบียนไว้ในปัจจุบัน โดยจัดกลุ่มตามเหตุการณ์ พร้อมระบุไฟล์ต้นทางและตัวจับคู่ (matcher) ของแต่ละรายการ เมนูนี้เป็นแบบอ่านอย่างเดียว ดังนั้นคุณต้องแก้ไข hook โดยการแก้ไขไฟล์การตั้งค่า ไฟล์ที่ถูกเฝ้าติดตาม (file watcher) มักจะตรวจพบการแก้ไขโดยไม่ต้องเริ่มโปรแกรมใหม่
เหตุการณ์ hook ของ Claude Code มีอะไรบ้าง
Release 2.1.232 ระบุเหตุการณ์ไว้ 31 รายการ ตั้งแต่ SessionStart ไปจนถึง SessionEnd ซึ่งครอบคลุมถึงการบีบอัดข้อมูล (compaction), subagents, worktrees และไฟล์การตั้งค่า โดยการทำงานบนเซิร์ฟเวอร์จะใช้เหตุการณ์เหล่านี้เพียงบางส่วนเท่านั้น
PreToolUse: ก่อนที่ tool call จะทำงาน นี่คือเหตุการณ์ที่สามารถบล็อกการทำงานได้PostToolUse: หลังจาก tool call ทำงานสำเร็จ ส่วนPostToolUseFailureจะทำงานเมื่อเกิดความล้มเหลว ดังนั้น hook ที่ต้องการตรวจสอบทุกผลลัพธ์จำเป็นต้องใช้ทั้งสองเหตุการณ์PermissionRequest: เมื่อ tool call ต้องการการตัดสินใจเรื่องสิทธิ์ ซึ่งเป็นจังหวะเดียวกับที่จะแสดง prompt ขออนุมัติUserPromptSubmit: เมื่อคุณส่ง prompt ก่อนที่ Claude จะประมวลผล สิ่งที่ hook นี้พิมพ์ออกมายัง stdout จะถูกเพิ่มเข้าไปในบริบท (context) ของโมเดลSessionStartและSessionEnd: ที่จุดเริ่มต้นและจุดสิ้นสุดของเซสชัน ส่วนSessionStartจะทำงานหลังจากการบีบอัดข้อมูลภายใต้ค่า matcher คือcompactStop: เมื่อ Claude ตอบกลับเสร็จสิ้น ซึ่งจะเกิดขึ้นหนึ่งครั้งต่อหนึ่งเทิร์น ไม่ใช่หนึ่งครั้งต่อหนึ่งงานที่เสร็จสมบูรณ์
ทุกกลุ่มจะมี matcher ที่ทำหน้าที่ตัดสินว่าเหตุการณ์ใดบ้างที่จะเรียกใช้ hook ในส่วนของเหตุการณ์เกี่ยวกับเครื่องมือ (tool events) ระบบจะกรองด้วยชื่อเครื่องมือ ดังนั้น "Edit|Write" จะทำงานเฉพาะกับการแก้ไขไฟล์เท่านั้น ค่า matcher จะคำนึงถึงตัวพิมพ์เล็ก-ใหญ่ หากเว้นว่างไว้ hook จะทำงานในทุกเหตุการณ์ เครื่องมือจากเซิร์ฟเวอร์ MCP (model context protocol) จะถูกตั้งชื่อเป็น mcp__<server>__<tool> ดังนั้นการกำหนด matcher เป็น "mcp__github__.*" จะดักจับเฉพาะเครื่องมือของเซิร์ฟเวอร์นั้นและไม่ยุ่งกับเครื่องมืออื่น
hook ประเภท Stop มีกับดักที่ควรทราบก่อนเริ่มเขียน หาก hook ประเภท Stop มีการบล็อกการทำงาน ระบบจะส่งโมเดลกลับไปทำงานต่อ และ Claude Code จะข้าม hook นั้นไปหลังจากถูกบล็อกติดต่อกัน 8 ครั้ง ให้อ่านฟิลด์ stop_hook_active จากอินพุตของ hook และสั่ง exit 0 เมื่อเป็นค่า true มิฉะนั้น hook ของคุณจะทำงานวนซ้ำจนกว่าจะถึงขีดจำกัดดังกล่าว
สิ่งที่ hook ได้รับผ่าน stdin
เมื่อ Claude กำลังจะรัน npm test ตัว hook ประเภท PreToolUse บน Bash จะอ่านข้อมูลต่อไปนี้ผ่าน stdin:
{
"session_id": "abc123",
"cwd": "/home/deploy/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}ทุกเหตุการณ์จะประกอบด้วย session_id, cwd, permission_mode, transcript_path และ hook_event_name ส่วนเหตุการณ์ของเครื่องมือ (tool events) จะเพิ่ม tool_name, tool_input และ tool_use_id เข้ามา เหตุการณ์อื่นๆ จะมีฟิลด์เฉพาะของตนเอง โดย UserPromptSubmit จะได้รับข้อความ prompt และ SessionStart จะได้รับ source ของ startup, resume, clear, compact หรือ fork
jq เป็นวิธีปกติในการอ่านข้อมูลนี้ภายใน shell script แต่ image ของเซิร์ฟเวอร์แบบ minimal จะไม่มีเครื่องมือนี้ติดตั้งมาให้ ให้ติดตั้งก่อนด้วย sudo apt install -y jq บน Ubuntu และ Debian
ผลของ exit status ต่อการเรียกใช้เครื่องมือที่กำลังทำงานอยู่
มีผลลัพธ์ที่เป็นไปได้ 3 รูปแบบ:
- Exit 0 หมายความว่า hook ของคุณไม่มีข้อโต้แย้ง สำหรับ
PreToolUseนี่ไม่ใช่การอนุมัติ และขั้นตอนการตรวจสอบสิทธิ์ตามปกติจะยังคงทำงานต่อไป สำหรับUserPromptSubmitและSessionStartข้อมูลใน stdout จะถูกเพิ่มเข้าไปในบริบทของโมเดล - Exit 2 จะระงับการดำเนินการในเหตุการณ์ที่สามารถระงับได้ ซึ่งรวมถึง
PreToolUseโดย stderr จะกลายเป็นเหตุผลที่แสดงให้โมเดลเห็น สำหรับเหตุการณ์ที่ไม่สามารถระงับได้ เช่นPostToolUseการระงับจะถูกเพิกเฉย แม้ว่า stderr จะยังคงถูกส่งไปยังโมเดลเพื่อเป็นข้อมูลป้อนกลับก็ตาม - Exit code อื่นๆ ถือเป็นข้อผิดพลาดที่ไม่ขัดขวางการทำงาน การดำเนินการจะดำเนินต่อไป โดยบันทึกการสนทนาจะแสดงประกาศข้อผิดพลาดของ hook พร้อมบรรทัดแรกของ stderr ต่อจากข้อความ
Failed with non-blocking status code:
หากต้องการทำมากกว่าการระงับหรือนิ่งเฉย ให้ใช้ exit 0 แล้วพิมพ์ออบเจกต์ JSON ออกทาง stdout แทน โดย hook PreToolUse จะตัดสินใจด้วย permissionDecision:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Database drops go through a migration, not through the agent."
}
}"allow" จะข้ามการถามตอบแบบโต้ตอบ "deny" จะยกเลิกการเรียกใช้และส่งเหตุผลไปยังโมเดล และ "ask" จะแสดงการถามตอบตามปกติ ให้เลือกเพียงรูปแบบเดียวต่อหนึ่ง hook การใช้ exit 2 ร่วมกับการตัดสินใจแบบ JSON บน stdout จะทำให้ได้ผลลัพธ์ที่คุณต้องตรวจสอบย้อนหลัง
เมื่อมีหลาย hook ตรงกับเหตุการณ์เดียวกัน พวกมันจะทำงานแบบขนานและทุกตัวจะทำงานจนเสร็จสิ้น การที่ hook หนึ่งส่ง deny จะไม่หยุดการทำงานของ hook อื่นๆ ดังนั้น hook สำหรับบันทึก log จะยังคงเขียนบรรทัดของมันในขณะที่ hook สำหรับป้องกัน (guardrail) ปฏิเสธการเรียกใช้นั้น จากนั้น Claude Code จะรวมคำตอบเข้าด้วยกันและเลือกผลลัพธ์ที่มีข้อจำกัดสูงสุด โดยเรียงลำดับจาก ปฏิเสธ (deny), เลื่อน (defer), ถาม (ask), และอนุญาต (allow)
ตัวอย่างที่ 1: การบล็อกคำสั่งที่เป็นอันตรายก่อนการทำงาน
บันทึกไฟล์นี้เป็น .claude/hooks/block-destructive.sh ในโปรเจกต์ของคุณ:
#!/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ทำให้ไฟล์สามารถรันได้ จากนั้นลงทะเบียนไว้ที่ PreToolUse ใน .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"
}
]
}
]
}
}ทดสอบสคริปต์ด้วยตนเองก่อนที่จะใช้งานจริง เพราะ hook ที่ทำงานผิดพลาดเมื่อได้รับ input จะทำให้ระบบเปิดช่องโหว่ (fail open):
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /var/lib/postgresql"}}' \
| .claude/hooks/block-destructive.sh
echo $?คุณควรเห็นบรรทัด Blocked by policy: ปรากฏบน stderr และได้รับ exit code เป็น 2 ลองทดสอบด้วยคำสั่งที่ไม่เป็นอันตราย เช่น ls -la ซึ่งคุณไม่ควรเห็นผลลัพธ์ใดๆ และควรได้รับ exit code เป็น 0 ในระหว่างเซสชัน การเรียกใช้งานที่ถูกปฏิเสธจะปรากฏในบันทึกพร้อมข้อความเหตุผลของคุณ ซึ่งโมเดลจะอ่านข้อความนั้นและปรับเปลี่ยนพฤติกรรม
คุณสมบัติประการหนึ่งที่ทำให้สิ่งนี้คุ้มค่าคือ hook แบบ PreToolUse จะทำงานก่อนการตรวจสอบ permission-mode ในทุกโหมด ดังนั้นการปฏิเสธคำสั่งจึงมีผลแม้ในโหมด bypassPermissions นี่คือสิ่งที่ทำให้ hook มีประโยชน์เมื่อใช้งานร่วมกับ Claude Code auto mode และการตั้งค่าสิทธิ์ ซึ่งแม้จะมีการลดระดับการแจ้งเตือนลง แต่ hook ก็ยังคงทำงานอยู่
โปรดเข้าใจธรรมชาติของเครื่องมือนี้ตามความเป็นจริง การจับคู่รูปแบบ (pattern matching) บนสตริงคำสั่งเป็นเพียงมาตรการป้องกันไม่ให้เอเจนต์ทำงานโดยประมาท แต่ไม่ใช่ขอบเขตป้องกันเอเจนต์ที่พยายามหลบเลี่ยง เนื่องจากคำสั่งเดียวกันสามารถเขียนในรูปแบบที่ grep ของคุณมองไม่เห็นได้ กฎที่เข้มงวดควรอยู่ในระบบจัดการสิทธิ์และในบัญชีผู้ใช้ที่กระบวนการนั้นทำงานอยู่เท่านั้น
ตัวอย่างที่ 2: การจัดรูปแบบและตรวจสอบ lint หลังการแก้ไขทุกครั้ง
PostToolUse ที่มีตัวจับคู่ Edit|Write จะทำงานหลังจากเครื่องมือแก้ไขไฟล์ใดๆ ทำงานเสร็จสิ้น ให้บันทึกสิ่งนี้เป็น .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
}
]
}
]
}
}ลองให้ Claude เพิ่มฟังก์ชันที่มีการย่อหน้าผิดพลาดลงในไฟล์ Python จากนั้นเปิดไฟล์นั้นขึ้นมา คุณจะพบว่าไฟล์ถูกจัดรูปแบบเรียบร้อยแล้ว นั่นคือการตรวจสอบว่า hook ทำงานแล้ว เพราะหาก hook ทำงานสำเร็จ จะไม่มีการแสดงผลใดๆ ในบทสนทนา
การใช้ exit 2 ในที่นี้ไม่ได้เป็นการย้อนกลับการเปลี่ยนแปลงใดๆ PostToolUse จะทำงานหลังจากที่เครื่องมือได้ดำเนินการไปแล้ว ดังนั้นการแก้ไขจึงถูกบันทึกลงในดิสก์ไม่ว่ากรณีใดก็ตาม สิ่งที่คุณได้รับจากการใช้ exit 2 คือการที่ผลลัพธ์ของ ruff check จะถูกส่งกลับไปยังโมเดลในฐานะข้อมูลป้อนกลับ ทำให้โมเดลแก้ไขข้อผิดพลาดที่เพิ่งเกิดขึ้นแทนที่จะดำเนินการต่อไป นี่คือความแตกต่างระหว่างความล้มเหลวในการ lint ที่คุณพบในขั้นตอน commit กับความล้มเหลวที่เอเจนต์ซ่อมแซมได้ในรอบการทำงานเดียวกัน
มีข้อจำกัดของตัวจับคู่สองประการที่สำคัญในที่นี้ Edit|Write จะไม่เห็นไฟล์ที่ถูกเปลี่ยนโดยคำสั่ง shell และ Claude ก็เขียนไฟล์ผ่าน Bash บ่อยพอที่จะทำให้ช่องว่างนี้เป็นปัญหาจริง สำหรับการครอบคลุมในทุกการเรียกใช้ ให้จับคู่ Bash เพิ่มเติมและให้สคริปต์แสดงรายการไฟล์ที่เปลี่ยนด้วย git status --porcelain สำหรับการครอบคลุมหนึ่งครั้งต่อรอบการทำงาน ให้ใส่การสแกนไว้ใน hook Stop แทน
ตัวอย่างที่ 3: บันทึกการเรียกใช้เครื่องมือทุกครั้งเพื่อการตรวจสอบ
ตัวจับคู่ (matcher) ที่ว่างเปล่าใน PostToolUse จะทำงานกับเครื่องมือทุกตัว การส่งบันทึกไปยัง system journal แทนการเก็บไว้ในไฟล์ใน home directory จะช่วยป้องกันไม่ให้ agent เข้าถึงบันทึกดังกล่าวผ่าน shell ของตัวเองได้:
{
"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"
}
]
}
]
}
}อ่านบันทึกกลับมาด้วย journalctl -t claude-code -o cat | tail -n 5 คุณควรจะเห็นข้อมูล JSON หนึ่งบรรทัดต่อการเรียกใช้เครื่องมือหนึ่งครั้ง โดยเรียงลำดับให้รายการล่าสุดอยู่ท้ายสุด หากไม่มีข้อมูลปรากฏ แสดงว่า hook ไม่ทำงาน ซึ่งส่วนการแก้ไขปัญหาด้านล่างจะครอบคลุมถึงกรณีนี้
ให้เพิ่มบล็อกเดียวกันนี้ภายใต้ PostToolUseFailure เพื่อบันทึกการเรียกใช้ที่ล้มเหลว เนื่องจาก PostToolUse จะทำงานเฉพาะเมื่อสำเร็จเท่านั้น แต่คำสั่งที่ล้มเหลวมักเป็นสิ่งที่น่าสนใจกว่า เหตุผลที่ใช้ logger แทนการเขียนต่อท้ายไฟล์ใน home directory ของคุณคือเรื่องความเป็นเจ้าของ (ownership): hook จะทำงานในฐานะผู้ใช้เดียวกับ shell ของ agent ดังนั้นไฟล์ใดก็ตามที่ผู้ใช้นั้นสามารถเขียนต่อท้ายได้ ผู้ใช้นั้นก็สามารถลบเนื้อหาทิ้งได้เช่นกัน ในขณะที่ journal จะถูกเขียนโดย systemd-journald ภายใต้บัญชีของระบบเอง
ระยะเวลาที่ hook สามารถทำงานได้
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
}
]โดยปกติแล้ว command hook จะได้รับเวลาทำงานเริ่มต้นที่ 600 วินาที ซึ่งเท่ากับ 10 นาที อย่างไรก็ตาม บางเหตุการณ์จะจำกัดเวลาลงอย่างมาก โดย hook ประเภท SessionEnd จะต้องใช้ทรัพยากรเวลาร่วมกันทั้งหมดไม่เกิน 1.5 วินาที ดังนั้นการล้างข้อมูลเมื่อสิ้นสุดเซสชันจึงต้องดำเนินการอย่างรวดเร็ว แม้ว่าการตั้งค่า timeout ที่นานขึ้นให้กับ hook จะช่วยขยายงบประมาณเวลารวมดังกล่าวให้เพิ่มขึ้นได้สูงสุดถึง 60 วินาทีก็ตาม
หาก hook ทำงานเกินเวลาที่กำหนด ระบบจะยกเลิกการทำงานนั้นและไม่มีการตัดสินใจใดๆ เกิดขึ้น สำหรับ guardrail ประเภท PreToolUse หมายความว่า hook จะไม่ขัดขวางการทำงาน: การเรียกใช้เครื่องมือ (tool call) จะดำเนินต่อไปตามขั้นตอนการตรวจสอบสิทธิ์ปกติ ด้วยเหตุนี้จึงควรเขียนสคริปต์ guardrail ให้มีขนาดเล็ก สำหรับงานที่ใช้เวลานานและไม่มีใครรอผลลัพธ์ เช่น การส่ง log ไปยังปลายทาง ให้ตั้งค่า "async": true เพื่อให้ hook ทำงานในเบื้องหลังโดยไม่ขัดจังหวะการเรียกใช้เครื่องมือ
Hooks, rules files, skills และ MCP servers
มีสี่สิ่งที่มักสับสนกันเพราะทั้งหมดล้วนเปลี่ยนพฤติกรรมของ agent แต่มีเพียงสิ่งเดียวเท่านั้นที่ไม่ใช่แค่คำแนะนำ
Rules file (CLAUDE.md หรือไฟล์ภายใต้ .claude/rules/) คือข้อความที่ถูกโหลดเข้าสู่บริบทของโมเดล มันช่วยกำหนดพฤติกรรมแต่ไม่ได้บังคับให้เกิดผลใดๆ เมื่อเทียบกับบทสนทนาที่ยาวเหยียด, diff ขนาดใหญ่ และคำขอใหม่จากผู้ใช้ ข้อความเพียงบรรทัดเดียวในไฟล์อาจถูกละเลยได้ นี่คือกลไกปกติที่อยู่เบื้องหลัง การที่ agent เพิกเฉยต่อคำสั่งที่คุณเขียนไว้
Skill คือโฟลเดอร์ที่เก็บคำสั่งและสคริปต์ซึ่งโมเดลจะโหลดขึ้นมาเมื่อพิจารณาว่า skill นั้นเกี่ยวข้อง การตัดสินใจดังกล่าวคือหัวใจสำคัญของ skill และในขณะเดียวกันก็เป็นข้อจำกัด เพราะโมเดลยังคงเป็นผู้ตัดสินใจเอง คุณสามารถเห็นทั้งสองด้านได้ใน skill อย่าง Ponytail ซึ่งผลักดันให้ agent เลือกการเปลี่ยนแปลงที่เล็กที่สุดที่ใช้งานได้ เพราะมันช่วยกำหนดแนวทางในการจัดการงานทั้งหมดในแบบที่ hook ทำไม่ได้ และจะทำงานก็ต่อเมื่อโมเดลเลือกที่จะโหลดมันเท่านั้น
MCP (model context protocol) server มอบเครื่องมือใหม่ให้โมเดลเรียกใช้ มันช่วยขยายขอบเขตสิ่งที่ agent สามารถเข้าถึงได้ แต่มันไม่ได้บังคับให้ agent ต้องเรียกใช้สิ่งใด และมันเป็นกระบวนการแยกต่างหากที่คุณต้องดูแล ซึ่งเป็นงานเฉพาะทาง: ดูที่ การรัน MCP servers บน VPS
Hook เป็นสิ่งเดียวในสี่อย่างนี้ที่ทำงานโดยที่โมเดลไม่ต้องเป็นผู้เลือก ให้ใช้ rules file สำหรับความต้องการทั่วไป และใช้ skill สำหรับขั้นตอนที่โมเดลควรปฏิบัติตามเมื่อถึงเวลาที่เหมาะสม ส่วน hook ให้ใช้สำหรับขั้นตอนที่ต้องเกิดขึ้นทุกครั้ง หรือสิ่งที่ต้องไม่เกิดขึ้นเด็ดขาด การเปรียบเทียบเชิงลึก รวมถึงกรณีที่ skill มีประสิทธิภาพเหนือกว่า rules file อยู่ใน การเปรียบเทียบระหว่าง skills, MCP และ rules files
Plugin เป็นเพียงรูปแบบการบรรจุภัณฑ์ไม่ใช่กลไกที่ห้า มันทำหน้าที่รวม hook เข้ากับ skill ให้เป็นหน่วยเดียวที่ติดตั้งได้ ซึ่งเป็นวิธีที่ทีมงานใช้ในการส่งมอบ guardrail เดียวกันไปยังทุกเครื่อง: ดูที่ วิธีการทำงานของ Claude Code plugins
การตัดสินใจด้านความปลอดภัยบน VPS แบบใช้งานร่วมกัน
Hook คือโค้ดที่ agent เรียกใช้งาน โดยจะรันในฐานะผู้ใช้ที่เริ่มการทำงานของ Claude Code ซึ่งจะได้รับสิทธิ์และสภาพแวดล้อมของผู้ใช้นั้นมาทั้งหมด บนแล็ปท็อปนี่เป็นเพียงประเด็นเรื่องขั้นตอนการทำงาน แต่บน VPS ที่ agent รันอยู่โดยไม่มีผู้ดูแล นี่คือประเด็นด้านความปลอดภัยที่มีองค์ประกอบสำคัญ 4 ประการ
Hook ใน repository คือโค้ดที่คุณไม่ได้เขียนเอง .claude/settings.json ถูก commit ไว้ ดังนั้นการ clone repository และเริ่ม session ภายในนั้นอาจเป็นการลงทะเบียน hook ที่มาพร้อมกับ repository ได้ Claude Code จะกั้น hook ของโปรเจกต์ไว้หลังกล่องโต้ตอบความเชื่อมั่นของ workspace สำหรับโฟลเดอร์นั้น ซึ่งหมายความว่าการยอมรับความเชื่อมั่นคือช่วงเวลาที่คุณตัดสินใจว่าจะรันโค้ดเหล่านั้น โปรดอ่านบล็อก hooks ก่อน
Hook สามารถมองเห็น input ของเครื่องมือทั้งหมด Audit hook ที่บันทึก tool_input จะเขียนอาร์กิวเมนต์ทุกตัวของทุกคำสั่งลงในไฟล์ รวมถึง token ใดๆ ที่อาจปรากฏอยู่บนบรรทัดคำสั่ง บันทึกนั้นจึงต้องการการป้องกันในระดับเดียวกับ secret ซึ่งเป็นส่วนหนึ่งของปัญหาที่กว้างกว่าเรื่อง การเก็บ secret ให้พ้นจากมือของ AI agent
Hook สามารถเขียนข้อมูลลงในบริบทของโมเดลได้ สิ่งใดก็ตามที่ hook ประเภท SessionStart หรือ UserPromptSubmit พิมพ์ออกมายัง stdout จะถูกเพิ่มเข้าไปในการสนทนา Hook ที่ดึงข้อความจากภายนอก เช่น จากระบบติดตามปัญหา (issue tracker) หรือไฟล์ log คือการส่งข้อความที่ไม่น่าเชื่อถือให้กับโมเดลเสมือนว่าคุณเป็นคนพิมพ์เอง Hook ที่ส่งต่อบันทึกจาก Claude Code session อื่นบน VPS เดียวกัน ก็ทำในลักษณะเดียวกัน และผลลัพธ์จาก agent หนึ่งก็ไม่ได้มีความน่าเชื่อถือไปมากกว่าข้อมูลจากระบบติดตามปัญหา ให้ถือว่า stdout นั้นเป็น input มากกว่าจะเป็น output
สิทธิ์การเข้าถึงคือการควบคุมที่แท้จริง ให้รัน agent ในฐานะผู้ใช้เฉพาะที่ไม่มีสิทธิ์สูง (unprivileged user) โดยกำหนดเฉพาะกฎ sudo ที่จำเป็นเท่านั้น การตั้งค่า PreToolUse deny เป็นสิ่งที่ควรมี แต่โดยการออกแบบแล้วมันเป็นเพียงความพยายามที่ดีที่สุด (best effort) เอกสารอ้างอิงระบุไว้เช่นเดียวกันสำหรับตัวกรอง if และแนะนำให้คุณใช้ระบบ permission ของระบบปฏิบัติการเมื่อต้องการการปฏิเสธการเข้าถึงอย่างเด็ดขาด กฎ permission และบัญชีผู้ใช้ที่กระบวนการนั้นรันอยู่คือส่วนที่ยังคงมีความมั่นคงภายใต้สภาวะกดดัน
มีคุณสมบัติหนึ่งที่คงอยู่เสมอในทุกการกำหนดค่า Hook ประเภท PreToolUse จะทำงานก่อนการตรวจสอบ permission-mode ในทุกโหมด ดังนั้น hook ที่ส่งค่ากลับเป็น deny จะบล็อกเครื่องมือนั้นแม้จะอยู่ในโหมด bypassPermissions ก็ตาม Hook สามารถจำกัดสิ่งที่กฎ permission อนุญาตได้ แต่ไม่สามารถขยายสิทธิ์ให้กว้างขึ้นได้
ทำไม hook ของฉันถึงไม่ทำงาน?
ให้ตรวจสอบตามลำดับขั้นตอนดังต่อไปนี้ แต่ละขั้นตอนจะระบุถึงอาการที่คุณอาจพบ:
- รันคำสั่ง
/hooksแล้วตรวจสอบว่า hook ปรากฏอยู่ภายใต้เหตุการณ์ที่คุณคาดหวังหรือไม่ หาก hook ไม่แสดงในเมนู มักหมายความว่าไฟล์การตั้งค่ามีข้อผิดพลาดทางไวยากรณ์ JSON เนื่องจากไม่อนุญาตให้มีเครื่องหมายจุลภาคปิดท้าย (trailing comma) หรือความคิดเห็น (comment) หรือไฟล์ไม่ได้อยู่ในตำแหน่งใดตำแหน่งหนึ่งจากทั้ง 6 แห่งที่ระบุไว้ข้างต้น - เปรียบเทียบตัวจับคู่ (matcher) กับชื่อเครื่องมือให้ตรงกันทุกประการ ตัวจับคู่มีความสำคัญที่ตัวพิมพ์เล็กและตัวพิมพ์ใหญ่ (case sensitive) ดังนั้น
"bash"จะไม่ตรงกับเครื่องมือBash - รันสคริปต์ด้วยตนเองโดยใช้ข้อมูลตัวอย่างตามตัวอย่างที่ 1 ด้านบน หากได้รับ exit code ที่ไม่คาดคิด แสดงว่าเป็นบั๊กในสคริปต์ของคุณ และ Claude Code จะรายงานว่าเป็นข้อผิดพลาดของ hook แทนที่จะเป็นการตัดสินใจ
- การแจ้งเตือนที่ระบุว่า
jq: command not foundหมายความว่าjqไม่มีอยู่บนเครื่องนั้น หากพบcommand not foundสำหรับสคริปต์ของคุณเอง หมายความว่า path ไม่สามารถระบุตำแหน่งได้ ให้ใช้${CLAUDE_PROJECT_DIR}หรือใช้ absolute path หากสคริปต์ไม่ทำงานเลย เป็นไปได้ว่าสคริปต์นั้นไม่ได้ถูกตั้งค่าให้เป็น executable - hook พิมพ์ JSON ที่ถูกต้องแต่ไม่มีอะไรเกิดขึ้น hook รูปแบบ shell จะรันผ่าน
sh -cหากโปรไฟล์ shell ของคุณมีการพิมพ์ข้อความต้อนรับ (banner) ข้อความนั้นจะถูกแทรกไว้หน้า JSON ของคุณ ทำให้ stdout ไม่ได้เริ่มต้นด้วย{อีกต่อไป Claude Code จึงอ่านข้อมูลทั้งหมดเป็นข้อความธรรมดาและเพิกเฉยต่อการตัดสินใจนั้น หาก exit code เป็น 0 จะไม่มีการรายงานใดๆ ยกเว้นใน debug log ให้ครอบคำสั่งechoใดๆ ในโปรไฟล์ของคุณเพื่อให้ทำงานเฉพาะใน interactive shell เท่านั้น - หากยังติดปัญหา: ให้เริ่มเซสชันด้วย
claude --debug-file /tmp/claude.logและรันtail -f /tmp/claude.logในเทอร์มินัลที่สอง debug log จะบันทึกว่า hook ใดบ้างที่ตรงกัน, แต่ละ hook ส่งคืน exit code ใด, และบันทึกทุกสิ่งที่ hook เหล่านั้นเขียนไปยัง stdout และ stderr
FAQ
ความแตกต่างระหว่าง hook ของ Claude Code กับคำสั่งใน CLAUDE.md คืออะไร?
คำสั่งใน CLAUDE.md เป็นข้อความที่อยู่ในบริบทของโมเดล จึงต้องแย่งพื้นที่ความสนใจกับบทสนทนาและคำขอปัจจุบัน ซึ่งโมเดลสามารถนำไปเปรียบเทียบน้ำหนักกันได้ ส่วน hook คือคำสั่ง shell ที่ Claude Code จะเรียกทำงาน ณ จุดที่กำหนดไว้ในวงจรการทำงานของมัน ดังนั้นมันจะทำงานทุกครั้งที่เกิดเหตุการณ์นั้นโดยไม่ขึ้นอยู่กับการตัดสินใจของโมเดล ให้ใช้คำสั่งในไฟล์สำหรับความต้องการทั่วไป และใช้ hook สำหรับขั้นตอนที่ต้องเกิดขึ้นเสมอหรือการกระทำที่ต้องห้ามเกิดขึ้นเด็ดขาด
ฉันจะหยุดไม่ให้ Claude Code เรียกใช้คำสั่ง shell บางอย่างได้อย่างไร?
ให้ลงทะเบียน hook ประเภท PreToolUse พร้อมกับตัวจับคู่ Bash ที่อ่านคำสั่งจาก .tool_input.command จากนั้นเขียนเหตุผลลงใน stderr แล้วสั่ง exit 2 ระบบ Claude Code จะยกเลิกการเรียกนั้นและแสดงเหตุผลของคุณให้โมเดลเห็น ซึ่งกระบวนการนี้เกิดขึ้นก่อนการตรวจสอบในโหมด permission-mode ดังนั้นการปฏิเสธคำสั่งจะมีผลแม้ในโหมด bypassPermissions การจับคู่รูปแบบ (pattern matching) บนสตริงคำสั่งถือเป็นเพียงมาตรการป้องกันเบื้องต้น ไม่ใช่ขอบเขตความปลอดภัยที่สมบูรณ์ เนื่องจากคำสั่งเดียวกันสามารถเขียนในรูปแบบที่ตัวจับคู่มองไม่เห็นได้ จึงควรเสริมด้วยกฎการอนุญาต (permission rules) และการใช้งานผ่านบัญชีผู้ใช้ที่ไม่มีสิทธิ์ระดับสูง
hook ของฉันพิมพ์ JSON ที่ถูกต้องแล้ว แต่ไม่มีอะไรเกิดขึ้น เพราะเหตุใด?
สาเหตุที่พบบ่อยที่สุดคือการตั้งค่า shell profile ของคุณ hook ที่ไม่มีฟิลด์ args จะทำงานผ่าน sh -c ซึ่ง shell profile บางตัวมีการพิมพ์ข้อความต้อนรับ (banner) ทุกครั้งที่เปิด shell ทำให้ข้อความนั้นไปปรากฏบน stdout ก่อนหน้า JSON ของคุณ เนื่องจากผลลัพธ์ไม่ได้ขึ้นต้นด้วย { อีกต่อไป Claude Code จึงมองว่าทั้งหมดเป็นข้อความธรรมดาและเพิกเฉยต่อการตัดสินใจนั้น และหาก exit 0 ก็จะไม่มีการรายงานใดๆ ใน transcript เลย ให้ป้องกันการพิมพ์ข้อความใน echo ของ profile ด้วยการตรวจสอบว่าเป็น interactive shell หรือไม่ จากนั้นยืนยันการแก้ไขโดยการอ่าน debug log จาก claude --debug-file /tmp/claude.log
การรัน hook ของ Claude Code บนเซิร์ฟเวอร์ที่ใช้งานร่วมกันมีความปลอดภัยหรือไม่?
hook จะทำงานในฐานะผู้ใช้ที่เริ่ม Claude Code โดยใช้สิทธิ์ไฟล์ของผู้ใช้นั้น ดังนั้น hook สามารถทำทุกอย่างที่บัญชีนั้นทำได้ แนวทางปฏิบัติ 2 ประการที่ช่วยลดความเสี่ยงส่วนใหญ่คือ: รัน agent ด้วยบัญชีผู้ใช้เฉพาะที่ไม่มีสิทธิ์ระดับสูงและมีนโยบาย sudo ที่จำกัด และอ่านบล็อก hooks ของ repository ใดๆ ก่อนที่คุณจะยอมรับกล่องโต้ตอบความเชื่อถือของ workspace เนื่องจาก project hook จะถูกส่งมาภายใน .claude/settings.json ให้ตั้งค่า "disableAllHooks": true ในไฟล์การตั้งค่าของคุณหากคุณไม่ต้องการให้ hook ใดๆ ทำงานเลย