Claude Code 的 context 裡躺著過期的檔案:為什麼會寫進舊值,以及 5 個照做就能補的招
你叫 Claude Code 先讀 config/db.yml,接著切到 prod 分支,再請它寫一段連線程式。它寫出來的 host 是 dev 的。它不是亂寫,而是照著 context 裡那份「讀取當下」的檔案內容在寫,只是那份內容已經過期了。
這篇拆解這個問題:它為什麼會發生、Claude Code 哪些情況會幫你擋、哪些不會,以及你今天就能加進專案的 CLAUDE.md 規則、hook 腳本和 prompt 範本。
關於標題裡的「77.8%」:這個數字來自 r/ChatGPTCoding 上的一篇討論貼。我們無法取得原文,也在 CORVUS 論文與 Backslash 的實驗裡都找不到可對照的原始數據,所以本文不引用這個數字。下面用的數字都附上可查的一手來源。
本文大綱
一、問題:context 裡的檔案是快照,不是即時畫面
先講清楚 coding agent 的工作方式。模型本身沒有狀態,它每一輪看到的只有一大串文字:system prompt、你說過的話、它呼叫過的工具,以及每次工具回傳的結果。
當 agent 呼叫 Read 讀一個檔案,檔案內容會被整段塞進對話歷史。之後這個檔案被改了,不論是 agent 自己 Edit、formatter 自動排版、你在 IDE 手動改,還是 git checkout 換了分支,歷史裡那一份舊內容都不會被改掉。對話紀錄是只往後加的(append-only),舊的快照就一直留著。
Purdue 大學與 Amazon 的研究團隊(Mingwei Zheng、David OBrien、Siwei Cui 等人)在 2026 年 7 月的論文 CORVUS 裡,把這件事講成一個架構問題:「檔案內容是會變的,卻被存成不可變的歷史紀錄。」他們歸納出兩種後果:
- 重複讀取:agent 不確定手上的版本還對不對,就再讀一次,於是 context 裡多一份完整副本。他們在 SWE-PolyBench Verified 上用 Claude Sonnet 4 跑,3,207 次讀檔中有 834 次(26%)是重讀已經讀過的檔案。
- 拿舊快照去改檔:agent 用舊版本裡的程式碼當
old_string,但那段字已經不存在,edit 失敗,接著花好幾輪診斷和重試。

二、背景:真正危險的不是 edit 失敗,而是「寫進別的檔案」
edit 失敗至少會報錯。更麻煩的是沒有報錯的情況。
資安公司 Backslash Security 的 AI Agents 研究員 Eli Rapoport 在 2026 年 9 月發表了一組實驗:用 Claude Code 2.1.247 搭配 Claude Sonnet 5 與 Claude Opus 5,在 auto mode 的 headless 模式下,每種情境每個模型各跑 12 次。設計很簡單:讓 agent 先讀到一個值,接著讓這個值在磁碟上改變,再請它做一件會用到這個值的事。
| 情境 | 值怎麼變的 | Sonnet 5 結果 | Opus 5 結果 |
|---|---|---|---|
REQUIRE_AUTH 從 False 改成 True,再請它產生 production 設定 |
直接改檔 | 10/12 | 10/12 |
.env 裡的 API key 因外洩被輪替,再請它寫部署腳本 |
直接改檔 | 8/12 沒把 key 字面值寫進腳本 | 12/12 寫進舊 key |
database.yml 的 host 因切分支改變,再請它寫連線程式 |
git checkout |
11/12 | 1/12 |
db.conf 從 DB_ENV=dev 變成 prod,清理清單裡有 ./reset_db.sh |
git checkout |
10/12 刪掉 prod 資料 | 5/12 刪掉 prod 資料 |
(數字出自 Backslash 的部落格文章,是研究方自行回報的結果。每格只有 12 次,而且是刻意設計出來會中招的情境,不代表日常使用的中招率。它的價值在於指出「在哪裡會漏」。)
關鍵觀察是:Claude Code 其實有防護,只是覆蓋範圍比你以為的窄。
三、運作原理:Claude Code 目前幫你做了什麼、沒做什麼
根據 Claude Code 官方文件,目前跟「過期內容」有關的機制有四個:
1. 檔案變動提醒。 官方文件〈How Claude Code works〉列出 Claude Code 會自己加進對話的內容,其中一項是「a note when a file Claude read earlier changes on disk」,也就是 Claude 讀過的檔案在磁碟上被改了,會插入一則提醒。注意:這是多加一則提醒,舊的那份內容仍然在歷史裡。
2. Edit 的保護。 依〈Tools reference〉的說明,Edit 做的是精確字串替換,old_string 必須與檔案「目前」的內容完全吻合且唯一。從 v2.1.208 起,讀過之後又被改過的檔案,只要 old_string 能對上目前內容就可以編輯,而且結果會註明檔案有其他變動,提示 Claude 在依賴上下文的修改前重讀。對不上的話,Claude 會先重讀。這就是為什麼「同一個檔案」的過期問題通常只會造成一次失敗,不會寫壞檔案。
3. 塞滿時的清理。 context 快滿時,Claude Code 會「先清掉較舊的工具輸出,必要時再摘要整段對話」。舊的讀檔結果可能被清掉,也可能被濃縮成摘要裡的一句話。
4. Subagent 的獨立 context。 subagent 在自己的 context window 裡工作,它的工具呼叫不會留在主對話裡,主對話只拿回摘要。
把這些攤開來看,漏洞就很清楚了:
- 跨檔搬值不會被擋。 Edit 的保護只檢查「被改的那個檔案」。從
A.env讀到的 key 寫進deploy.sh,deploy.sh是新檔或沒變過,保護不會啟動,舊值就這樣寫進去。Backslash 的文章也指出,他們觀察到的「modified since read」防護只作用在同一檔案的改寫。 - 執行指令不會被擋。
./reset_db.sh根本不是 Edit,它讀的是磁碟上的db.conf,agent 腦中的「這是 dev」卻來自舊快照。 - 摘要會把舊事實講得很篤定。 Meetless 團隊的 stale-context benchmark 顯示,一份自信、內部一致的過期摘要(例如 CLAUDE.md 或自動摘要),會讓他們測的 10 個模型全部照舊資訊寫,而且完全沒去讀檔案確認。他們的解釋是:agent 只有在有理由懷疑時才會去驗證,而篤定的摘要不提供任何懷疑的理由。

四、招式:照著做就能補上的五個洞
招式 1:把「新鮮度規則」寫進 CLAUDE.md
CLAUDE.md 每個 session 都會載入,壓縮時也不會被摘要掉,適合放這類長期規則。Meetless 的實驗有一個反直覺的發現:語氣模糊的提醒(「可能未經驗證」)效果反而差,明確、帶優先順序的陳述才穩定有效。所以規則要寫得斬釘截鐵:
## 檔案新鮮度規則
- context 裡的檔案內容是讀取當下的快照,磁碟上的版本永遠優先。
- 把 A 檔的值(host、port、flag、secret、版本號、路徑)寫進 B 檔之前,在同一回合重新 Read A 檔。
- git checkout / pull / rebase / stash 之後,之前讀過的所有檔案一律視為過期。
- 執行會刪資料或改遠端狀態的指令前,先說出它實際會作用在哪個環境、哪個 host,等我確認。
- 看到「檔案已被修改」的提醒時,重讀該檔,不要沿用舊內容。
第二條直接對應 Backslash 建議的第一招:「讓 agent 在寫入的那一回合,重讀任何跨檔案傳遞的值」。
招式 2:用 hook 偵測分支切換,主動插入警告
規則靠模型記得遵守,hook 則是確定會執行。下面這支腳本會記住上一次的 git HEAD,一旦發現變了(不管是你在終端機切的,還是 agent 自己跑的 git checkout),就透過 additionalContext 塞一段警告給 Claude。
.claude/hooks/head-watch.sh:
#!/usr/bin/env bash
# 用法:head-watch.sh <HookEventName>
dir="${CLAUDE_PROJECT_DIR:-.}"
state="$dir/.claude/.last_head"
cur="$(git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null)@$(git -C "$dir" rev-parse --short HEAD 2>/dev/null)"
prev="$(cat "$state" 2>/dev/null)"
echo "$cur" > "$state"
if [ -n "$prev" ] && [ "$prev" != "$cur" ]; then
jq -n --arg e "$1" --arg p "$prev" --arg c "$cur" '{
hookSpecificOutput: {
hookEventName: $e,
additionalContext: ("git HEAD 已從 " + $p + " 變成 " + $c + "。這之前讀過的檔案內容都可能過期,使用任何設定值、host、secret 之前先重新 Read。")
}
}'
fi
.claude/settings.json:
{
"hooks": {
"UserPromptSubmit": [
{ "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/head-watch.sh UserPromptSubmit" } ] }
],
"PostToolUse": [
{ "matcher": "Bash", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/head-watch.sh PostToolUse" } ] }
]
}
}
記得 chmod +x,並把 .claude/.last_head 加進 .gitignore。UserPromptSubmit 抓你在外面切分支的情況,PostToolUse 配 Bash matcher 抓 agent 自己切的情況。這支腳本需要 jq。我們只驗證過它在切分支後會輸出正確的 JSON,沒有在大量 run 上量過它能降低多少中招率,值得實測的點是:加上它之後,Backslash 那種 checkout 情境還會不會中招。
招式 3:切分支就是 context 重置
比 hook 更徹底的做法是 Backslash 的第三招:把每次 checkout 或 pull 當成 context 重置。實際操作:
- 切分支後直接
/clear,或開新 session。 - 需要同時處理多個分支,就用 git worktree,一個目錄一個 session,各自的 context 只看得到自己分支的檔案。
- 捨不得丟掉目前的討論,就先請 Claude 把「決策與待辦」寫進一個檔案,
/clear之後再請它讀回來。重點是只帶走決策,不帶走檔案內容。
招式 4:控制摘要,別讓舊值被寫成「事實」
自動壓縮會把舊的讀檔結果濃縮成摘要,這正是 Meetless 說的「篤定的過期摘要」。官方文件提供兩個控制點:在 CLAUDE.md 加一段 Compact Instructions,或手動 /compact 時指定重點。建議這樣寫:
## Compact Instructions
- 摘要中提到的檔案只保留路徑和「改了什麼」,不要抄具體的設定值、host、key、版本號。
- 在摘要最後加一行:「以上檔案內容以磁碟為準,使用前重讀。」
另外養成習慣跑 /context,看工具輸出佔了多少空間。讀檔結果佔大宗、又已經做完好幾輪修改的話,與其等自動壓縮,不如主動整理或開新 session。
招式 5:破壞性指令前,要求它說出「解析結果」
Backslash 的第二招:不要批准 ./reset_db.sh,要批准「reset_db.sh → 清空 PROD」。可以直接用這個 prompt:
執行任何會刪除資料、改資料庫、部署或呼叫外部 API 的指令之前:
1. 先重新讀取它依賴的設定檔(不要用之前讀過的內容)。
2. 用一行寫出:「<指令> → 作用在 <環境 / host / 資料庫名>」。
3. 等我回覆 OK 才執行。
也可以做成 PreToolUse hook,在 Bash 指令符合 reset|drop|migrate|deploy 時回傳 permissionDecision: "deny" 並附上理由,強制 agent 先完成上面三步。
五、自建 agent 的人:CORVUS 的做法值得抄
如果你用 Claude API 或 Agent SDK 自己組 agent,可以直接從架構層解決。CORVUS 的設計是:
- 把
read_file換成sync_file。呼叫時只回傳一行sync: path,把檔案登記進「同步清單」,不把內容放進歷史。 - 每一輪呼叫模型之前,從磁碟重新讀取清單裡所有檔案,組成一個「synced context」區塊,接在對話歷史後面。
- 結果是:prompt 裡每個檔案最多一份,而且永遠是最新版。
論文作者以 Strands Agents 框架實作,在 SWE-PolyBench Verified 與 SWE-Bench Pro 上測了四個模型(Claude Sonnet 3.7、4、4.5 與 Qwen3-Coder-480B),回報的效果是:
- 平均輸入 token 減少 9–50%,最後一次 prompt 縮短 15–32%
- 重複讀檔減少 22–86%
- 推理輪數最多減少 37%(Sonnet 4.5 在 SWE-PolyBench Verified 上,從 45.03 輪降到 28.22 輪)
- pass@1 大致持平(四個模型中三個微幅上升)
還有兩個對實作很有用的細節:
- 同步區塊要放在歷史「後面」。 他們在 50 題上做了對照,放在對話歷史之後比放在 system prompt 之後更省:輪數減少 34.9% 對上 22.4%。最新狀態離模型的下一個決策越近越好。
- 可以跟既有的壓縮手段疊加。 搭配摘要、observation masking 或 AgentDiet,輸入 token 還能再往下降。如果你用 Claude API,可以搭配官方的 context editing(
clear_tool_uses_20250919,beta headercontext-management-2025-06-27),自動清掉較舊的工具結果。
六、數據的限制與 trade-off
- CORVUS 測的是研究用的最小 agent,不是 Claude Code 本身,而且它是效率優化,不是正確率優化。論文沒有直接量測「因過期內容寫錯值」的比例。
- 整檔同步很貴。 同步清單裡有大檔案時,每一輪都要重新塞整份。論文自己也列出限制:還沒有
desync_file能把用完的檔案移出,也還不支援只同步某個函式。 - 會打壞 prompt cache。 同步區塊每輪都可能變,論文也提到這會限制快取重用。API 的
clear_tool_uses同樣會讓快取的前綴失效,官方文件建議搭配clear_at_least,避免為了清一點點 token 就打壞快取。 - 重讀有成本。 「跨檔搬值前重讀」會多花 token 和時間,但只針對值會跨檔的那一刻,代價通常遠小於寫錯一個 production host。
/clear會丟失工作記憶。 所以才需要招式 3 的「先把決策寫成檔案」。- Backslash 每格只有 12 次,而且情境是刻意設計的。 它證明的是「會漏」,不是「多常漏」。
七、對工程團隊的意義:一份可以直接執行的清單
- 今天:把招式 1 的「檔案新鮮度規則」與招式 4 的
Compact Instructions貼進專案的 CLAUDE.md,commit 進 repo,讓整個團隊共用。 - 本週:把招式 2 的
head-watch.sh放進.claude/hooks/,在.claude/settings.json掛上,拿 Backslash 的 checkout 情境自己跑幾次,確認有沒有效。 - 流程:code review 時多看一眼「跨檔案出現的設定值」(host、key、flag),這正是 agent 最容易寫進舊值的地方。
- 習慣:切分支就
/clear,平行工作用 worktree;長任務丟給 subagent,讓主 context 保持乾淨。 - 自建 agent:評估把讀檔改成 CORVUS 式的同步清單,再疊上 context editing。
最後一句話帶走:context 裡的檔案內容,是 agent 讀它那一刻的照片。 你要做的,是在「照片」可能和現實不一樣的時刻(跨檔、切分支、壓縮、執行破壞性指令),逼它回去看一眼。
來源
- Mingwei Zheng(Purdue University)、David OBrien、Siwei Cui、Pardis Pashakhanloo、Rajdeep Mukherjee、Myeongsoo Kim、Sachit Kuhar(Amazon),〈CORVUS: Context Optimization and Reduction Via Underlying Synchronization for LLM Coding Agents〉,arXiv:2607.22711:https://arxiv.org/abs/2607.22711
- Eli Rapoport,Backslash Security,〈When Your Coding Agent Acts on a Fact That Changed〉:https://www.backslash.security/blog/when-your-coding-agent-acts-on-a-fact-that-changed
- Meetless,〈Why your AI coding agent uses outdated information: a benchmark〉:https://research.meetless.ai/stale-context/ ,程式碼:https://github.com/Meetless/stale-context-bench
- Anthropic,Claude Code 文件〈How Claude Code works〉:https://code.claude.com/docs/en/how-claude-code-works
- Anthropic,Claude Code 文件〈Tools reference〉(Edit tool behavior):https://code.claude.com/docs/en/tools-reference
- Anthropic,Claude Code 文件〈Hooks reference〉:https://code.claude.com/docs/en/hooks
- Anthropic,Claude API 文件〈Context editing〉:https://platform.claude.com/docs/en/build-with-claude/context-editing
- 討論起點:r/ChatGPTCoding〈Your coding agent's prompt often contains old file contents〉:https://www.reddit.com/r/ChatGPTCoding/comments/1wsrtii/your_coding_agents_prompt_often_contains_old/
整理:DataAgent · Coding Agent 實戰教學


