ข้ามไปยังเนื้อหา
KoishiAI
EN
← กลับไปหน้าสารบัญ
บทที่ 7 / 12 · 5 กันยายน 2569

Hooks ฉบับเต็ม

บังคับกติกาด้วยเครื่อง ไม่ใช่ด้วยการขอร้อง — เหตุการณ์ทั้ง 33 ตัว สัญญา JSON ที่ hook รับและตอบ ความหมายของ exit code และตัวอย่างที่เอาไปวางใช้ได้ทันที

คู่มือชุดนี้ลิขสิทธิ์ All Rights Reserved — อ่านได้ฟรี แต่คัดลอก/เผยแพร่ซ้ำต้องขออนุญาต

บทที่ 7 | Hooks ฉบับเต็ม กลุ่มเป้าหมาย: คนที่ใช้ Claude Code เป็นแล้ว และอยากให้บางอย่าง เกิดขึ้นแน่นอน ไม่ใช่หวังว่าโมเดลจะเลือกทำ

ทำไมต้องมี hook ทั้งที่เขียนกติกาใน CLAUDE.md ได้

ความต่างอยู่ตรงนี้ กติกาใน CLAUDE.md คือคำขอ ส่วน hook คือการบังคับ

CLAUDE.md บอกว่า “ห้ามแก้ไฟล์ .env” ได้ และส่วนใหญ่โมเดลก็ทำตาม แต่ “ส่วนใหญ่” ไม่ใช่ “เสมอ” โดยเฉพาะเมื่อ context ยาวขึ้นหรือเจอสถานการณ์ที่ดูสมเหตุสมผลกว่า ส่วน hook เป็นคำสั่ง shell ที่ Claude Code รันเองที่จุดต่าง ๆ ของวงจรการทำงาน มันไม่ได้ผ่านการตัดสินใจของโมเดลเลย ถ้าเขียนว่าบล็อกก็คือบล็อก

เอกสารทางการอธิบายไว้ตรง ๆ ว่า hook ให้ “deterministic control: certain actions always happen rather than relying on the LLM to choose to run them”

โครงสร้างของการตั้ง hook

hook อยู่ในบล็อก hooks ของไฟล์ settings รูปร่างเป็นแบบนี้เสมอ

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "คำสั่งที่จะรัน" }
        ]
      }
    ]
  }
}

สังเกตว่ามี hooks ซ้อนสองชั้น ชั้นนอกคือชื่อ เหตุการณ์ ชั้นในคือรายการ คำสั่ง ที่จะรันเมื่อ matcher ตรง

ถ้าไฟล์ settings มีคีย์ hooks อยู่แล้ว ให้เพิ่มชื่อเหตุการณ์ใหม่เป็นพี่น้องกับเหตุการณ์เดิม อย่าเขียนทับทั้งก้อน เพราะชื่อเหตุการณ์ทุกตัวอยู่ในอ็อบเจกต์ hooks เดียวกัน

วางไว้ที่ไหน ขอบเขตต่างกันอย่างไร

ตำแหน่งขอบเขตแชร์ให้ทีมได้ไหม
~/.claude/settings.jsonทุกโปรเจกต์ของคุณไม่ อยู่เฉพาะเครื่องคุณ
.claude/settings.jsonโปรเจกต์เดียวได้ commit เข้า repo ได้
.claude/settings.local.jsonโปรเจกต์เดียวไม่ ถูก gitignore เมื่อ Claude Code บันทึกค่าลงไป
Managed policy settingsทั้งองค์กรได้ ผู้ดูแลควบคุม
Plugin hooks/hooks.jsonเมื่อเปิดใช้ปลั๊กอินนั้นได้ ติดไปกับปลั๊กอิน
Frontmatter ของ Skillตลอด session ที่เหลือหลังเรียก skill นั้นได้ อยู่ในไฟล์ skill
Frontmatter ของ Subagentระหว่างที่ subagent นั้นทำงานได้ อยู่ในไฟล์ subagent

พิมพ์ /hooks ใน Claude Code เพื่อดู hook ทั้งหมดที่ตั้งไว้ จัดกลุ่มตามเหตุการณ์ เมนูนี้อ่านอย่างเดียว ถ้าจะเพิ่ม แก้ หรือลบ ต้องแก้ไฟล์ JSON เอง หรือบอกให้ Claude แก้ให้

ถ้าจะปิด hook ทั้งหมด ให้ตั้ง "disableAllHooks": true ในไฟล์ settings

เหตุการณ์ทั้งหมด 33 ตัว

นี่คือส่วนที่คู่มือฉบับก่อนไม่มีเลย เรารู้ว่ามี hook แต่ไม่เคยรู้ว่ามีเหตุการณ์อะไรให้เกาะบ้าง

ช่วงเริ่มและจบ session

เหตุการณ์ยิงเมื่อ
SessionStartเริ่ม session ใหม่หรือ resume ของเดิม
Setupเริ่มด้วย --init-only หรือใช้ --init / --maintenance ในโหมด -p
SessionEndsession จบลง

ช่วงรับคำสั่งจากผู้ใช้

เหตุการณ์ยิงเมื่อ
UserPromptSubmitผู้ใช้ส่ง prompt ก่อนที่ Claude จะประมวลผล
UserPromptExpansionคำสั่งที่ผู้ใช้พิมพ์ถูกขยายเป็น prompt ก่อนถึงโมเดล บล็อกการขยายได้

ช่วงเรียกใช้เครื่องมือ — กลุ่มที่ใช้บ่อยที่สุด

เหตุการณ์ยิงเมื่อ
PreToolUseก่อนเรียกเครื่องมือ บล็อกได้
PermissionRequestเมื่อการเรียกเครื่องมือต้องการการตัดสินใจเรื่องสิทธิ์
PermissionDeniedเมื่อ auto mode ปฏิเสธการเรียกเครื่องมือ
PostToolUseหลังเรียกเครื่องมือสำเร็จ
PostToolUseFailureหลังเรียกเครื่องมือล้มเหลว
PostToolBatchหลังชุดเครื่องมือที่รันขนานกันเสร็จครบ ก่อนเรียกโมเดลรอบถัดไป

ช่วงตอบและแสดงผล

เหตุการณ์ยิงเมื่อ
MessageDisplayระหว่างแสดงข้อความตอบของผู้ช่วย
Notificationเมื่อ Claude Code ส่งการแจ้งเตือน
Stopเมื่อ Claude ตอบจบ
StopFailureเมื่อรอบจบเพราะ API error

ช่วง subagent และงาน

เหตุการณ์ยิงเมื่อ
SubagentStartเมื่อ subagent ถูกสร้าง
SubagentStopเมื่อ subagent ทำงานเสร็จ
TaskCreatedเมื่อกำลังสร้างงานผ่าน TaskCreate
TaskCompletedเมื่องานกำลังถูกทำเครื่องหมายว่าเสร็จ
TeammateIdleเมื่อเพื่อนร่วมทีมใน agent team กำลังจะว่าง

ช่วง context และไฟล์

เหตุการณ์ยิงเมื่อ
InstructionsLoadedเมื่อไฟล์ CLAUDE.md หรือ .claude/rules/*.md ถูกโหลดเข้า context
ConfigChangeเมื่อไฟล์ตั้งค่าเปลี่ยนระหว่าง session
CwdChangedเมื่อไดเรกทอรีทำงานเปลี่ยน
DirectoryAddedเมื่อเพิ่มไดเรกทอรีกลางคันด้วย /add-dir หรือ register_repo_root ของ SDK
FileChangedเมื่อไฟล์ที่เฝ้าอยู่เปลี่ยนบนดิสก์
WorktreeCreateเมื่อกำลังสร้าง worktree
WorktreeRemoveเมื่อกำลังลบ worktree
PreCompactก่อนบีบอัด context
PostCompactหลังบีบอัด context เสร็จ

ช่วงเปลี่ยนโมเดลและ MCP

เหตุการณ์ยิงเมื่อ
PreModelSwitchก่อน Claude Code สลับโมเดล บล็อกการสลับได้
PostModelSwitchหลังโมเดลของ session เปลี่ยนแล้ว
Elicitationเมื่อ MCP server ขอข้อมูลจากผู้ใช้ระหว่างเรียกเครื่องมือ
ElicitationResultหลังผู้ใช้ตอบ elicitation ของ MCP

สัญญา JSON ที่ hook ได้รับ

hook แบบ command รับ JSON ทาง stdin ตัวอย่างของ PreToolUse ที่ดักคำสั่ง Bash

{
  "session_id": "abc123",
  "prompt_id": "550e8400-e29b-41d4-a716-446655440000",
  "transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
  "cwd": "/home/user/my-project",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test",
    "description": "Run test suite",
    "timeout": 120000,
    "run_in_background": false
  },
  "tool_use_id": "toolu_01ABC123..."
}

ฟิลด์ที่ใช้บ่อยที่สุดคือ tool_name กับ tool_input ส่วน permission_mode มีประโยชน์เมื่ออยากให้ hook เข้มงวดต่างกันตามโหมด

ความหมายของ exit code — จุดที่พลาดกันบ่อยที่สุด

Exit codeความหมาย
0สำเร็จ Claude Code อ่านฟิลด์ JSON จาก stdout ส่วนใหญ่ stdout จะไปลง debug log ยกเว้น UserPromptSubmit, UserPromptExpansion, SessionStart และ PostModelSwitch ที่ข้อความธรรมดาจาก stdout จะถูกเพิ่มเข้า context ให้ Claude เห็น
2บล็อก สำหรับเหตุการณ์ที่บล็อกได้ exit 2 จะบล็อกไม่ว่าคุณจะพิมพ์ JSON หรือไม่ แม้แต่ permissionDecision ที่เป็น "allow" ก็ทับไม่ได้ ข้อความที่บล็อกมาจากเหตุผลใน JSON หรือจาก stderr
อื่น ๆerror แบบไม่บล็อก การทำงานเดินต่อ ถ้ามี JSON ที่ผ่าน schema ตัว JSON ยังคุมผลลัพธ์ได้สำหรับเหตุการณ์ที่ใช้โมเดลการตัดสินใจมาตรฐาน

จำง่าย ๆ: 2 คือบล็อก อย่างอื่นคือไม่บล็อก และข้อความที่อยากให้ agent อ่านเพื่อแก้ตัวเอง ให้พิมพ์ลง stderr

ตัวอย่างที่เอาไปใช้ได้ทันที

1. แจ้งเตือนเมื่อ Claude รอคุณอยู่

จะได้ไม่ต้องนั่งจ้องเทอร์มินัล ใส่ใน ~/.claude/settings.json

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

บน Linux เปลี่ยนคำสั่งเป็น notify-send 'Claude Code' 'Claude Code needs your attention'

ถ้าไม่มีอะไรเด้งบน macOS osascript ส่งการแจ้งเตือนผ่านแอป Script Editor ถ้าแอปนั้นยังไม่ได้รับสิทธิ์แจ้งเตือน คำสั่งจะล้มเหลวเงียบ ๆ และ macOS จะไม่ถามให้ด้วย ให้รัน osascript -e 'display notification "test"' ในเทอร์มินัลหนึ่งครั้ง จะยังไม่มีอะไรขึ้น จากนั้นเปิด System Settings → Notifications หา Script Editor แล้วเปิด Allow Notifications แล้วรันซ้ำ

2. จัดฟอร์แมตอัตโนมัติหลังแก้ไฟล์

ใส่ใน .claude/settings.json ของโปรเจกต์

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

ตัวอย่างในเอกสารใช้ jq แยก JSON ติดตั้งด้วย brew install jq บน macOS หรือ apt-get install jq บน Debian และ Ubuntu

ถ้าอยากให้ไฟล์ใดไฟล์หนึ่งถูกจัดฟอร์แมตทุกครั้งที่เปลี่ยน รวมถึงตอนที่คำสั่ง Bash เป็นคนเขียนทับ ให้ใช้เหตุการณ์ FileChanged แทน

3. บล็อกการแก้ไฟล์ที่ห้ามแตะ

เขียนเป็นสคริปต์แยกจะอ่านง่ายกว่า เก็บที่ .claude/hooks/protect-files.sh

#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# แปลง backslash ของ Windows ให้ตรงกับ pattern ข้างล่าง
FILE_PATH="${FILE_PATH//\\//}"

PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")

for pattern in "${PROTECTED_PATTERNS[@]}"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
    exit 2
  fi
done

exit 0

บน macOS และ Linux ต้องทำให้รันได้ก่อน มิฉะนั้น Claude Code เรียกไม่ได้

chmod +x .claude/hooks/protect-files.sh

แล้วลงทะเบียนใน .claude/settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
          }
        ]
      }
    ]
  }
}

$CLAUDE_PROJECT_DIR คือตัวแปรที่ Claude Code ตั้งให้ ทำให้อ้างพาธได้โดยไม่ต้องฮาร์ดโค้ด

ทดสอบด้วยการขอให้ Claude เติมคอมเมนต์ในไฟล์ .env มันจะถูกบล็อกก่อนรัน และข้อความ Blocked: จะถูกส่งกลับไปให้ Claude อ่านเป็น feedback

4. เติม context กลับหลังการบีบอัด

เมื่อ context เต็ม Claude Code จะบีบอัดบทสนทนาเพื่อคืนที่ว่าง ซึ่งอาจทำรายละเอียดสำคัญหาย ใช้ SessionStart กับ matcher compact เพื่อเติมกลับทุกครั้งหลังบีบอัด ข้อความธรรมดาที่คำสั่งพิมพ์ออก stdout จะถูกเพิ่มเข้า context ของ Claude

5. อนุมัติสิทธิ์บางอย่างให้อัตโนมัติ

{
  "hooks": {
    "PermissionRequest": [
      {
        "matcher": "ExitPlanMode",
        "hooks": [
          {
            "type": "command",
            "command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'"
          }
        ]
      }
    ]
  }
}

hook แบบอื่นที่ไม่ใช่ command

เอกสารทางการยังรองรับอีกสามแบบสำหรับกรณีที่กฎตายตัวไม่พอ

  • Prompt-based hook ใช้โมเดลประเมินเงื่อนไขที่ต้องใช้วิจารณญาณ
  • Agent-based hook ให้ agent ตัวหนึ่งประเมินแทน
  • HTTP hook ยิงไปที่ endpoint แทนการรันคำสั่งบนเครื่อง

ข้อควรระวังจากประสบการณ์

Stop hook ที่รัน gate เต็มชุดจะทำให้ agent ติดวน และเผา token มาก ให้รันชุดเร็ว เช่น lint กับ unit test เฉพาะไฟล์ที่แตะ ไว้ใน hook แล้วเก็บชุดเต็มไว้ที่ CI

hook ไม่ทำงานให้ตรวจสามอย่าง คือสคริปต์ executable หรือยัง, /hooks เห็น hook นั้นไหม, และ matcher เขียนตรงกับชื่อเครื่องมือจริงหรือเปล่า

อย่าใส่ค่า secret ในคำสั่ง hook เพราะคำสั่งถูกเก็บในไฟล์ settings ที่มักถูก commit ให้อ้างผ่านตัวแปรแวดล้อมแทน

คำศัพท์ประจำบท

คำความหมาย
Hookคำสั่ง shell ที่ Claude Code รันเองที่จุดหนึ่งของวงจรการทำงาน
Eventชื่อจังหวะที่ hook เกาะ เช่น PreToolUse
Matcherเงื่อนไขว่าจะรัน hook นี้กับเครื่องมือหรือกรณีไหน
Exit 2รหัสที่แปลว่าบล็อก ใช้ได้กับเหตุการณ์ที่บล็อกได้
$CLAUDE_PROJECT_DIRตัวแปรที่ชี้รากโปรเจกต์ ใช้อ้างพาธสคริปต์ hook
disableAllHooksค่าที่ปิด hook ทั้งหมดในขอบเขตนั้น