ฮุกของ Claude Code จะปล่อยผ่านเมื่อหมดเวลา — ทางออกฉุกเฉินที่เอกสารทางการระบุไว้ ในกลไกที่ตั้งใจให้บังคับใช้

AI Claude Code Operations Security 検証

บทนำ

ฉันเคยเขียนถึงการสร้างสิ่งที่ไม่ใช่ “คำแนะนำด้วยวาจา” แต่เป็น “ประตูที่หยุดสิ่งต่าง ๆ ก่อนรันเสมอ” ไว้ในผู้ช่วย 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, or mcp_tool hook that reaches its timeout is canceled: Claude Code discards the hook’s output, and the hook renders no decision.

A timed-out command, http, or mcp_tool hook 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 if filter 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.json leaves 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 วิ ในอีกเอกสาร
      │                          │                                       │
      │                          └──(เส้นย้อนกลับ)──▶ อ่านมันใหม่ ไม่ใช่ในฐานะ "รูที่เปิดโดยบังเอิญ"
      │                                              แต่ในฐานะ "รูที่แลกมาด้วยเวลารอ"

      └─▶ รูโหว่เงียบอีกรู — พาธที่พิมพ์ผิด
               └─ ข้อผิดพลาดปรากฏแต่การกระทำไม่หยุด (สิ่งเดียวที่มีคือการจับการแจ้งเตือน)
                     │                                           │
                     └───────────────┬───────────────────────────┘

                    รูโหว่ทั้งสองมีรูปร่างเดียวกัน: งานเดินหน้าต่อ
                    โดยที่คุณไม่ถูกบอกว่าไม่มีอะไรถูกหยุด

การทบทวนการออกแบบของชั้นบังคับหมายถึงอะไร

ฉันเคยเขียนที่อื่นถึงข้อบกพร่องอีกชนิดที่โดนมาจริง — ประตูของฉันเองตอบสนองผิดต่ออินพุตของฉันเอง สิ่งที่พบคราวนี้เป็นคนละชนิด: แม้ตรรกะของฮุกแต่ละตัวจะถูกต้อง ปัจจัยหนึ่งที่อยู่นอกการออกแบบ — เวลาตอบสนอง — ก็ทำให้ทั้งประตูเหมือนไม่เคยมีอยู่ได้ นั่นคือขีดจำกัดในโครงกระดูกของตัวชั้นบังคับเอง

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

การ์ดที่ฉันสร้างให้ Claude Code ระเบิดใส่ข้อความคอมมิตของตัวมันเอง — ลากเส้นแบ่งระหว่างผู้ตรวจกับสิ่งที่ถูกตรวจใหม่ ด้วยตัวเนื้อหาจริง

สร้างกลไก “ไม่ให้ทำ” สำหรับผู้ช่วย AI เขียนโค้ด — จากคำแนะนำสู่การบังคับ แล้วพบว่าประตูนั้นเองก็มีช่องโหว่ข้าง

สิทธิ์, แซนด์บ็อกซ์ และพาธที่ถูกป้องกันใน Claude Code — อ่านสเปกทางการ 3 ฉบับเพื่อหาประตูจริงที่หยุด AI ไม่ให้แก้ไฟล์ตั้งค่าของตัวเอง

ประตูไม่ส่งเสียงตอนที่มันถูกฝ่า ฉันจำได้ว่าเขียนเลข 20 ลงไป แต่จำไม่ได้ว่ามันเป็นหนึ่งในสามสิบของค่าตั้งต้น และจำไม่ได้ว่ามันถูกแลกมากับอะไร — หนึ่งบรรทัดข้างการตั้งค่าที่บอกว่า “ฉันยอมสละอะไรไปเพื่อเลือกค่านี้” ก็คงทำให้ไม่ต้องอ่านใหม่ทั้งฉบับกว่าจะสังเกตเห็น

ส่งข้อความได้ตามสบาย

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