Claude Code hooks: เปลี่ยนกฎของเอเจนต์ให้เป็นการรับประกัน
Hooks ของ Claude Code คือคำสั่ง shell ที่ตัว Claude Code รันเองในเหตุการณ์ที่กำหนด ได้แก่ ก่อนและหลังการเรียกเครื่องมือ เมื่อส่ง prompt เมื่อเริ่มเซสชัน และก่อนที่เอเจนต์จะหยุด ตั้งค่าใน settings.json และ exit code 2 จะบล็อกการกระทำและส่งข้อความผิดพลาดกลับไปให้เอเจนต์ ทำให้ hook เปลี่ยนกฎจาก CLAUDE.md ให้เป็นการรับประกันที่โมเดลลืมไม่ได้
Hooks ของ Claude Code คืออะไร
Hook คือคำสั่งที่รันโดยโปรแกรม Claude Code เองในจังหวะที่กำหนดของการทำงานของเอเจนต์ ไม่ใช่รันโดยโมเดล เอเจนต์กำลังจะรัน rm -rf hook ทำงานและตัดสินว่าจะปล่อยคำสั่งผ่านหรือไม่ เอเจนต์แก้ไขไฟล์ hook เรียก formatter เอเจนต์อยากจบงาน hook รันเทสต์และไม่ให้หยุดจนกว่าเทสต์จะผ่าน
ความต่างหลักจากคำแนะนำใน CLAUDE.md คือ คำแนะนำเป็นข้อความที่โมเดลพิจารณาด้วยความน่าจะเป็นบางระดับ ส่วน hook เป็นโค้ดที่กำหนดผลแน่นอน มันทำงานเสมอไม่ว่าบริบทจะยาวแค่ไหน เอเจนต์จะตัดสินใจอะไร หรือลืมกฎไปในนาทีที่ 100 ของเซสชันหรือไม่ นี่คือวิธีหลักในการเปลี่ยนคำขอให้เป็นการรับประกัน
ทำไมกฎใน CLAUDE.md อย่างเดียวไม่พอ
ภาพที่พบบ่อยคือ เอเจนต์เขียนฟีเจอร์เสร็จ เทสต์ยูนิตผ่านหมด ในแชตตอบมั่นใจว่า "เสร็จแล้ว" แต่ไม่มีใครรันเทสต์แบบ end-to-end ลินเตอร์ไม่ได้รัน และ migration ล้มกลางคัน ทั้งที่ใน CLAUDE.md เขียนว่า "ก่อนจบงานให้รันเทสต์" โมเดลไม่ได้หลอกลวงโดยเจตนา มันแค่ประเมินตัวเองและการประเมินตัวเองนั้นไม่น่าเชื่อถือ
นี่คือหลักการของฮาร์เนส: สิ่งที่ตรวจด้วยกลไกได้ ให้กลไกเป็นคนตรวจ ไม่ใช่โมเดล กฎ "รันลินเตอร์" กลายเป็น hook ที่รันลินเตอร์ กฎ "อย่าแตะคอนฟิกโปรดักชัน" กลายเป็น hook ที่บล็อกการเขียนไฟล์นั้น ข้อความคำสั่งยังคงอยู่ แต่ทำหน้าที่เพียงอธิบายให้เอเจนต์ทราบว่าทำไมมันถูกหยุด
เหตุการณ์: hook ทำงานเมื่อไร
แต่ละ hook ผูกกับเหตุการณ์ในวงจรชีวิตของเซสชัน เหตุการณ์หลักมีดังนี้:
| เหตุการณ์ | ทำงานเมื่อ | การใช้งานทั่วไป |
|---|---|---|
PreToolUse | ก่อนเรียกเครื่องมือ (Bash, Edit, Write ฯลฯ) | บล็อกคำสั่งอันตราย ป้องกันไฟล์ |
PostToolUse | หลังเรียกเครื่องมือสำเร็จ | formatter ลินเตอร์ เทสต์เร็วๆ หลังแก้ไข |
UserPromptSubmit | เมื่อผู้ใช้ส่ง prompt ก่อนประมวลผล | เพิ่มบริบท หรือตัด prompt ที่มีความลับ |
Stop | เมื่อเอเจนต์กำลังจะจบคำตอบ | ไม่ให้หยุดจนกว่าการตรวจจะผ่าน |
SubagentStop | เมื่อ subagent จบการทำงาน | ตรวจผลลัพธ์ของ subagent |
SessionStart | เมื่อเริ่มหรือกลับมาทำเซสชันต่อ | โหลดสถานะ: git status แผนงาน บันทึกความคืบหน้า |
SessionEnd | เมื่อเซสชันจบ | ล้างข้อมูล บันทึก log |
Notification | เมื่อ Claude Code ส่งการแจ้งเตือน (เช่น รอการอนุญาต) | เสียงหรือข้อความไปยังโทรศัพท์ |
PreCompact | ก่อนบีบอัดบริบท | บันทึกสิ่งสำคัญก่อนการบีบอัด |
เหตุการณ์ที่มีประโยชน์ที่สุดต่อความน่าเชื่อถือคือ PreToolUse, PostToolUse และ Stop เพราะปิดจุดอ่อนสามจุดของเอเจนต์ ได้แก่ การกระทำอันตราย การแก้ไขที่ยังไม่ได้ตรวจ และการรายงานว่า "เสร็จแล้ว" เร็วเกินไป
ตั้งค่า hook ที่ไหน: settings.json
Hook ถูกอธิบายในไฟล์ตั้งค่าของ Claude Code:
~/.claude/settings.jsonตั้งค่าส่วนตัวของคุณ ใช้ได้กับทุกโปรเจกต์.claude/settings.jsonใน repository ใช้ร่วมกันในทีม commit ลง git ได้.claude/settings.local.jsonตั้งค่าเฉพาะเครื่องของโปรเจกต์ ไม่เข้า git
โครงสร้างคือ เหตุการณ์ → รายการกลุ่มที่มี matcher → รายการคำสั่ง
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint.sh",
"timeout": 60
}
]
}
]
}
}
matcher คือชื่อเครื่องมือหรือ regular expression ที่ตรงกับชื่อนั้น เช่น Bash, Edit|Write, .* ใช้กับเหตุการณ์ที่เกี่ยวกับเครื่องมือ สำหรับ Stop หรือ UserPromptSubmit ไม่ต้องระบุก็ได้ ตัวแปร CLAUDE_PROJECT_DIR ชี้ไปยังรากของโปรเจกต์ จึงสะดวกที่จะเก็บสคริปต์ไว้ใน .claude/hooks/ และเรียกผ่านตัวแปรนี้ ดู hook ที่ตั้งไว้ได้ด้วยคำสั่ง /hooks ภายใน Claude Code
Hook รับข้อมูลและตอบกลับอย่างไร
อินพุต: JSON ทาง stdin
Claude Code ส่งข้อมูล JSON ที่อธิบายเหตุการณ์ให้ hook ได้แก่ รหัสเซสชัน ไดเรกทอรีทำงาน ชื่อเหตุการณ์ และสำหรับเครื่องมือจะมี tool_name กับ tool_input ใน Bash คำสั่งอยู่ใน tool_input.command ส่วน Edit และ Write พาธไฟล์อยู่ใน tool_input.file_path อ่านได้สะดวกด้วย jq
{
"session_id": "abc123",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "rm -rf build/" },
"cwd": "/home/dev/project"
}
เอาต์พุต: รหัสผลลัพธ์
| รหัส | สิ่งที่เกิดขึ้น |
|---|---|
0 | สำเร็จ งานดำเนินต่อ สำหรับ UserPromptSubmit และ SessionStart stdout จะถูกเพิ่มเข้าไปในบริบทของเอเจนต์ |
2 | ข้อผิดพลาดที่บล็อก stderr ถูกส่งให้เอเจนต์ และการกระทำถูกหยุด (สำหรับ PreToolUse การเรียกเครื่องมือถูกยกเลิก สำหรับ Stop เอเจนต์ทำงานต่อ) |
| อื่นๆ | ข้อผิดพลาดที่ไม่บล็อก stderr แสดงต่อผู้ใช้ และงานดำเนินต่อ |
Exit code 2 คือกลไกสำคัญ เอเจนต์ได้รับข้อความจาก stderr เป็นข้อมูลป้อนกลับและแก้ไขตัวเองได้ เช่น เขียนคำสั่งใหม่ แก้เทสต์ หรือไม่แตะไฟล์นั้น ดังนั้นให้เขียนลง stderr ไม่ใช่แค่คำว่า "error" แต่เป็นเหตุผลที่เข้าใจได้และบอกว่าควรทำอะไรแทน
นอกจากรหัสผลลัพธ์ hook ยังส่ง JSON ทาง stdout ที่มีการตัดสินใจ เช่น "decision": "block" และ "reason" ได้ ซึ่งมีประโยชน์สำหรับการปรับแต่งละเอียด แต่สำหรับงานส่วนใหญ่ exit code 2 กับข้อความใน stderr ก็เพียงพอ
ตัวอย่างที่ 1: ลินเตอร์และเทสต์หลังทุกการแก้ไข
Hook บน PostToolUse ที่มี matcher Edit|Write ทำงานหลังไฟล์ถูกแก้ไข เครื่องมือรันเสร็จแล้ว ยกเลิกการแก้ไขไม่ได้ แต่ exit code 2 จะส่งผลลัพธ์ของลินเตอร์กลับให้เอเจนต์ และมันจะแก้ข้อผิดพลาดทันที ไม่ต้องรอครึ่งชั่วโมง
#!/usr/bin/env bash
# .claude/hooks/lint.sh
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
*.py)
if ! out=$(ruff check "$file" 2>&1); then
echo "ลินเตอร์พบข้อผิดพลาดใน $file:" >&2
echo "$out" >&2
exit 2
fi ;;
*.php)
if ! out=$(php -l "$file" 2>&1); then
echo "$out" >&2
exit 2
fi ;;
esac
exit 0
อย่าใส่การรันเทสต์ทั้งชุดที่ใช้เวลาสิบนาทีไว้ตรงนี้ เพราะ hook จะทำงานทุกครั้งที่แก้ไข หลังการแก้ไขให้ใช้การตรวจที่เร็ว (ไวยากรณ์ ลินเตอร์ เทสต์ของโมดูลเดียว) และการตรวจที่หนักให้ไปไว้ใน hook Stop
ตัวอย่างที่ 2: บล็อกคำสั่งอันตราย
Hook บน PreToolUse ที่มี matcher Bash เห็นคำสั่งก่อนรัน exit code 2 ยกเลิกการเรียกและเอเจนต์ได้รับคำอธิบาย
#!/usr/bin/env bash
# .claude/hooks/guard.sh
cmd=$(jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq 'rm -rf /|git push (-f|--force)|DROP (TABLE|DATABASE)'; then
echo "คำสั่งถูกบล็อกโดยนโยบายของโปรเจกต์: $cmd. การดำเนินการอันตรายให้มนุษย์เป็นผู้ทำ" >&2
exit 2
fi
exit 0
ไฟล์ก็ป้องกันได้ในลักษณะเดียวกัน hook บน Edit|Write ตรวจ file_path และบล็อกการเขียนไฟล์ .env คอนฟิกโปรดักชัน โค้ดที่สร้างอัตโนมัติ หรือไฟล์ migration ที่ถูกนำไปใช้แล้ว แต่ควรระวังว่าการจับคู่ด้วย regular expression หลบเลี่ยงได้ เอเจนต์อาจเขียนคำสั่งต่างออกไป hook เป็นการป้องกันที่แข็งแรง แต่ไม่ใช่การแทนที่สิทธิ์การเข้าถึงและ sandbox
ตัวอย่างที่ 3: เอเจนต์พูดว่า "เสร็จ" ไม่ได้ตราบใดที่เทสต์ยังแดง
Hook Stop ทำงานเมื่อเอเจนต์กำลังจะจบ ถ้าเทสต์ไม่ผ่าน exit code 2 จะไม่ให้หยุด และผลของเทสต์ถูกส่งต่อให้เอเจนต์เป็นงานถัดไป นี่คือ Definition of Done ในรูปแบบที่กลไกบังคับใช้
#!/usr/bin/env bash
# .claude/hooks/done.sh
input=$(cat)
# กันวงวนไม่รู้จบ: hook นี้ต่ออายุการทำงานไปแล้วหนึ่งครั้ง
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
if ! out=$(npm test --silent 2>&1); then
echo "เทสต์ไม่ผ่าน งานยังไม่เสร็จ:" >&2
echo "$out" | tail -n 40 >&2
exit 2
fi
exit 0
ฟิลด์ stop_hook_active จะเป็น true เมื่อเอเจนต์กำลังทำงานต่อเพราะ stop hook อยู่แล้ว ถ้าไม่มีการตรวจนี้ hook อาจวนให้เอเจนต์ทำงานไม่รู้จบ ซึ่งเป็นสถานการณ์ที่เอเจนต์ "ทำงานทั้งคืนไปผิดทาง" ในวงวนอัตโนมัติ ให้เพิ่มลิมิตจำนวนครั้งและเงื่อนไขที่เรียกคนเข้ามาด้วย
ตัวอย่างที่ 4: โหลดสถานะตอนเริ่มเซสชัน
ใน SessionStart stdout ของ hook จะเข้าไปอยู่ในบริบท สะดวกที่จะแสดงสาขาปัจจุบัน การเปลี่ยนแปลงที่ยังไม่ commit และงานถัดไปจากแผน เพื่อให้เอเจนต์ไม่ต้องเริ่มจากศูนย์
#!/usr/bin/env bash
echo "สาขา: $(git branch --show-current)"
git status --short | head -n 20
[ -f PROGRESS.md ] && tail -n 15 PROGRESS.md
exit 0
hook เปลี่ยนกฎให้เป็นการรับประกันอย่างไร
กฎของฮาร์เนสแบ่งได้เป็นสามระดับ ระดับแรกคือข้อความใน CLAUDE.md ซึ่งอธิบายวิธีทำงาน แต่ถูกปฏิบัติตามตามความน่าจะเป็น ระดับที่สองคือการตรวจที่รันได้ เช่น เทสต์ E2E หรือกฎสถาปัตยกรรมในรูปสคริปต์ที่ล้มเหลวเมื่อมีการละเมิด ระดับที่สามคือ hook ที่รันการตรวจเหล่านี้โดยอัตโนมัติในจังหวะที่เหมาะสมโดยไม่ต้องให้โมเดลเข้ามาเกี่ยวข้อง
หลักปฏิบัติคือ ถ้าเอเจนต์ละเมิดกฎเดิมซ้ำสองครั้ง อย่าเพิ่มคำเตือนตัวพิมพ์ใหญ่อีกบรรทัดลงใน CLAUDE.md ให้ถามว่าการละเมิดนี้ตรวจด้วยสคริปต์ได้หรือไม่ ถ้าได้ ให้ทำสคริปต์แล้วผูกกับ hook ข้อความคำสั่งจะสั้นลง และพฤติกรรมจะคงที่ขึ้น
| กฎในข้อความ | การรับประกันผ่าน hook |
|---|---|
| "จัดรูปแบบโค้ดตามมาตรฐาน" | PostToolUse รัน formatter กับไฟล์ที่เปลี่ยน |
| "ห้าม push แบบ force" | PreToolUse บน Bash บล็อกคำสั่ง ด้วย exit 2 |
| "ห้ามพูดว่าเสร็จ ตราบใดที่เทสต์ยังแดง" | Stop รันเทสต์และไม่ให้จบงาน |
| "ห้ามแก้ไข .env" | PreToolUse บน Edit|Write ตรวจพาธ |
| "จำไว้ว่าหยุดค้างที่ไหน" | SessionStart แสดงแผนและ git status |
ความผิดพลาดที่พบบ่อย
- ใช้รหัส 1 แทน 2 สคริปต์ล้มด้วยรหัส 1 เอเจนต์ไม่รู้อะไรเลย และการกระทำผ่านไป มีเพียงรหัส 2 เท่านั้นที่บล็อก
- ส่งข้อความทาง stdout เมื่อใช้รหัส 2 เอเจนต์จะได้รับ stderr ให้เขียนผ่าน
>&2 - hook ช้าในเหตุการณ์ที่เกิดบ่อย การรันเทสต์ทั้งชุดใน PostToolUse ทำให้ทุกการแก้ไขใช้เวลาเป็นนาที ให้ย้ายงานหนักไปไว้ใน Stop
- Stop ที่วนไม่รู้จบ ไม่มีการตรวจ
stop_hook_activeหรือไม่มีลิมิต เอเจนต์จะวนไปเรื่อยๆ - พาธสัมพัทธ์ hook รันในไดเรกทอรีทำงานของเซสชัน ให้ใช้
$CLAUDE_PROJECT_DIR - hook ของคนอื่นที่ไม่ได้ตรวจ hook รันด้วยสิทธิ์ของคุณโดยไม่ขออนุมัติ ให้อ่านสคริปต์จาก repository ของคนอื่นก่อนไว้ใจ
สรุป
Claude Code hooks ย้ายกฎที่สำคัญที่สุดจากข้อความมาเป็นโค้ด PreToolUse หยุดการกระทำอันตราย PostToolUse ตรวจทุกการแก้ไข Stop ไม่ให้รายงานความพร้อมก่อนเวลา และ SessionStart คืนความจำเกี่ยวกับโปรเจกต์ให้เอเจนต์ เริ่มจาก hook สองสามตัวสำหรับกฎที่เอเจนต์เคยละเมิดแล้ว และเขียนข้อความใน stderr ให้เข้าใจง่าย เอเจนต์จะแก้ข้อผิดพลาดด้วยตัวเอง และคุณจะตรวจผลลัพธ์แทนที่จะตรวจกระบวนการ การตรวจสอบความพร้อม และวงวนอัตโนมัติ เราอธิบายอย่างละเอียดพร้อมการลงมือทำจริงในคอร์ส
คำถามที่พบบ่อย
ต้องรีสตาร์ท Claude Code หลังแก้ hook หรือไม่?
การแก้ settings.json ใช้ได้แน่นอนที่สุดในเซสชันใหม่ ส่วนการตั้งค่า hook ปัจจุบันดูได้ด้วยคำสั่ง /hooks
เขียน hook ด้วยภาษาอื่นที่ไม่ใช่ bash ได้ไหม?
ได้ hook คือคำสั่งใดก็ได้ เช่น สคริปต์ Python, Node.js หรือไบนารี สิ่งสำคัญคืออ่าน JSON จาก stdin และคืนรหัสผลลัพธ์ที่ถูกต้อง
เอเจนต์เห็นผลลัพธ์ของ hook ที่สำเร็จหรือไม่?
โดยทั่วไปไม่เห็น เมื่อรหัสเป็น 0 ผลลัพธ์จะไม่เข้าไปในบริบท ข้อยกเว้นคือ UserPromptSubmit และ SessionStart ที่ stdout จะถูกเพิ่มเข้าไปในบริบทของเอเจนต์
hook ใช้กับ subagent ด้วยหรือไม่?
ใช้ การเรียกเครื่องมือของ subagent ผ่าน PreToolUse และ PostToolUse เหมือนกัน และสำหรับการจบการทำงานของ subagent มีเหตุการณ์แยกคือ SubagentStop
ปลอดภัยไหมที่จะ commit hook ลงใน repository?
ปลอดภัย การเก็บ .claude/settings.json และสคริปต์ใน .claude/hooks/ ไว้ใน git เป็นแนวปฏิบัติปกติของทีม ส่วน hook ส่วนตัวให้เก็บไว้ใน settings.local.json หรือ ~/.claude/settings.json
หัวข้อนี้สอนอย่างละเอียดพร้อมฝึกปฏิบัติในคอร์ส “Harness Engineering: инженерия надёжности AI-агентов”
- Агент написал «готово», а фича не работает: verification gap и Definition of Done
- Все тесты зелёные, а фича сломана: E2E-тесты и архитектурные правила для AI-агента
- Автономный AI-агент часами без присмотра: Loop Engineering и условия остановки