ฮุกของ Claude Code จะปล่อยผ่านเมื่อหมดเวลา — ทางออกฉุกเฉินที่เอกสารทางการระบุไว้ ในกลไกที่ตั้งใจให้บังคับใช้
บทนำ
ฉันเคยเขียนถึงการสร้างสิ่งที่ไม่ใช่ “คำแนะนำด้วยวาจา” แต่เป็น “ประตูที่หยุดสิ่งต่าง ๆ ก่อนรันเสมอ” ไว้ในผู้ช่วย AI เขียนโค้ด กลไกของประตูนั้นใช้สิ่งที่ Claude Code จัดไว้ให้ในชื่อ “ฮุก” — ความสามารถที่แทรกโปรแกรมตรวจสอบของคุณเองเข้าไปโดยไม่มีข้อยกเว้น ก่อนหรือหลังการกระทำบางอย่าง (การแก้ไฟล์, การรันคำสั่ง และอื่น ๆ)
คราวนี้ฉันอ่านแหล่งข้อมูลปฐมภูมิทางการเรื่องฮุกจนจบ (3,409 บรรทัด) เป้าหมายไม่ใช่การมองหาฟีเจอร์ใหม่ แต่คือ การหาหลักฐานยืนยันในถ้อยคำทางการ ให้กับตัวการออกแบบที่ฉันสันนิษฐานว่า “เป็นชั้นบังคับ มันจึงทำงาน 100%” การยืนยันสำเร็จไปครึ่งหนึ่ง และอีกครึ่งหนึ่งรื้อสมมติฐานของฉันออกเป็นชิ้น ๆ
ประตูหลายบานทำงานพร้อมกันหมด
ก่อนอื่นฉันยืนยันพฤติกรรมเมื่อมีการตั้งฮุกหลายตัวสำหรับสถานการณ์เดียวกัน
All matching hooks run in parallel.
ฮุกที่ตรงเงื่อนไขจะทำงาน พร้อมกันหมด ไม่ใช่ต่อคิว ลำดับความสำคัญเมื่อคำตัดสินต่างกันก็ถูกนิยามไว้ด้วย
When multiple PreToolUse hooks return different decisions, precedence is
deny>defer>ask>allow.
deny แข็งแรงที่สุด แล้วตามด้วย defer, ask, allow ถ้าประตูใดประตูหนึ่งในหลายบานคืนค่า “deny” มันก็หยุด ไม่ว่าบานอื่นจะตัดสินอย่างไร ถึงตรงนี้ ข้อตั้งต้นที่ฉันออกแบบมาก็ถูกระบุไว้อย่างเป็นทางการเช่นกัน
การตั้งค่า “ทำงานเฉพาะภายใต้เงื่อนไขนี้” ทำงานกว้างกว่าที่คาด
ฮุกสามารถมีเงื่อนไข (if) กำกับว่า “รันเฉพาะกับคำสั่งนี้” เอกสารทางการแสดงขอบเขตของเงื่อนไขนั้นด้วยตัวอย่างรูปธรรม
| เงื่อนไขที่ตั้ง | คำสั่งจริง | มันทำงานไหม | เพราะอะไร |
|---|---|---|---|
Bash(git *) | npm test && git push | ใช่ | เมื่อมีหลายคำสั่งต่อกัน แต่ละคำสั่งถูกตัดสินแยกกัน |
Bash(rm *) | echo $(rm -rf /) | ใช่ | คำสั่งที่ซ่อนอยู่ใน $() หรือ backtick ก็อยู่ในขอบเขตด้วย |
แม้คุณจะคิดว่าหดเงื่อนไขไว้แค่ “เฉพาะคำสั่ง git” คำสั่งอื่นที่ต่อกันด้วย && หรือคำสั่งที่ซ่อนอยู่ในการขยายตัวแปร ก็เข้ามาอยู่ในขอบเขต ช่องว่างวิ่งไปในทิศทางนี้: คุณเขียนเงื่อนไขให้แคบ แล้วมันไปทำงานกับสตริงคำสั่งที่คุณไม่เคยคาดคิด
ข้อค้นพบหลัก: การตอบสนองที่ช้าหมายความว่าประตูนั้นไม่เคยมีอยู่
นี่คือหัวใจของเรื่อง ฮุกสามารถมีขีดจำกัดเวลาการทำงาน (timeout) กำกับได้ สิ่งที่เกิดขึ้นเมื่อถึงขีดจำกัดนั้นถูกนิยามไว้อย่างเป็นทางการดังนี้
A
command,http, ormcp_toolhook that reaches its timeout is canceled: Claude Code discards the hook’s output, and the hook renders no decision.A timed-out
command,http, ormcp_toolhook doesn’t block the tool call. The call continues through the normal permission flow, so don’t count on a stalled hook to act as a gate.
ถ้าฮุกตอบภายในเวลาที่กำหนดไม่ได้ คำตัดสินของมันจะถูกปฏิบัติไม่ใช่ในฐานะ “deny” แต่ในฐานะ “ไม่ได้พูดอะไร” และการกระทำก็ผ่านไป สำหรับการออกแบบที่ฉันเรียกว่าชั้นบังคับมาตลอด นั่นเรียกร้องการทบทวนข้อตั้งต้นอย่างถึงราก ฉันเคยคิดว่าชั้นบังคับ “หยุดสิ่งต่าง ๆ 100% ไม่ว่า Claude จะตัดสินอย่างไร” ส่วนข้อความทางการระบุชัดเจนว่า “ถ้าการตอบของฮุกมาไม่ทัน มันจะผ่านไปเลย” ตัวกลไกการบังคับใช้เองมีตัวแปรอีกตัวพันอยู่: ความเร็วในการตอบสนอง
เอกสารทางการระบุทางเลือกไว้ในที่เดียวกัน
Because the
iffilter is best-effort, use the permission system rather than a hook to enforce a hard allow or deny.
ฮุกที่มีตัวกรองเงื่อนไขเป็นกลไกแบบ best-effort ไม่ใช่การบังคับใช้ที่เชื่อถือได้ ถ้าคุณต้องการให้บางอย่างถูกหยุดอย่างเชื่อถือได้ ตำแหน่งที่วางไว้คือคุณใช้กฎ deny ในระบบสิทธิ์ ไม่ใช่ฮุก
รูโหว่เงียบอีกรู — คำผิดหนึ่งตัวปิดประตูโดยไม่มีการแจ้งเตือน
นอกจาก timeout ยังมีรูโหว่รูปแบบ “ผ่านไปโดยไม่มีใครสังเกต” เดียวกันจากอีกเส้นทางหนึ่ง
A hook that can’t start lands in the same non-blocking bucket. When the script path doesn’t exist or isn’t executable, the shell exits with a code like 127 and you see the same notice with the interpreter’s message … When you set up a policy hook, watch for this notice on its first run: a mistyped path in
settings.jsonleaves the gate silently disabled.
พิมพ์พาธของฮุก (ที่ที่โปรแกรมอยู่) ผิดไปตัวอักษรเดียวในไฟล์ตั้งค่า แล้ว ข้อผิดพลาดจะปรากฏ แต่ตัวการกระทำเองก็ยังรันต่อไปโดยไม่ถูกหยุด การที่คุณจะจับคำผิดได้หรือไม่ ขึ้นอยู่กับว่าคุณเฝ้าดูการแจ้งเตือนนั้นในการรันครั้งแรกหรือเปล่า เพียงอย่างเดียว
วัดจริง — การตั้งค่าของฉันอยู่ในรูโหว่นี้หรือไม่
หลังอ่านข้อความทางการ ฉันตรวจการตั้งค่าจริง
[実測] 実行前に発火するフック(PreToolUse)の設定一覧を確認
対象: 実行前チェックの全ハンドラ
type: すべて "command"(プログラムを直接起動する形式)
timeout: すべて 20 秒 に設定
⇒ 応答に20秒以上かかるフックが1つでもあれば、
そのフックだけ「何も言っていない」扱いになり、他の判定に委ねられる
สิ่งที่กัดตรงนี้คือ ค่าตั้งต้นของขีดจำกัดนั้นคือเท่าไหร่ เอกสารทางการอีกฉบับ (นิยามฮุกในฝั่งชุดพัฒนา) ระบุค่าตั้งต้นเมื่อไม่ได้ใส่ค่าไว้
Timeout in seconds. When omitted, the per-event default applies: 600 for most events, 30 for
UserPromptSubmit
นั่นคือ ถ้าไม่ระบุอะไรเลยมันคือ 600 วินาที ส่วนฉันตั้งไว้ที่ 20 ฉันได้ย้ายเงื่อนไขของการเปิดรูโหว่ fail-open ไปสู่การตั้งค่าที่มีโอกาสทำงานมากกว่าค่าตั้งต้นสามสิบเท่า ด้วยมือตัวเอง และนี่ไม่ใช่อุบัติเหตุแต่เป็นการเลือกอย่างจงใจ — ระหว่างรอการตอบของฮุก การตอบกลับต่ออินพุตของฉันเองก็ค้างอยู่ ค่า 20 วินาทีถูกเลือกเพื่อจำกัดการรอนั้น ฉันแลกการบังคับใช้กับเวลารอ และมีเพียงด้านที่ฉันจ่ายไปเท่านั้นที่ไม่ถูกเขียนไว้ที่ไหนเลย
ว่า 20 วินาทีเคยถูกไปถึงจริงหรือไม่ ฉันปล่อยไว้โดยไม่ยืนยัน ณ จุดนี้ ฉันไม่ได้เปลี่ยนการตั้งค่าทันที ฉันยุติเฉพาะข้อเท็จจริงก่อน — ว่าชั้นบังคับไม่ใช่ 100% และมีตัวแปรอีกตัวคือความเร็วในการตอบสนองติดอยู่ด้วย
เส้นทางที่ข้อค้นพบเดินผ่าน
เริ่ม: หาหลักฐานยืนยันข้อตั้งต้นที่ว่า "ประตูก่อนรัน (ฮุก) ทำงาน 100%" กับข้อความทางการฉบับเต็ม
│ (แนวตั้ง = ลำดับที่อ่าน, แนวนอน = ประเด็นที่ตรวจ, การซ้อน = รายละเอียดของมัน)
├─▶ ยืนยันการทำงานขนานของฮุกหลายตัวและลำดับความสำคัญ
│ └─ ✅ ตรงตามข้อตั้งต้น (deny มีลำดับสูงสุด) ──────────┐
│ │ ข้อตั้งต้น
├─▶ ยืนยันขอบเขตการทำงานของตัวกรองเงื่อนไข (if) │ ยืนได้แค่
│ └─ ⚠️ ทำงานกว้างกว่าที่สันนิษฐาน │ ครึ่งทาง
│ (คำสั่งที่ต่อกันและการขยายตัวแปรอยู่ในขอบเขต) │
│ │
├─▶ ยืนยันพฤติกรรมเมื่อถึง timeout ★ ข้อตั้งต้นพังตรงนี้ │
│ └─ ⛔ ไม่ใช่ "deny" แต่คือ "ผ่านไปอย่างเงียบ ๆ" │
│ ├─ ทางเลือกที่เอกสารทางการระบุ: │
│ │ กฎ deny ในระบบสิทธิ์ │
│ └─ วัดการตั้งค่าของตัวเอง ── ทุกแฮนด์เลอร์ 20 วิ │
│ │ │
│ └─▶ แล้วค่าตั้งต้นคือเท่าไหร่ ── 600 วิ ในอีกเอกสาร
│ │ │
│ └──(เส้นย้อนกลับ)──▶ อ่านมันใหม่ ไม่ใช่ในฐานะ "รูที่เปิดโดยบังเอิญ"
│ แต่ในฐานะ "รูที่แลกมาด้วยเวลารอ"
│
└─▶ รูโหว่เงียบอีกรู — พาธที่พิมพ์ผิด
└─ ข้อผิดพลาดปรากฏแต่การกระทำไม่หยุด (สิ่งเดียวที่มีคือการจับการแจ้งเตือน)
│ │
└───────────────┬───────────────────────────┘
▼
รูโหว่ทั้งสองมีรูปร่างเดียวกัน: งานเดินหน้าต่อ
โดยที่คุณไม่ถูกบอกว่าไม่มีอะไรถูกหยุด
การทบทวนการออกแบบของชั้นบังคับหมายถึงอะไร
ฉันเคยเขียนที่อื่นถึงข้อบกพร่องอีกชนิดที่โดนมาจริง — ประตูของฉันเองตอบสนองผิดต่ออินพุตของฉันเอง สิ่งที่พบคราวนี้เป็นคนละชนิด: แม้ตรรกะของฮุกแต่ละตัวจะถูกต้อง ปัจจัยหนึ่งที่อยู่นอกการออกแบบ — เวลาตอบสนอง — ก็ทำให้ทั้งประตูเหมือนไม่เคยมีอยู่ได้ นั่นคือขีดจำกัดในโครงกระดูกของตัวชั้นบังคับเอง
เอกสารออกแบบที่บอกว่า “หยุดเสมอ” กับการอิมพลีเมนต์ทางการที่บอกว่า “ผ่านไปเมื่อหมดเวลา” ถูกต้องทั้งคู่ แต่อย่างแรกคืออุดมคติ และอย่างหลังคือพฤติกรรมจริง แม้หลังสร้างชั้นบังคับแล้ว ประตูจะมีผลหรือไม่ ขึ้นอยู่กับว่าคนที่อิมพลีเมนต์มันเข้าใจขีดจำกัดที่ตัวกลไกการบังคับใช้แบกไว้หรือเปล่า
ประตูไม่ส่งเสียงตอนที่มันถูกฝ่า ฉันจำได้ว่าเขียนเลข 20 ลงไป แต่จำไม่ได้ว่ามันเป็นหนึ่งในสามสิบของค่าตั้งต้น และจำไม่ได้ว่ามันถูกแลกมากับอะไร — หนึ่งบรรทัดข้างการตั้งค่าที่บอกว่า “ฉันยอมสละอะไรไปเพื่อเลือกค่านี้” ก็คงทำให้ไม่ต้องอ่านใหม่ทั้งฉบับกว่าจะสังเกตเห็น