Boom Leverage
บทความทั้งหมด

กัน AI Agent ลบไฟล์พลาด: Hook ใน Claude Code ที่เปลี่ยน rm เป็นถังกู้คืน (2026)

AI agent ที่รันเองลบไฟล์จริงได้ — ผมเลยวางกฎว่าห้าม rm แต่ให้ย้ายเข้า .archive/ แทน แล้วบังคับด้วย PreToolUse hook ที่ก็อปไปใช้ได้เลย มุมคนทำ model risk เรื่องกู้คืนได้

Varanchai Yingkhamnueng·
claude-codeBoom Leverage

กัน AI Agent ลบไฟล์พลาด: Hook ใน Claude Code ที่เปลี่ยน rm เป็นถังกู้คืน (2026)

AI agent ที่รันเองลบไฟล์จริงได้ — ผมเลยวางกฎว่าห้าม rm แต่ให้ย้ายเข้า .archive/ แทน แล้วบังคับด้วย PreToolUse hook ที่ก็อปไปใช้ได้เลย มุมคนทำ model risk เรื่องกู้คืนได้

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

ในงานที่ผมทำมาสายวัดความเสี่ยงของโมเดล (model risk) มีหลักหนึ่งที่ใช้กับทุกระบบอัตโนมัติ: แยกการกระทำที่ "ย้อนกลับได้" ออกจากการกระทำที่ "ย้อนกลับไม่ได้" แล้วออกแบบให้ของที่ย้อนไม่ได้ กลายเป็นย้อนได้ก่อนจะปล่อยให้มันเกิดเอง บทความนี้คือวิธีทำข้อนั้นให้เป็นจริงใน Claude Code ด้วย hook สั้น ๆ ที่ก็อปไปวางแล้วใช้ได้เลย

ทำไมต้องโฟกัสที่ "กู้กลับได้" ไม่ใช่แค่ "อย่าให้พลาด"

ก่อนหน้านี้ผมเขียนเรื่อง ทำไมต้องรัน AI agent ใน Sandbox ไว้ว่า คำถามที่ถูกไม่ใช่ "มันจะพลาดไหม" แต่คือ "ถ้าพลาด วงความเสียหายกว้างแค่ไหน และกู้กลับได้เร็วแค่ไหน" — Sandbox ตอบครึ่งแรกคือ จำกัดวงความเสียหาย (blast radius) ส่วนบทความนี้ตอบครึ่งหลังคือ ทำให้กู้กลับได้ (recoverability)

ทั้งสองอย่างนี้คนละชั้นกัน สมมติว่าคุณให้ agent ทำงานในโฟลเดอร์โปรเจกต์จริง (ไม่ได้อยู่ในกล่อง) หรืออยู่ในกล่องแต่ mount โฟลเดอร์งานเข้าไปด้วย ถ้ามันสั่ง rm ผิดที่ ไฟล์งานของคุณเองก็หายจริง Sandbox กันไม่ให้ลามออกไปแตะไฟล์อื่นบนเครื่องได้ก็จริง แต่ ไม่ได้กัน agent จากการลบของในกล่องที่คุณตั้งใจให้มันทำงาน ตรงนี้แหละที่ต้องมีชั้นกู้คืนเพิ่ม

กฎจากงานวัดความเสี่ยง: เวลาประเมินระบบอัตโนมัติ ผมจะไล่ทุกการกระทำลงในสองถัง — ถังที่กด undo ได้ กับถังที่กดไม่ได้ ของในถังแรกปล่อยให้ทำเองได้สบายใจ ส่วนของในถังที่สอง (ลบไฟล์, ส่งเงิน, ยิง API ที่แก้ข้อมูล production) ต้องมีด่านหรือกลไกที่ทำให้มัน "ย้อนได้" เสียก่อน ไม่งั้นห้ามปล่อยให้อัตโนมัติเด็ดขาด

กฎง่าย ๆ ที่ผมใช้: ห้าม rm — ย้ายเข้า .archive/ แทน

หัวใจของวิธีนี้เรียบง่ายมากจนหลายคนมองข้าม: เลิกลบ แล้วเปลี่ยนเป็นย้าย แทนที่จะให้ agent (หรือแม้แต่ตัวเราเอง) สั่ง rm ให้เปลี่ยนเป็น mv ไปไว้ในถังกู้คืนที่มีวันที่กำกับ เช่นโฟลเดอร์ .archive/2026-07-26/ โครงสร้างเดิมของไฟล์ยังอยู่ครบ อยากได้คืนก็ mv กลับมา อยากเคลียร์จริง ๆ ค่อยมาตัดสินใจลบ .archive/ ทีหลังตอนที่เราสติครบ ไม่ใช่ตอนที่ agent กำลังวิ่งเอง

ข้อดีของการทำแบบนี้ในเชิงคนทำ risk มีสามข้อ:

  • กู้คืนได้ทันที — ไฟล์ไม่ได้หาย แค่ย้ายที่ ความผิดพลาดกลายเป็นเรื่องเสียเวลาห้านาที ไม่ใช่เรื่องเสียงานทั้งวัน
  • มี audit trail — โฟลเดอร์ .archive/ ที่แบ่งตามวันที่ คือบันทึกว่า "อะไรถูกสั่งลบเมื่อไหร่" ย้อนไปดูได้ ต่างจาก rm ที่ลบแล้วเงียบหาย ไม่เหลือร่องรอย
  • ทำซ้ำได้และอธิบายได้ — กฎมีข้อเดียว จำง่าย บังคับด้วยเครื่องได้ ไม่ต้องหวังว่าทุกคน (หรือทุก agent) จะจำได้เอง

ปัญหาเดียวคือ กฎที่อาศัย "ความจำ" หรือ "ความตั้งใจดี" ไม่เคยรอด agent ที่รันเองไม่ได้อ่าน CLAUDE.md ทุกบรรทัดทุกครั้ง และถึงอ่าน มันก็อาจพลาดได้ กฎแบบนี้ต้อง บังคับด้วยเครื่อง ถึงจะเชื่อถือได้จริง — ซึ่งพอดีที่ Claude Code มี hook ให้ทำตรงนี้

ทำจริง: PreToolUse hook ที่บล็อก rm ให้อัตโนมัติ

พอแล้วสำหรับหลักการ มาดูของจริง Claude Code มี hook ชื่อ PreToolUse ที่จะถูกเรียก ก่อน ที่ tool ทุกตัวจะรัน — รวมถึง tool Bash ที่ agent ใช้สั่ง shell ไอเดียคือ ดักทุกคำสั่ง Bash ก่อนมันรัน ถ้าเจอ rm ให้ปฏิเสธแล้วบอก agent ว่าให้ไปใช้วิธีย้ายเข้า .archive/ แทน

เริ่มจากลงทะเบียน hook ใน .claude/settings.json ของโปรเจกต์:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard_rm.py\""
          }
        ]
      }
    ]
  }
}

จากนั้นสร้างสคริปต์ hook ที่ .claude/hooks/guard_rm.py — มันจะอ่านคำสั่งที่ agent กำลังจะรันจาก stdin ตรวจว่ามี rm ไหม ถ้ามีก็ปฏิเสธพร้อมบอกเหตุผล:

#!/usr/bin/env python3
"""PreToolUse hook: บล็อกคำสั่ง rm แล้วบอก agent ให้ย้ายไฟล์เข้า .archive/ แทน"""
import json, re, sys

data = json.load(sys.stdin)
command = data.get("tool_input", {}).get("command", "")

# จับ rm ที่เป็น "คำสั่ง" จริง: ต้นบรรทัด หรือหลังตัวคั่นคำสั่ง ; && ||  |
if re.search(r"(^|[;&|]\s*)rm(\s|$)", command):
    reason = (
        "โปรเจกต์นี้ห้ามใช้ rm — การลบไฟล์ต้องกู้คืนได้เสมอ "
        "ให้ย้ายไฟล์เข้าถังกู้คืนที่มีวันที่กำกับแทน เช่น:\n"
        "  mkdir -p .archive/$(date +%F) && mv <target> .archive/$(date +%F)/\n"
        "ถ้าตั้งใจจะเคลียร์ถังกู้คืนจริง ค่อยให้คนเป็นคนตัดสินใจลบ .archive/ เอง"
    )
    print(json.dumps({
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": reason
        }
    }))
    sys.exit(0)

# ไม่ใช่ rm → ไม่ตัดสินใจอะไร ปล่อยให้ flow ปกติทำงานต่อ
sys.exit(0)

แค่นี้ก็ใช้งานได้แล้ว ครั้งต่อไปที่ agent พยายามสั่ง rm something hook จะเด้งขึ้นมาปฏิเสธ แล้ว Claude Code จะส่งข้อความใน permissionDecisionReason กลับไปให้ agent อ่าน — มันจึงรู้ทันทีว่าต้องเปลี่ยนไปใช้ mv เข้า .archive/ ไม่ใช่แค่โดนบล็อกเฉย ๆ แล้วงง

มุมคนทำ model validation: สังเกตว่ากฎ "ห้าม rm" เปลี่ยนจากตัวหนังสือใน CLAUDE.md (ที่อาศัยความร่วมมือ) มาเป็น control ที่ บังคับด้วยเครื่อง (ที่ไม่ต้องพึ่งความจำใคร) นี่คือความต่างระหว่าง "นโยบายบนกระดาษ" กับ "control ที่ตรวจสอบได้จริง" — อันหลังคือมาตรฐานเดียวกับที่เราต้องส่งให้ผู้ตรวจดูในงานแบงก์

อ่าน hook ทีละบรรทัดว่ามันกันอะไร

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

  • data = json.load(sys.stdin)Claude Code ส่งข้อมูลของ tool ที่กำลังจะรันเข้ามาทาง stdin เป็น JSON เราแค่รับมาอ่าน
  • data["tool_input"]["command"] — สำหรับ tool Bash คำสั่ง shell จริงที่ agent จะรันอยู่ในฟิลด์นี้ นี่คือสิ่งที่เราต้องตรวจ
  • re.search(r"(^|[;&|]\s*)rm(\s|$)", command) — จับ rm เฉพาะตอนที่มันเป็น คำสั่ง จริง คือขึ้นต้นบรรทัด หรือมาหลังตัวคั่นคำสั่งอย่าง ;, &&, | เจตนาคือไม่ให้ไปเผลอจับคำที่บังเอิญมี "rm" อยู่ข้างใน (เช่น ชื่อไฟล์หรือ path)
  • permissionDecision: "deny" — นี่คือรูปแบบปัจจุบันที่ hook ใช้ "ปฏิเสธ" tool call ส่วน permissionDecisionReason คือข้อความที่จะถูกส่งกลับไปให้ agent อ่าน เราจึงใส่ "วิธีที่ถูก" ลงไปเลย ไม่ใช่แค่คำว่าห้าม
  • sys.exit(0) ทั้งสองทาง — ออกด้วยโค้ด 0 เพราะเรา "ตัดสินใจ" ผ่าน JSON แล้ว (อีกทางเลือกหนึ่งคือออกด้วยโค้ด 2 แล้วเขียนเหตุผลลง stderr ซึ่งก็บล็อกได้เหมือนกัน แต่แบบ JSON คุมข้อความได้ชัดกว่า)

จุดที่ต้องรู้เกี่ยวกับ path: ในไฟล์ตั้งค่า $CLAUDE_PROJECT_DIR จะถูกแทนเป็น root ของโปรเจกต์ให้อัตโนมัติ ทำให้ hook ทำงานไม่ว่าจะรันจากโฟลเดอร์ย่อยไหน แต่ เครื่องหมาย ~ ไม่ถูกขยายให้ ถ้าจะอ้าง home ให้ใช้ path เต็มหรือ $HOME แทน

ข้อจำกัดที่ต้องรู้ (อย่าเข้าใจผิดว่านี่คือกำแพงกันทุกอย่าง)

จะให้แฟร์ วิธีนี้ไม่ใช่ยาครอบจักรวาล และผมอยากพูดถึงข้อจำกัดตรง ๆ ตามสไตล์คนทำ risk ที่ไม่ขายฝัน:

  • regex ตัวอย่างจับได้แค่กรณีตรงไปตรงมา — ถ้า agent เลี่ยงด้วยวิธีอ้อม เช่น เขียนสคริปต์แล้วสั่งลบข้างใน หรือใช้คำสั่งลบตัวอื่นอย่าง rmdir, find ... -delete, shred hook ตัวนี้ยังไม่ครอบ ของจริงต้องขยาย pattern ให้ครอบคำสั่งทำลายข้อมูลที่คุณห่วง และทดสอบว่ามันจับได้จริงก่อนวางใจ
  • hook ไม่ใช่ security boundary — มันคือ "ราวกันตก" ที่กันความผิดพลาดโดยไม่ตั้งใจ (accident) ได้ดี แต่ไม่ได้ออกแบบมากันคนหรือ agent ที่ จงใจ จะหลบ ถ้าโจทย์คือกันเจตนาร้าย ต้องใช้การจำกัดสิทธิ์ระดับระบบ (เช่น รันใน container ที่ mount แบบ read-only) ควบคู่ไปด้วย
  • ยังต้องมี Sandbox อยู่ดี — ชั้นกู้คืนนี้เสริม ไม่ได้แทน การจำกัด blast radius กันความผิดพลาดคนละแบบ ใช้คู่กันถึงจะครบ

เส้นที่ห้ามข้าม: อย่าเอา hook แบบนี้ไปเป็นข้ออ้างว่า "ปลอดภัยแล้ว เลยปล่อย agent รันอะไรก็ได้บนเครื่องจริง" ราวกันตกช่วยลดโอกาสตกบันได แต่ไม่ได้แปลว่ายืนริมเหวได้ หลักการเดิมยังอยู่ — จำกัดสิทธิ์เท่าที่จำเป็น แล้วค่อยเพิ่มชั้นกู้คืนซ้อนไปอีก เรื่องกรอบกำกับ agent ที่รันเองอย่างเป็นระบบผมเขียนต่อไว้ใน เช็คลิสต์กำกับ AI agent ในงานการเงิน

ทำไมมันสำคัญกับเรา: งานที่กู้คืนได้ = งานที่กล้าปล่อยให้อัตโนมัติ พอทุกการลบกลายเป็นแค่การย้ายที่ ความกลัวที่ทำให้เราต้องนั่งเฝ้า agent ทุกวินาทีก็หายไป เราเลยปล่อยให้มันทำงานเองได้ยาวขึ้นและมากขึ้นโดยไม่ต้องแลกกับความเสี่ยงที่ย้อนกลับไม่ได้ นี่คือหลักเดียวกับที่ทำให้ ด่านกดอนุมัติของคน (human-in-the-loop) กับ hook ทำงานร่วมกันได้ดี — ด่านหนึ่งให้คนตัดสิน อีกด่านบังคับกฎให้อัตโนมัติ

ภาพรวม

AI agent ยุคนี้ลบไฟล์จริงได้ และยิ่งเราปล่อยให้มันทำงานเองมากเท่าไหร่ คำถามเรื่อง "ถ้าลบผิด กู้กลับได้ไหม" ก็ยิ่งสำคัญ วิธีที่ผมใช้เรียบง่าย — เลิก rm เปลี่ยนเป็น mv เข้า .archive/ ที่มีวันที่กำกับ แล้วบังคับกฎนั้นด้วย PreToolUse hook ใน Claude Code เพื่อไม่ให้มันขึ้นกับความจำของใคร ต้นทุนคือสคริปต์ไม่กี่บรรทัด แต่สิ่งที่ได้กลับมาคือความมั่นใจว่าความผิดพลาดจะเป็นแค่ "เสียเวลา" ไม่ใช่ "เสียของ"

ตรงนี้คือชั้นเดียวที่ก็อปไปตั้งต้นได้ทันที ส่วนของจริงที่ผมใช้ — ชุด hook ที่ครอบคำสั่งทำลายข้อมูลหลายแบบพร้อม test ว่าจับได้จริง, การผูก .archive/ เข้ากับ session ritual ให้เคลียร์อัตโนมัติเมื่อถึงเวลา, และการวาง hook พวกนี้ให้เข้ากับ workflow agent ที่รันเองในงานสายการเงินโดยเฉพาะ — ผมประกอบเป็นแล็บให้ลงมือทำตามทีละขั้นในคอร์ส Claude Code ถ้าอยากได้ทั้งชุด hook ไปวางทับงานตัวเอง เรื่องการวาง hook ให้เข้ากับพิธีเปิด-ปิด session ผมเขียนต่อไว้ใน session ritual กับ hooks ใน Claude Code

อ่านต่อ