你用 Claude Code 做完一輪重構,Session 結束前也留下了摘要;隔天重新開工,它卻再次提議昨天已否決的方案,甚至把「已合併、尚未部署」說成「已上線」。這不是單純忘記聊天,而是缺少一份能回答哪個決策仍有效、為什麼、證據在哪裡的控制面。
這篇 Claude Code Decision Ledger 教學專為第一次替 coding agent 設計跨 Session 交接的人寫。你不需要先懂資料庫或 Agent 架構;我們會用一本「工程判例簿」的比喻,從最小 JSONL schema、真相來源優先序、SessionStart/Stop Hook,一路做到五種失憶回歸測試。
先說結論:保存決策狀態,不是重播整段聊天
- Decision Ledger 記「為什麼與目前狀態」:現行決策、否決方案、限制、證據與取代關係。
- Git 記「程式到底長什麼樣」:commit/tree 能定位實作版本,但不等於那一版已部署。
- 部署收據記「哪一版真的在環境裡」:production success、commit SHA、時間與 log URL 要能互相對上。
- 聊天摘要只當線索:它可以幫你回想,不該單獨證明完成或上線。
一句話記住:跨 Session 正確續跑=Decision Ledger(why/state)+ Git(what)+部署收據(where/when)。
Claude Code Decision Ledger 解決哪五種失憶?
一則 Claude Code 社群討論把問題拆成五類:重提已否決方案、遺失適用限制、把未部署功能當成已上線、缺少完成證據,以及分不清現行與已取代決策。這五題很適合直接變成你的回歸測試。
它和聊天記憶的差別,像「會議錄音」與「法院判例」。錄音保留整段對話;判例只保留最後裁定、理由、適用範圍與後來是否被新判例推翻。Claude Code 官方文件也把 CLAUDE.md 與 auto memory定位為跨 Session 的 context,而且明確說它們不是強制設定。Decision Ledger 因此不是再做一份萬能記憶,而是把少數會改變後續行動的決策變成可查、可驗、可失效的紀錄。
第一步:先建最小 schema,別急著做知識圖譜
架構決策紀錄(ADR)的經典格式本來就有 proposed、accepted、deprecated/superseded 等狀態。Michael Nygard 的 ADR 原始文章強調:後來的決策若改變舊決策,應把舊項目標成 superseded 並連回替代者。新手版 Decision Ledger 可沿用這個骨架,再補上部署時間與機械證據。
{"id":"DEC-2026-002","scope":"auth/callback","status":"implemented","decision":"OAuth callback 使用 PKCE,企業 SSO callback 拆成獨立路由","rejected":["共用一條 callback 再用 query 參數分流"],"reason":"企業 SSO 有獨立稽核與回滾邊界","evidence":["git:1a2b3c4","test:auth-callback"],"supersedes":["DEC-2026-001"],"deployed_at":null}
id:永遠不重用的識別碼,例如DEC-2026-002。scope:決策生效的最小邊界;寫auth/callback比寫「後端」更不容易誤傷。status:只允許proposed、accepted、implemented、deployed、rejected、superseded。decision/rejected/reason:保存裁定、曾否決的路與理由,不保存辯論全文。evidence:用git:<sha>、測試名稱或 production deploy URL 指向可重查的東西。supersedes:新紀錄指向被取代的舊 ID;舊項目同步改成superseded。deployed_at:只有 production 收據成功且 SHA 對上時才填 ISO 時間;上例仍是null,所以只能說已實作。
存成 decisions/ledger.jsonl 的好處,是每一行都是獨立 JSON,Git diff 清楚,Node 也能逐行驗證。若一個舊決策只有局部被改,先把 scope 切細,再建立新 ID;不要讓兩條互相矛盾的紀錄都保持 active。
第二步:替四種真相分工,不要排成一條萬用順位

「誰優先」要看你問的是什麼。問程式碼,就看 Git;Git 的物件是內容定址資料,而 commit 會連到一個可重建的 tree。問 production 跑哪一版,就看部署平台回傳的成功狀態、environment 與 SHA;例如 GitHub Deployments把 deployment 綁到 ref/SHA,而 Deployment Status另外記錄 pending、in progress、success 等狀態與 log URL。
- 問「程式有沒有做」:
git show <sha>與測試證據優先。 - 問「production 有沒有上線」:成功部署收據與 runtime probe 優先;Ledger 若寫錯,要把它降回 implemented。
- 問「為什麼這樣做、哪些路不要再走」:Ledger 優先,但它引用的 Git/部署證據必須存在。
- 問「上次聊了什麼」:再查 transcript 或摘要;它是導航,不是完成證明。
白話說,Ledger 是索引卡,不是神諭。它若和可觀察證據衝突,修 Ledger;不要為了保住一行 Markdown,反過來否認真實 repo 或 production。
第三步:用 SessionStart Hook 只載入有效決策
Claude Code 官方 Hooks reference說明,SessionStart會在 startup、resume、clear、compact 與 fork 等來源觸發;command hook 的 stdout 可以加入 Claude 的 context。這正適合把 active 決策壓成十幾行,而不是把整本歷史塞回 prompt。
{
"hooks": {
"SessionStart": [{
"matcher": "startup|resume|clear|compact|fork",
"hooks": [{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PROJECT_DIR}/scripts/decision-context.mjs"],
"timeout": 10
}]
}],
"Stop": [{
"hooks": [{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PROJECT_DIR}/scripts/decision-check.mjs", "--hook"],
"timeout": 10
}]
}]
}
}
把設定放在可 review 的 .claude/settings.json,腳本則固定做三件事:先驗 schema,只挑 accepted/implemented/deployed,最後輸出 ID、scope、decision、rejected、evidence 與 deployed_at。每個欄位限制長度、移除控制字元,且不執行 Ledger 裡的任何字串。
const active = rows.filter(r =>
["accepted", "implemented", "deployed"].includes(r.status)
);
for (const r of active) {
console.log(`- ${clean(r.id)} [${clean(r.status)}] ${clean(r.scope)}`);
console.log(` decision: ${clean(r.decision)}`);
console.log(` rejected: ${r.rejected.map(clean).join(" | ")}`);
console.log(` evidence: ${r.evidence.map(clean).join(" | ") || "none"}`);
console.log(` deployed_at: ${r.deployed_at ?? "not deployed"}`);
}
上面省略了 production 版應有的完整 JSON Schema、單元測試、最大檔案大小與錯誤處理;這些省略會影響安全與可用性,所以別把示意片段直接當組織級 parser。先從一個 repo、一個 JSONL、幾十筆決策做起,再加資料庫或搜尋。
第四步:用 Stop Hook 驗證更新,不讓 Hook 偷寫決策
Stop會在 Claude 準備結束回應時觸發;回傳 {"decision":"block","reason":"…"}可要求它先修正。Checker 至少要擋住:重複 ID、未知狀態、同 scope 多個 active 決策、implemented 缺 Git SHA、deployed 缺 production receipt/時間,以及 superseded 沒有 replacement link。
DECISION_LEDGER=decisions/ledger.jsonl \
node scripts/decision-check.mjs
# 通過:Decision Ledger: OK
# 失敗:exit 1;Stop Hook 模式則回傳 decision=block
不要讓 Stop Hook 從最後一段聊天「自動推理」後直接改 Ledger。比較穩的流程是:Agent 提出差異、你在 diff 裡 review、明確更新 JSONL,Hook 只驗格式與可反駁條件。官方也提醒 command hooks 以使用者完整權限執行,應驗證輸入、引用 shell 變數、擋 path traversal、使用絕對腳本路徑並跳過敏感檔案。
第五步:完整走一次「已實作、未部署」案例
- 團隊接受
DEC-2026-001:callback 使用 PKCE,並記下「token 放 localStorage」已否決。 - 程式合併到
9f2c1ab,測試通過,所以狀態從 accepted 改成 implemented;deployed_at仍是null。 - 下一個 Session 啟動時,Hook 注入「implemented/not deployed」。Claude 可以繼續準備上線,但不能把 production 說成已更新。
- 企業 SSO 帶來新邊界,建立
DEC-2026-002並以supersedes指回舊 ID;舊紀錄改成 superseded。 - 部署平台回傳 production success 且 SHA 對上後,才加入
deploy:production:https://…證據、填入deployed_at,狀態改成 deployed。
這條 trace 的關鍵不是多一個檔案,而是每次狀態跳轉都有可反駁條件。聊天說「done」不會讓 implemented 自動變 deployed;Ledger 寫「deployed」也不會覆蓋一張失敗的部署收據。
五題回歸測試:每次改 Hook 都重播
本文把示例 checker 放在 Node.js 24.15.0 執行,正常 Ledger 通過;再注入下列五個壞案例,五題都被預期規則攔下。這只是 schema/state machine 的本機 smoke test,不代表模型在所有 repo 都會遵守決策。
- 否決方案復活:新增 proposed 決策,內容精確重複 active entry 的
rejected;預期指出來源 ID。 - 限制遺失:讓同一
scope同時存在兩個 active 決策;預期拒絕多重現行狀態。 - 未部署卻報上線:把 implemented 改成 deployed,但不附 production receipt;預期拒絕。
- 完成卻無證據:implemented 只留一句測試文字,移除
git:<sha>;預期拒絕。 - 新舊同時有效:移除
supersedes連結,卻保留舊項目為 superseded;預期指出沒有替代者。
再加一組人工題:開新 Session 問「目前 callback 怎麼做?哪個方案被拒絕?production 是哪一版?」答案必須逐項附上 Ledger ID、Git SHA 與部署收據;找不到證據就停在 unknown,而不是補完故事。若你還想把「Agent 說完成」變成可執行 gate,可接著看 Unlazy Acceptance Ledger 教學。
四個常見坑:Ledger 也會漂移
- 把 Ledger 當唯一真相:它最適合保存 why/state;程式與環境仍要回 Git、deploy receipt、runtime probe 查證。
- scope 寫太大:「整個 backend」讓局部取代變得含糊;改成可獨立驗收的模組、API 或資料邊界。
- 把全部歷史塞進 context:SessionStart 只輸出 active 摘要與必要的否決理由;完整 JSONL 留在 repo,需要時再讀。
- Hook 解析任意文字:Ledger 是 repo 輸入,不是可信程式;只讀 allowlist 欄位、限制長度、不要 eval,也不要讓 evidence 字串變成 shell command。
如果你想先補 Agent 的執行層心智模型,可讀 Agent Harness 是什麼與 30 行 Harness 實作;若問題更偏向長期聊天召回,再比較 Lossless Memory A/B Test,不要把不同問題都塞進同一份 memory。
FAQ:Claude Code Decision Ledger 的 8 個直球問題
1. Decision Ledger 可以取代 CLAUDE.md 嗎?
不該。CLAUDE.md 放每個 Session 都要遵循的規則與專案慣例;Ledger 放會演化、會被取代、需要證據的決策狀態。CLAUDE.md 只要指向 Ledger 與更新規則即可。
2. 用 Markdown 還是 JSONL?
先選你能驗證的格式。人讀為主可用一檔一 ADR;要跑 schema、Hook 與回歸測試,JSONL 比自由格式 Markdown 更容易 fail closed。
3. 每個小決定都要記嗎?
不用。只記會改變後續行動、容易被重新爭論、跨 Session 仍有效,或需要部署/合規證據的決策。能從程式直接推導的細節留在 Git。
4. deployed_at 可以手動填嗎?
技術上可以,流程上最好由 CI 收據驅動。至少要求 environment、commit SHA、success 狀態與 log URL 對上;否則保持 implemented。
5. Stop Hook 會不會陷入迴圈?
會有風險。腳本應檢查 stop_hook_active,只回報可修復的 deterministic 錯誤,避免每次都產生相同 block。官方也為連續續跑設有上限,但你的 checker 仍要自己保持冪等。
6. 可以把 Ledger 放在 auto memory 嗎?
不建議當團隊唯一版本。auto memory 是 Claude 自己累積的 per-repository notes;可 review、可 merge、能和程式一起版本化的 repo 檔案,更適合團隊決策。
7. supersedes 可以只取代舊決策一小段嗎?
可以,但先切小 scope。讓新紀錄只接管被改變的邊界;若舊 scope 太大,重寫成數個可獨立驗收的決策,會比在長文字裡埋例外更清楚。
8. 新手今天先做哪一步?
先寫三筆。挑一個現行決策、一個已否決方案、一個已實作但未部署的功能,填入 schema,再跑 checker。等這三筆真的幫你避開一次重工,再擴充自動化。
給新手的 7 點檢查清單
- 一個 ID 只對應一個可說清楚的決策。
- 一個精確 scope 同時只留一個 active 決策。
- implemented 必須能回到 Git SHA 與測試。
- deployed 必須能回到 production success receipt。
- 舊決策標 superseded,新決策反向連回舊 ID。
- SessionStart 只注入 active 摘要,不重播整段聊天。
- Stop Hook 只驗證,不替人類偷偷做決策。
接著閱讀
左右滑動查看更多推薦
結語:先讓一個決策能被下一個 Session 反駁
Decision Ledger 最有價值的地方,不是讓 Claude Code「記得更多」,而是讓下一個 Session 知道什麼仍有效、什麼已被否決,以及哪一句話必須先拿證據才能成立。今天先在 repo 建立 decisions/ledger.jsonl,放進一筆 implemented-but-not-deployed 的真實案例,讓 checker 故意失敗一次;等你親眼看見它擋住錯誤狀態,再把它接到 Hook。想系統化學會 Agent、Claude Code 與自動化工作流,可從 AlphaLab 課程挑一條適合你的實作路線。






