跳到主要內容

【2026 最新】Claude Code Decision Ledger 怎麼做?跨 Session 不忘決策的 5 步實戰教學

最後更新: ·
Claude Code Decision Ledger 教學首圖

你用 Claude Code 做完一輪重構,Session 結束前也留下了摘要;隔天重新開工,它卻再次提議昨天已否決的方案,甚至把「已合併、尚未部署」說成「已上線」。這不是單純忘記聊天,而是缺少一份能回答哪個決策仍有效、為什麼、證據在哪裡的控制面。

這篇 Claude Code Decision Ledger 教學專為第一次替 coding agent 設計跨 Session 交接的人寫。你不需要先懂資料庫或 Agent 架構;我們會用一本「工程判例簿」的比喻,從最小 JSONL schema、真相來源優先序、SessionStartStop 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}
  1. id:永遠不重用的識別碼,例如 DEC-2026-002
  2. scope:決策生效的最小邊界;寫 auth/callback 比寫「後端」更不容易誤傷。
  3. status:只允許 proposedacceptedimplementeddeployedrejectedsuperseded
  4. decisionrejectedreason:保存裁定、曾否決的路與理由,不保存辯論全文。
  5. evidence:用 git:<sha>、測試名稱或 production deploy URL 指向可重查的東西。
  6. supersedes:新紀錄指向被取代的舊 ID;舊項目同步改成 superseded
  7. deployed_at:只有 production 收據成功且 SHA 對上時才填 ISO 時間;上例仍是 null,所以只能說已實作。

存成 decisions/ledger.jsonl 的好處,是每一行都是獨立 JSON,Git diff 清楚,Node 也能逐行驗證。若一個舊決策只有局部被改,先把 scope 切細,再建立新 ID;不要讓兩條互相矛盾的紀錄都保持 active。

第二步:替四種真相分工,不要排成一條萬用順位

Claude Code Decision Ledger、Git、部署收據與聊天摘要的真相分工圖
同一個問題要問對來源:Git 證明程式版本,部署收據證明環境狀態,Ledger 保存決策理由與取代鏈。

「誰優先」要看你問的是什麼。問程式碼,就看 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,只挑 acceptedimplementeddeployed,最後輸出 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、使用絕對腳本路徑並跳過敏感檔案。

第五步:完整走一次「已實作、未部署」案例

  1. 團隊接受 DEC-2026-001:callback 使用 PKCE,並記下「token 放 localStorage」已否決。
  2. 程式合併到 9f2c1ab,測試通過,所以狀態從 accepted 改成 implemented;deployed_at仍是 null
  3. 下一個 Session 啟動時,Hook 注入「implemented/not deployed」。Claude 可以繼續準備上線,但不能把 production 說成已更新。
  4. 企業 SSO 帶來新邊界,建立 DEC-2026-002並以 supersedes指回舊 ID;舊紀錄改成 superseded。
  5. 部署平台回傳 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 都會遵守決策。

  1. 否決方案復活:新增 proposed 決策,內容精確重複 active entry 的 rejected;預期指出來源 ID。
  2. 限制遺失:讓同一 scope同時存在兩個 active 決策;預期拒絕多重現行狀態。
  3. 未部署卻報上線:把 implemented 改成 deployed,但不附 production receipt;預期拒絕。
  4. 完成卻無證據:implemented 只留一句測試文字,移除 git:<sha>;預期拒絕。
  5. 新舊同時有效:移除 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 課程挑一條適合你的實作路線。

ALPHALAB 社群

有問題?來 Telegram 聊

和 Terry、編輯、其他網友一起討論這篇文章。提問、分享觀點,回覆更即時。

加入 Telegram 討論

📩 訂閱 AlphaLab 電子報

每週最多三封:一封 Weekly 週報與最多兩封關鍵 Alpha Signal。

我們不會 spam,隨時可退訂。