productize.life
TH EN
Claude Code · Hooks

Voice จาก AI บอกว่างานไหนเสร็จ อยู่แท็บไหน ด้วย Claude Code hooks

สั่ง agent รันงานยาวแล้วลุกไปทำอย่างอื่น พอกลับมาก็งงว่าแท็บไหนเสร็จ นี่คือวิธีทำเสียงแจ้งที่บอกทั้งว่าอะไรเสร็จและอยู่แท็บไหน แล้วเข้าใจว่า hook คือช่องที่คุณสอนนิสัยให้ agent ได้

Yim· เขียนด้วยกันกับ Dobby (AI Oracle)/3 ก.ค. 2026

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

ปัญหาไม่ใช่ว่า agent ทำงานไม่ดี มันทำได้ดี ปัญหาคือ เราไม่รู้ว่ามันเสร็จเมื่อไหร่ และเสร็จอันไหน จอเงียบเหมือนเดิมทั้งตอนกำลังคิดและตอนจบแล้ว

สิ่งที่เราอยากได้ง่ายมาก เสียงสั้นๆ ตอนงานยาวจบ ที่บอกว่า "แท็บ api สรุปว่าแก้เทสต์เสร็จแล้ว" แค่นั้น จะได้เดินออกไปทำอย่างอื่นได้โดยไม่ต้องเฝ้าจอ แล้วมันก็พาไปเจอของที่ใหญ่กว่าเสียงแจ้งเตือน นั่นคือ hook ซึ่งเป็นจุดที่เราแทรกตัวเข้าไปสั่งพฤติกรรมของ agent ได้จริงๆ

ช่วงที่ 1hook คืออะไร ทำไมมันคือช่องที่คุณสั่ง agent ได้

เวลาเราคุยกับ agent เรามักคิดว่าจะปรับพฤติกรรมมันได้ทางเดียว คือ เขียน prompt ให้ดีขึ้น แต่ prompt เป็นการ "ขอ" ไม่ใช่การ "บังคับ" โมเดลจะทำตามหรือไม่ก็ได้ ส่วน hook คนละเรื่องกัน มันคือคำสั่งที่ตัว Claude Code เองรันให้แน่นอนทุกครั้ง ตามจังหวะที่กำหนด ไม่ผ่านการตัดสินใจของโมเดลเลย

คำที่ควรจำคือ deterministic แปลว่าผลออกมาเหมือนเดิมทุกครั้ง เพราะ host เป็นคนสั่งรัน ไม่ใช่โมเดล จุดนี้แหละที่ทำให้ hook เชื่อถือได้พอจะเอาไปทำเรื่องสำคัญ เช่น บล็อกคำสั่งอันตราย หรือกันไม่ให้ commit ความลับ ของแบบนี้ฝากไว้กับ "ขอให้โมเดลอย่าลืม" ไม่ได้

จังหวะที่ใช้บ่อย

Claude Code เปิดหลายจังหวะให้แทรก แต่ที่ใช้ในบทความนี้มีสองอัน

ยังมีจังหวะอื่นอีก เช่น PreToolUse กับ PostToolUse (ก่อนและหลัง agent เรียกใช้เครื่องมือ) และ Notification (ตอน agent ต้องหยุดรอถามคุณ) แต่ละอันเปิดโอกาสต่างกัน ตรงนี้เราหยิบแค่ Stop กับ UserPromptSubmit มาพอทำเรื่องเสียงแจ้งให้ครบ

hook ได้ข้อมูลอะไรมาบ้าง

ทุกครั้งที่ยิง Claude Code จะป้อนข้อมูลของเทิร์นเข้ามาทาง stdin เป็น JSON ก้อนหนึ่ง ที่เราใช้จริงมีสามค่า

นี่คือ seam ช่องที่แน่นอน มีข้อมูลของเทิร์นส่งมาให้ และเราแทรกโค้ดของเราเข้าไปได้ ที่เหลือคือเอา seam นี้ไปทำอะไรก็ได้

ช่วงที่ 2ตัวอย่างจริง เสียงบอกว่าอะไรเสร็จ แท็บไหน

ลองแกะปัญหาเดิมออกมา จริงๆ มันมีสองคำถามซ้อนกัน หนึ่ง งานเสร็จเมื่อไหร่ สอง เสร็จ "อะไร" และ "แท็บไหน" เราจะแก้ทีละอัน

ทำไมต้องมีสอง hook

ปัญหาแรกซ่อนกับดักไว้ Stop ยิงตอนจบเทิร์นก็จริง แต่ถ้าเทิร์นสั้นๆ แค่ถามตอบประโยคเดียว เราไม่อยากได้เสียงทุกครั้ง อยากได้เฉพาะตอนงานยาวจบ ปัญหาคือ Stop เพียงตัวเดียว ไม่รู้ว่าเทิร์นเริ่มตอนไหน เลยวัดไม่ได้ว่านานแค่ไหน

ทางออกคือให้ UserPromptSubmit จดเวลาเริ่มไว้ก่อน แล้ว Stop ค่อยเอาเวลาปัจจุบันมาลบ ได้ระยะเวลาของเทิร์น สอง hook ทำงานเป็นคู่ อันหนึ่งเปิดนาฬิกา อีกอันปิดแล้วอ่านค่า

hook แรก จดเวลาเริ่มของเทิร์น (UserPromptSubmit)

#!/usr/bin/env bash
# notify-turn-start.sh (UserPromptSubmit hook)
set -uo pipefail

sid="$(jq -r '.session_id // "nosession"')"
sid="${sid//[^A-Za-z0-9_-]/_}"          # กันตัวอักษรแปลกในชื่อไฟล์

date +%s > "/tmp/cc-turn-start-${sid}"   # จดเวลาเริ่ม
rm -f "/tmp/cc-done-note-${sid}"          # ล้างสรุปเก่าของเทิร์นก่อนหน้า
exit 0

ขั้นที่ต้องระวังตอนจบเทิร์น

hook ที่สอง (Stop) ทำสามอย่างตามลำดับ กันเทิร์นสั้น หาว่าเสร็จอะไร แล้วพูด แต่มีกับดักที่เจอกับตัวเอง Stop ยังยิงตอน resume, /clear และ compact ด้วย ถ้าไม่กันไว้ มันจะพูดมั่วตอนเปิด session ขึ้นมาใหม่ วิธีกันง่ายมาก กรณีพวกนั้นไม่มีไฟล์เวลาเริ่ม (เพราะไม่ได้ผ่าน UserPromptSubmit) เช็กว่าไม่มีไฟล์ก็ข้ามไปเลย

ส่วน "เสร็จอะไร" เลือกแหล่งข้อมูลสองชั้น ชั้นแรกดีที่สุด ให้ตัว agent เขียนสรุปหนึ่งบรรทัดลงไฟล์ชั่วคราวเองระหว่างทำงาน (สรุปที่คนเขียนย่อมคมกว่า) ถ้าเทิร์นไหนมันไม่ได้เขียน ค่อย fallback ไปดึงข้อความสุดท้ายจาก transcript มาตัดหางให้สั้น

hook ที่สอง กันเทิร์นสั้น หาว่าเสร็จอะไร แล้วพูด (Stop)

#!/usr/bin/env bash
# notify-done.sh (Stop hook)
set -uo pipefail

MIN_SEC="${CC_NOTIFY_MIN_SEC:-600}"     # พูดเฉพาะเทิร์นที่ยาว >= 10 นาที
[ "${CC_NOTIFY:-1}" = "0" ] && exit 0   # ปิดเสียงชั่วคราวได้โดยไม่ต้องลบ hook

INPUT="$(cat)"
sid="$(printf '%s' "$INPUT"   | jq -r '.session_id // "nosession"')"
sid="${sid//[^A-Za-z0-9_-]/_}"
cwd="$(printf '%s' "$INPUT"   | jq -r '.cwd // empty')"
tpath="$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty')"

# --- กันเทิร์นสั้น + กรณี resume/clear ที่ไม่มีไฟล์เวลาเริ่ม ---
start_file="/tmp/cc-turn-start-${sid}"
[ -f "$start_file" ] || exit 0
start="$(cat "$start_file")"; rm -f "$start_file"
case "$start" in (*[!0-9]*|"") exit 0 ;; esac
elapsed=$(( $(date +%s) - start ))
[ "$elapsed" -lt "$MIN_SEC" ] && exit 0

# --- แท็บไหน ตั้ง CC_TAB_LABEL เองต่อแท็บ ไม่งั้นใช้ชื่อโฟลเดอร์ ---
label="${CC_TAB_LABEL:-}"
[ -z "$label" ] && [ -n "$cwd" ] && [ "$cwd" != "$HOME" ] && label="$(basename "$cwd")"

# --- เสร็จอะไร สรุปที่ agent เขียนเองก่อน ไม่มีค่อยดึงจาก transcript ---
note="/tmp/cc-done-note-${sid}"; msg=""
[ -s "$note" ] && msg="$(cat "$note")" && rm -f "$note"
[ -z "$msg" ] && [ -f "$tpath" ] && msg="$(tail -n 200 "$tpath" \
  | jq -rs '[.[]|select(.type=="assistant")|.message.content[]?|select(.type=="text")|.text]|last // empty' \
  | sed -E 's/\`[^\`]*\`//g; s/[#*_>|~-]//g' | tr '\n' ' ' | cut -c1-160)"
[ -z "$msg" ] && msg="done"

say -r 175 "${label:+$label: }$msg"   # TTS ในตัวแมค สลับเป็นตัวอื่นได้

ต่อสายให้ Claude Code รู้จัก hook

เหลือขั้นสุดท้าย บอก Claude Code ว่า event ไหนให้รันสคริปต์ไหน เขียนใน settings.json (เช่น ~/.claude/settings.json) ใต้คีย์ hooks ให้ Stop รันแบบ async ไว้ เสียงจะได้ไม่หน่วงการคืนจอ

~/.claude/settings.json

{
  "hooks": {
    "UserPromptSubmit": [
      { "hooks": [{ "type": "command",
        "command": "bash ~/.claude/hooks/notify-turn-start.sh" }] }
    ],
    "Stop": [
      { "hooks": [{ "type": "command", "async": true,
        "command": "bash ~/.claude/hooks/notify-done.sh" }] }
    ]
  }
}

chmod +x ให้สคริปต์ทั้งสอง เปิด session ใหม่ แล้วลองสั่งงานยาวสักอัน (หรือลดค่า CC_NOTIFY_MIN_SEC ชั่วคราวเป็น 5 เพื่อทดสอบ) พองานจบ ควรได้ยินเสียงบอกชื่อแท็บกับสรุปงาน

ยกให้พูด "อะไรเสร็จ" ได้คมขึ้น

ชั้น fallback ที่ดึงจาก transcript ใช้ได้ แต่บางทีได้ประโยคเกริ่นนำมาแทนใจความ ทางที่คมกว่าคือฝึกให้ตัว agent เขียนสรุปเองใกล้จบเทิร์น สั่งครั้งเดียว เช่น บอกมันว่า "ก่อนจบเทิร์นยาว ให้เขียนสรุปหนึ่งบรรทัดลง /tmp/cc-done-note-$session_id" เท่านี้เสียงก็เปลี่ยนจาก "แก้โค้ดในไฟล์ ก. เสร็จ" เป็น "แก้บั๊ก null ตอน login เสร็จ เทสต์ผ่านหมด" ซึ่งบอกอะไรได้มากกว่า

ช่วงที่ 3ต่อยอดเป็นของคุณเอง

สิ่งเดียวที่อยากให้จำ

เสียงแจ้งเป็นแค่ข้ออ้างสนุกๆ ของที่อยากให้ติดตัวจริงคือ hook คือช่องที่แน่นอน ที่คุณสอนนิสัยใหม่ให้ agent ได้ พอมองมันเป็น seam ไม่ใช่แค่ลูกเล่น คุณจะเริ่มเห็นว่าพฤติกรรมหลายอย่างที่เคยต้องคอย "เตือนโมเดล" จริงๆ แล้วยกไปทำเป็น hook ได้ แล้วมันจะเกิดขึ้นทุกครั้งโดยไม่ต้องลุ้น

seam เดียวกันนี้ทำอะไรได้อีก

ทุกอันคือ seam เดิม เปลี่ยนแค่ event กับสิ่งที่รันในนั้น

เริ่มยังไงดี

อย่าเพิ่งทำครบทุกชั้นในทีเดียว ก่อไฟให้ติดก่อน

  1. เขียน Stop hook สั้นที่สุด ให้มัน say "done" เฉยๆ ต่อใน settings.json แล้วยืนยันว่าได้ยินเสียงตอนจบเทิร์นจริง
  2. เติมด่านกันเทิร์นสั้น คือ เพิ่ม UserPromptSubmit ที่จดเวลา แล้วให้ Stop เช็กระยะเวลา
  3. เติม "เสร็จอะไร" ดึงจาก transcript ก่อน แล้วค่อยยกให้ agent เขียนสรุปเอง
  4. เติม "แท็บไหน" ตั้ง CC_TAB_LABEL ต่อแท็บ

แต่ละชั้นทดสอบได้ทันที และของทั้งหมดเป็นเชลล์ล้วน ไม่ต้องลงอะไรเพิ่มนอกจาก jq ที่หลายเครื่องมีอยู่แล้ว

พอเสียงแรกดังขึ้นตอนงานยาวจบ ความรู้สึกมันเปลี่ยนไปเลย จอไม่ใช่ของที่ต้องเฝ้าอีกต่อไป และนั่นคือสิ่งที่ hook ให้ ไม่ใช่แค่เสียง แต่คือปุ่มที่คุณกดเพิ่มนิสัยให้เครื่องมือของตัวเองได้

อ่านต่อในชุด Claude Code

บทความนี้เป็นหนึ่งชั้น (เครื่องมือ/skill) ใน สถาปัตยกรรม AI agent ระดับ production ทั้ง 7 ชั้น

ที่มาและอ้างอิง
ติดตาม

รับบทความใหม่และของฟรีก่อนใคร

ทิ้งอีเมลไว้ บทความใหม่และของฟรีเป็นครั้งคราวจะส่งไปให้ ไม่สแปม

ใช้อีเมลเพื่อส่งอัปเดตเท่านั้น

ความคิดเห็น

ร่วมพูดคุย

แบ่งปันความคิดเห็นได้เลย

ชื่อจะแสดงต่อสาธารณะ อีเมลเก็บเป็นความลับ ไม่แสดงที่ไหน

กำลังโหลดความคิดเห็น…