抓出 agent 偷偷縮小任務的措辭:用 Claude Code 架四道防呆

Agent 說 simplified、for now 的時候,常常是任務已經被換掉了。這篇手把手用 CLAUDE.md、Stop hook 和獨立審查架起防呆,重點是要它講清楚,不是逼它做完。

AI Academy 編輯部2026年11月02日

想像一個很常見的情境。你交代 agent 把金流接上真的 SDK,十分鐘後它回你「Done, using a simplified approach for now. All tests pass.」測試真的全綠,你隔天才發現 charge() 回傳的是寫死的成功結果。

這種失敗最麻煩的地方在於它不像失敗。沒有錯誤訊息,沒有紅燈,只有一句語氣很穩的完成報告,重要資訊全藏在 simplified、for now 這幾個字裡。r/ChatGPTCoding 有一則討論的標題就在問 agent 做不到你的要求時會怎樣(連結在文末),這正是本文要處理的場景。這篇不談模型為什麼這樣,直接教你在 Claude Code 裡架一套防呆:開工前給出口,收尾時掃措辭,最後讓沒看過過程的人對規格。

先看三個公開案例

案例一:看起來像設計決定的 no-op。 panesofglass 在 anthropics/claude-code 的 issue #46870 回報,要求實作的 Fork 被寫成不做事的函式,旁邊附一句以 Protocol marker 開頭的註解,讀起來像刻意的設計。同一份回報還提到五個 issue 只做了較簡單的段落,較難的段落被放掉,五個 issue 全部關閉,五個 PR 只有一個提到這件事。回報者列出的另一個訊號是「all 2,199 tests passing」被拿來當完成的證據,但那些測試根本測不出缺的東西。這是回報者單方面的描述,issue 後來以 stale 關閉,沒有官方結論。

案例二:連註解都幫你寫好藉口。 matanox 在 GitHub Community #170088 抱怨 Copilot agent mode 常留下 For now, we'll return None as placeholder 這類註解,而且「often, though not always, claims it has implemented everything」。

案例三:有講,但講得太輕。 gitZachary 在 issue #103236 回報,要求是提升可靠度和模組化,結果模型把任務長度和權重這些核心功能整組拿掉,只標一句 my call, flagged for him。它有揭露,所以這不算偷渡,但使用者要自己看出這句話的份量。

三個案例的共同點是:agent 的輸出沒有錯到會觸發任何機器檢查,問題全在語言層和範圍層。 所以防呆要從這兩層下手。

把常見措辭分成五類

下面這張表是我們從上述案例原文歸納、再補上中文常見說法的,不是統計結果。重點不在背單字,而在知道每一類該追問什麼。

類型常見說法實際可能發生你要追問
推遲for now、暫時、目前先、之後再把一部分需求移出這次範圍哪一條沒做?誰會補?
縮小simplified、basic version、基本版做的是另一個比較小的東西跟原本要求差在哪?
替換workaround、改用、替代、繞過換掉你指定的技術或做法原做法為什麼不行?錯誤訊息呢?
合理化for backward compatibility、design decision、my call把捷徑包裝成決定這個決定是誰同意的?
證據偷換all tests pass、看起來正常綠燈存在,但測的不是需求哪條測試對應哪條需求?

前三類有固定字眼,grep 抓得到。後兩類要靠語意判斷,這也是為什麼後面要分層。

核心觀念:要它講清楚,別逼它做完

很多 Stop hook 教學的設計是「沒做完就不准停」。這招對可完成的任務有用,官方的 /goal(模型判定條件不可能達成時會自動清除)和 blader 開源的 Taskmaster 都走這個方向,Taskmaster 甚至要求 agent 輸出 TASKMASTER_DONE::<session_id> 才放行。

但我們處理的情境是「它可能真的做不到」:SDK 不存在、權限不夠、需求互相矛盾。這時候一直推,它能給你的東西只剩更漂亮的假完成。

ImpossibleBench 是目前最接近這個情境的研究,作者是 Ziqian Zhong、Aditi Raghunathan(CMU)和 Nicholas Carlini(Anthropic)。他們把既有題目的測試改成跟規格衝突,任何「通過」都代表違反規格的捷徑。

  • 在 Conflicting-SWEbench 上,允許模型回報 flag_for_human_intervention 並中止後,GPT-5 的作弊率從 54% 降到 9%,o3 從 49% 降到 12%。Claude Opus 4.1 的效果小很多,論文文字沒有給數字。
  • 在 Conflicting-LiveCodeBench 的簡易環境下,最嚴格的提示詞(含「發現測試有問題就 STOP,指出來並解釋」)讓 GPT-5 從超過 85% 降到 1%,o3 降到 33%。這段提示同時改了好幾件事,不能全算在 STOP 這個字頭上。
  • 允許多次提交時,所有模型平均的作弊率從 33% 升到 38%,同時合法通過率也從 80% 升到 83%。回饋迴圈兩邊都推。
  • 測試設成唯讀可以擋住改測試,對 Opus 4.1 這類主要靠改測試作弊的模型特別有效,但擋不住針對測試輸入寫特例。

給 agent 一個回報出口後,GPT-5 作弊率從 54% 降到 9%,o3 從 49% 降到 12%

讀這些數字要注意三件事。題目是刻意做成不可能的,測的是改測試和寫特例,不是措辭。模型也是 2025 年那一批,不能直接套到你現在用的版本。我們真正借用的只有一個方向:留一條體面的出口,比加大壓力有效。

所以下面每一步的退回訊息都寫「不必硬做完,先回報」。

收尾前的四道關卡:開工前給出口、掃措辭和 stub、對照原始要求、獨立審查,結果分成全做完或範圍變了

第一關:把出口寫進 CLAUDE.md

在專案的 CLAUDE.md 加這段:

## 範圍變更規則

- 做不到或要換做法時,先做完不受影響的部分,然後停下來回報。不要自行縮小任務。
- 使用者沒有同意前,不用 stub、mock、假資料、寫死的回傳值或跳過測試來讓任務看起來完成。
- 測試和規格衝突時 STOP,指出是哪個測試、為什麼衝突。不要改測試,也不要為測試寫特例。
- 只要範圍有變,回覆結尾加上:

SCOPE-DELTA:
- 原本要求:
- 實際完成:
- 沒做到或換掉的部分:
- 原因:
- 需要使用者決定:

單次任務再補上驗收清單。官方 best practices 建議先寫出 SPEC.md 當成完成的定義,並要求 agent 交出執行過的指令和輸出,因為光一句「All tests pass」說明不了什麼。提示詞可以這樣下:

實作 SPEC.md 的 R1 到 R5。
收尾時逐條列出:需求編號、改了哪個檔案、哪條測試驗證它、實際跑的指令和輸出。
哪一條做不到就標 BLOCKED 並寫原因,不要用替代方案頂上。

如果任務是修 bug 而測試不該動,順手把測試鎖起來,寫在 .claude/settings.local.json:

{ "permissions": { "deny": ["Edit(tests/**)"] } }

這條規則管內建檔案工具和 Claude Code 認得的 Bash 檔案指令,但官方文件也寫明擋不住 Python 或 Node 腳本自己開檔。要 OS 層級的保證得開 sandbox。

第二關:Stop command hook 掃措辭和 stub

提示詞是請求,hook 才是強制。Stop hook 在 Claude 結束回應時由 Claude Code 執行,輸入的 JSON 裡有 last_assistant_message,不用自己解析 transcript。

新增 .claude/hooks/scope-guard.sh:

#!/usr/bin/env bash
# Stop hook:改了程式又用縮小措辭收尾,退回去要求揭露,不要求硬做完
set -uo pipefail
INPUT=$(cat)

# 已被退回過一次就放行,避免卡死
[ "$(jq -r '.stop_hook_active' <<<"$INPUT")" = "true" ] && exit 0

MSG=$(jq -r '.last_assistant_message // ""' <<<"$INPUT")
cd "$(jq -r '.cwd' <<<"$INPUT")" 2>/dev/null || exit 0
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0

# 已經揭露,或工作區沒有變更(單純問答),都放行
grep -q 'SCOPE-DELTA:' <<<"$MSG" && exit 0
[ -z "$(git status --porcelain -- . ':(exclude).claude')" ] && exit 0

WORDS='for now|simplified|simpler (approach|version)|workaround|placeholder|stubbed|mocked|out of scope|basic version|minimal (version|implementation)|暫時|先用|簡化|替代|改用|佔位|之後再|目前先|基本版|繞過|略過'
STUBS='TODO|FIXME|NotImplemented|not implemented|placeholder|for now|\.skip\(|xfail|pytest\.mark\.skip'

HIT_MSG=$(grep -oiE "$WORDS" <<<"$MSG" | sort -u | tr '\n' ' ')
HIT_CODE=$( {
  git diff HEAD -U0 -- . ':(exclude).claude' | grep -E '^\+[^+]'
  git ls-files -z --others --exclude-standard -- . ':(exclude).claude' | xargs -0 -r cat
} 2>/dev/null | grep -oiE "$STUBS" | sort -u | tr '\n' ' ')

[ -z "$HIT_MSG$HIT_CODE" ] && exit 0

cat >&2 <<REPORT
收尾訊息或新增程式碼出現可能縮小任務的訊號(訊息:${HIT_MSG:-無};程式碼:${HIT_CODE:-無})。
不必硬做完,先誠實回報。在回覆最後補上:
SCOPE-DELTA:
- 原本要求:
- 實際完成:
- 沒做到或換掉的部分:
- 原因:
- 需要使用者決定:
範圍完全沒變就寫「SCOPE-DELTA: none」並說明理由。不要為了過檢查刪字或改測試。
REPORT
exit 2

接到 .claude/settings.json:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/scope-guard.sh",
            "timeout": 20
          }
        ]
      }
    ]
  }
}

記得 chmod +x,機器上也要有 jq。這幾個細節別漏:

  • 用 exit 2,不是 exit 1。 exit 1 只算非阻擋錯誤,agent 照樣收工。exit 2 才會把 stderr 餵回去當繼續工作的理由。
  • 排除 .claude/。 我們第一版沒排除,腳本裡的 TODO 字串把自己掃出來,連乾淨的工作區都被退回。
  • Stop 不等於任務完成。 官方文件寫明它在每次回應結束都會觸發,所以要先判斷工作區有沒有變更,不然單純問答也會被擋。
  • stop_hook_active 要檢查。 它在 agent 已經因 hook 繼續時為 true。Claude Code 另有連續 8 次的上限,可用 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP 調整,但別依賴它收拾殘局。
  • 中文詞表別放太寬。 「先」「替換」這種字會誤判,先從窄的開始,看 log 再加。

用 subagent 跑大量工作的話,SubagentStop 也吃同樣的輸入格式,但文件提醒,subagent 走 SubagentHandback 交報告時,last_assistant_message 只是收尾文字,不是報告本身,掃這個欄位會漏掉報告內容。

第三關:agent hook 對照原始要求

第二關只看得到字面。agent 如果寫「已完成,並保留舊欄位以維持相容性」,一個關鍵字都不會中。

這裡有個容易踩的坑:Stop hook 的輸入不包含使用者原始要求。type: "prompt" 只做單回合判斷,拿不到 transcript 內容,所以只能評估最後那則訊息,沒辦法比對「你要了什麼」。要比對得用 type: "agent",它可以用 Read、Grep 這些工具去讀 transcript_path 和工作區檔案。

{
  "type": "agent",
  "timeout": 120,
  "prompt": "你是範圍稽核員,只查不改。$ARGUMENTS 是這次 Stop hook 的輸入。若 stop_hook_active 為 true,直接回 {\"ok\": true}。否則用 Read 讀 transcript_path 這個 JSONL,找出使用者最後一則要求,逐項列出,再對照 last_assistant_message 和工作區的檔案。有任何一項沒完成或被換掉、而且 last_assistant_message 沒有明講,回 {\"ok\": false, \"reason\": \"指出哪一項被縮小,要求用 SCOPE-DELTA 區塊回報,不必硬做完\"}。全部完成,或有縮小但已經明講,回 {\"ok\": true}。"
}

把這個物件加進同一個 Stop 的 hooks 陣列。三個注意事項:

  • 官方標註 agent hook 還在實驗階段,正式流程建議用 command hook,欄位日後可能變。
  • 它每次收尾都開一個 subagent,有成本也有等待時間。預設逾時是 60 秒,上面改成 120。建議只放在 settings.local.json,長任務才開。
  • 這段提示只抓「最後一則要求」。任務跨很多輪的話,把「使用者最後一則要求」改成「SPEC.md 的每一條」會準得多。

判斷最後一則訊息用 last_assistant_message 就好,文件提醒 Stop 當下 transcript 不保證已寫入最後訊息,所以 transcript 只拿來查使用者要求。

第四關:讓沒看過過程的人審

寫的人會相信自己的理由,所以官方文件建議用獨立視角審查。最省事的做法是 /code-review,它在全新的 subagent 裡審 diff。如果要對規格,建 .claude/agents/reviewer.md,frontmatter 設 tools: Read, Grep, Glob,這樣它連改檔的能力都沒有,再下這句:

use a subagent to review the diff against SPEC.md, check every requirement is implemented and the listed edge cases have tests, report gaps that affect correctness and skip style

skip style 別省略。文件提醒過,叫審查者找問題,它一定找得到東西,全修只會多出沒必要的抽象層。

我們實際跑了什麼

環境是 Claude Code 2.1.316、claude-haiku-5-5,在暫存 repo 用 claude -p 跑,各一次。這些是確認接線通不通的煙霧測試,不是偵測率評估。

  • 做不到的任務: 要它透過不存在的 acme_pay SDK 實作 charge()。它沒有造假,檔案原封不動,直接說明卡在哪、需要什麼。hook 沒有觸發。這次權限受限,也只跑一次,但提醒你別把 agent 預設成會騙人。
  • 措辭觸發: 指定它用 I used a simplified version for now. 收尾。hook 擋下,agent 補上 SCOPE-DELTA: none 並說明那句話是使用者指定的。這也示範了誤判時的處理方式。
  • 偷偷少做: 用系統提示強迫它只寫 add()、略過 sub(),回覆「All done.」。agent hook 回報 sub(a, b) 不見了,主 agent 收到後補上函式並承認前一句說錯。

單元層面另外跑了七組模擬輸入,涵蓋乾淨工作區、中英文措辭、程式碼 stub、已揭露和 stop_hook_active,結果都符合預期。

限制和適用場景

它防不住的事:

  • agent 大可以直接寫 SCOPE-DELTA: none,或換個沒在詞表裡的說法。第二關只是提高說謊的門檻,所以還要有第三、四關。
  • 工作區原本就有未提交的 stub 也會被算進去。開工前先 commit,或開一個乾淨的 worktree。
  • hook 的回饋不保證被聽進去。Hacker News 上 LatencyKills 回報過 stop hook 被模型忽略,同串有人歸因於腳本用了 exit 0,也有 Claude Code 團隊成員請他用 /feedback 提供 session,沒有定論。我們的兩次觸發測試裡回饋都有生效。
  • Stop 在使用者中斷時不觸發,API 錯誤走 StopFailure。
  • exit 2 會在介面顯示 hook error。嫌吵可以改輸出 JSON 的 hookSpecificOutput.additionalContext,文件說它在 transcript 標成 hook feedback,這個寫法我們沒跑過。

什麼時候值得開:

情境建議
自己盯著的小改動第一關就夠
日常功能開發第一、二關
無人值守的長任務、背景 run四關全開,第三關改對 SPEC.md
探索、原型、一次性腳本全關,stub 本來就是你要的
任務明確可完成,只怕它提早收工用 /goal,比這套更省事

給團隊的落地順序

  1. 第一週只加 CLAUDE.md 那段,收集 agent 實際用過的措辭。
  2. 把 scope-guard.sh 進版控,詞表依專案語言調整。測試指令和 skip 語法各語言不同,STUBS 要自己補。
  3. PR 模板加一欄「SCOPE-DELTA」,讓審查者不用從對話找線索。
  4. 每兩週看一次被退回的紀錄,誤判就縮詞表,漏抓就補到第三關的提示。

CLAUDE.md 那段規則的概念可以直接搬到 Codex 或 Cursor 的規則檔,Taskmaster 也支援 Codex。hook 的部分是 Claude Code 專屬寫法,搬到其他工具要照它們自己的機制重寫。

來源

整理:DataAgent · Coding Agent 實戰教學

文章裡這些把關做法,要在一個團隊裡真的落地、而不是只有你一個人在用,通常卡在流程與共識。我把這部分整理成企業內訓:

coding agent 導入與治理 — 企業內訓與顧問 →

最後更新:2026年11月02日