#!/bin/bash # PreToolUse hook(Write|Edit|MultiEdit)— 長效資料不准寫進 KV,一律走 KBDB API # # 【portage 記錄,inkstone/ISEP#122】本檔 2026-08-25 立於 # `InkStoneCo/.claude/hooks/kv-write-guard.sh`(那時 ISEP 還沒吸收它), # 2026-09-02 原樣搬進 ISEP——它是那次盤點揪出來的**唯一一支**「專案版有、 # plugin 沒有」的真閘(另外 3 支同名殘骸是已退役機制,直接刪掉,不搬)。 # 搬完之後 `InkStoneCo/.claude/hooks/kv-write-guard.sh` 就是廢棄複本, # 待這裡升版、`/plugin update` 裝上之後即可從專案版與 settings.json 一併移除。 # # 【leo 2026-08-25 立】原話(三句,一句比一句尖): # 「已經禁止寫入 KV 了,這是太貴的資源,應該都去寫入 KBDB,到底寫入 KV 做什麼?」 # 「上次刪掉一堆東西,現在又寫入,**你沒有規範嗎**」 # 「**我強制禁止寫 SQL,要用 KBDB API,你就讓它繞過寫 KV 代替?**」 # # ── 為什麼存在(2026-08-25 實查,這是本閘的正身)───────────────────────── # 規則早就在:`system-dev/wiki/decisions-summary.md:133` # 「**KV =暫存(cache / transient),不是長期真相源。**」 # 而且打過一場「KV 退休戰」(同檔 254 行,SUBMISSIONS_KV 併入那一役)。 # # 🔴 **但 `.claude/hooks/` 底下跟 KV 有關的閘:0 支。** # ⇒ 規則被讀到了,卻沒有任何機制驗證有沒有照做——與 2026-08-25 一整天挖出來的 # 四層 migration 缺陷、以及 08-10 抓到的 history-first/KBDB-first/stage-first # **完全同一個形狀**。 # # 🔴 **更糟的是 leo 指出的因果**(總管用 git 查證屬實): # 禁 SQL 的牆 2026-08-07 # app-system.ts 誕生 2026-08-24 ← 17 天之後,**從第一行就在寫 KV** # 而 `kbdbFetch()` 裡的 KV 字樣是 **0** ⇒ 走 KBDB API 根本不碰 KV # ⇒ 那些 KV 寫入**每一處都是繞過**,沒有一處經過知識庫。 # **牆立起來了,水從旁邊流走了。** # # ── 判準(封動作,不封措辭)────────────────────────────────────────── # 新增一行「往 KV 寫入」的程式碼 ⇒ 擋一次,要求說明它為什麼不是長效資料 # 只是讀(.get/.list) ⇒ 放行 # 刪除既有的 KV 寫入(退休戰) ⇒ 放行(那正是我們要的方向) # # ── 豁免(留痕,不是後門)──────────────────────────────────────────── # 那一行尾端加 `kv-ok: <理由>`。 # 合法的理由只有一種形狀:**這筆資料本來就是短命的**(session、一次性 nonce、 # 有 TTL 的快取)。寫「暫時先這樣」「之後再搬」都不算—— # 那是把債留給下一個人,而 2026-08-25 證明了沒人會回來還。 set -uo pipefail payload=$(cat) read -r tool content file <<<"$(printf '%s' "$payload" | python3 -c " import json,sys try: d=json.load(sys.stdin); ti=d.get('tool_input') or {} # Write 用 content;Edit 用 new_string;MultiEdit 把每筆 new_string 串起來 parts=[ti.get('content') or '', ti.get('new_string') or ''] for e in (ti.get('edits') or []): parts.append(e.get('new_string') or '') body='\n'.join(p for p in parts if p) print(d.get('tool_name','') or 'x', len(body), ti.get('file_path','') or 'x') sys.stderr.write(body) except Exception: print('x 0 x') " 2>/tmp/.kv-guard-body)" || exit 0 body=$(cat /tmp/.kv-guard-body 2>/dev/null) rm -f /tmp/.kv-guard-body [ -z "$body" ] && exit 0 # 只管程式碼檔;文件、設定、測試裡提到 KV 不算違規 case "$file" in *.ts|*.js|*.mjs|*.tsx|*.jsx) ;; *) exit 0 ;; esac # 閘自己、與退休戰的測試檔放行(否則永遠改不動它) case "$file" in *kv-write-guard*|*.test.*|*/tests/*) exit 0 ;; esac # 命中的行:往 KV 綁定寫入,且**該行沒有 kv-ok 豁免** # 判準是形狀不是名字:**全大寫的 binding 後面接 .put(/.delete(**。 # 為什麼不列名字清單(第一版就是這樣寫壞的):名字會長出新的(下一個 KV 綁定叫什麼沒人知道), # 而清單只認得已知的那幾個 ⇒ 新的 KV 從清單的縫隙走過去,本閘就變成裝飾。 # 全大寫識別字 + .put/.delete 這個形狀,在 Workers 裡就是 KV(D1 沒有 .put,JS 物件用 .set)。 hits=$(printf '%s' "$body" | grep -nE '\b[A-Z][A-Z0-9_]{1,}\.(put|delete)\(' \ | grep -v 'kv-ok' || true) [ -z "$hits" ] && exit 0 cat >&2 <\`,留痕放行。 ③ **你在做的是退休戰(刪掉 KV 寫入)** → 本閘不擋刪除,只擋新增。 🔴 \`kv-ok: 暫時先這樣\` / \`之後再搬\` **不算理由**——那是把債留給下一個人, 而 2026-08-25 已經證明了沒人會回來還。 EOF exit 2