身為總管,我要推 gitea main 不必請任何人動手,我才不會每次交付都卡在一道自己人的閘上 #61
Notifications
Due Date
No due date set.
Blocks
#30 讓閘擋對東西
inkstone/ISEP
Reference: inkstone/ISEP#61
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
身為總管,我要推 gitea main 不必請任何人動手,我才不會每次交付都卡在一道自己人的閘上。
leo 2026-08-27 的原話(本票的來由)
而權責 leo 2026-08-10 就講死了:
⇒ 總管推 gitea main 從來就不是人閘。 現在它變成人閘,是實作的意外,不是設計。
現況:
main-and-prod-push-guard.sh的戳記機制已經走進死路那支 hook 自己的註解記著完整的演化史,三個階段都失敗:
CLAUDE_CODE_CHILD_SESSION=1判斷 subagent→ 註解裡寫著實測結果:總管主 session 也是 1 ⇒ 那個變數不是身分標記 ⇒ 誰都擋
/tmp/.main-push-ok,總管手動寫)→ 2026-08-11 被穿透:總管為推 A repo 寫的戳記,被並行的 subagent 拿去推了 B repo 的 main
→ 再改成「綁 repo、單次、15 分鐘」
git rev-parse --show-toplevel > /tmp/.main-push-ok被環境的權限分類器擋下;連
env | grep CLAUDE、printf '%s' "$CLAUDE_AGENT_NAME"都擋⇒ 總管既無法造戳記,也無法自己實測有哪些身分訊號可用
⇒ 這道閘現在的效果是:總管推不了 main,只能請 leo 代跑指令 = 正是 leo 說的人閘
要達成什麼
總管推 gitea main 是全自動的;subagent 推 gitea main 仍然擋得住。
兩件同時成立,而且不需要任何人(leo 或總管)先做一個手動動作。
🔴 不要把「總管放行」做成一把 subagent 也拿得到的鑰匙——那是 2026-08-11 那次穿透的形狀。
🔴 也不要靠時間窗——在多條 subagent 並行的情況下,時間窗本身就是漏洞(那支 hook 的註解已經寫過這句)。
這一格要你自己查,不要照抄總管的猜測
總管沒有能力實測(見上面第 3 點),所以不指定做法。可查的方向至少有:
session_id/transcript_path/agent 相關欄位)——這是 hook 唯一一定拿得到、而且不受 shell 權限分類器影響的輸入
CLAUDE_AGENT_NAME等)在兩種身份下的實際值——你在 subagent 裡,可以印你自己的;主 session 那半可以請總管在回覆裡代印,或用別的方法取得
先把「哪個訊號真的分得出兩種身份」查出來並貼實測,再談改法。
查不到可靠訊號 ⇒ 那本身就是結論,回報它,並提出退路(例如把判斷點從 hook 移到別的地方)。
怎麼驗
git push gitea main,零手動前置動作就過--dry-run、讀取類指令、指令裡只是提到 main 字樣,全部照樣放行(那支 hook 現有的測試已經涵蓋這幾種,不要讓它們變紅)
plugin.json升版——ISEP 靠版本號分發,版本沒動=plugin update是 no-op紅線
claude-code、改 tag已知會撞的東西
inkstone/ISEP#60正在同一個 repo 移除subagent-claim-worksheet.sh/claim-verify-police.sh,不同檔,但主 checkout 停在
feat/ticket-carries-the-task,各自開 worktreeinkstone/ISEP#58(三格原生欄位)尚未 mergeleo 給的解法方向:建一個 agent 身份,派工一律叫那個 agent
⇒ 這正好補上前一則指出的洞。前面卡住的原因是沒有任何訊號分得出兩種身份;
而 leo 的方向是:不要去「偵測」身份,而是「造」一個身份出來——
派工一律經由某個具名的 agent,那它在 hook 那端就不是空的。
總管實證:現行的署名機制確實是空的(這是打出來的,不是推測)
那支 hook 被擋時會把請求留在
.claude/pending-main-push/,檔名用CLAUDE_AGENT_NAME署名。實際留下來的五份:
全部是
unnamed。 ⇒CLAUDE_AGENT_NAME在那些 subagent 身上是空值,現行的署名機制形同虛設——它連「誰想推」都記不下來,
而 leo 2026-08-12 立它的時候原話是「它會問你的意見,所以每個你叫起來的 subagent 都有名字」。
⇒ 那個前提(每個 subagent 都有名字)在現場不成立,這一點本身就要修。
所以這張票要查/要做的,順序是這樣
CLAUDE_AGENT_NAME什麼時候有值?用 Agent 工具的name參數派工時,subagent 裡拿不拿得到?.claude/agents/*.md)的 frontmatter 支援哪些欄位?能不能讓「經由某個自訂 agent type 派出去的 subagent」在 hook 那端被認出來?
總管派工一律經由那個身份,hook 認那個身份 ⇒ 有身份=subagent=擋,沒有=總管=放行
pending-main-push的檔名不該再出現unnamed🔴 查不到官方依據就明講查不到,不要給一個看起來像答案的猜測——
這道閘錯了會讓 subagent 推上 main(2026-08-10 真的發生過),也會讓總管交不出貨(今天正在發生)。
查機制的結果(isep61-push-identity 分身,讀的是這個 session 正在跑的那顆 Claude Code 二進位
2.1.246,用strings對照原始碼字串——不是查文件記憶,是拆這顆檔案)1️⃣ hook stdin JSON 有沒有能分辨主 session/subagent 的欄位?有,而且比「造身份」更乾淨
直接從二進位裡挖出 hook 輸入的 zod schema 定義(帶官方
.describe()說明文字,逐字照抄):PreToolUse 再疊上
hook_event_name / tool_name / tool_input / tool_use_id。🔴
agent_id就是答案——它的官方說明白紙黑字寫「只在 subagent 內才有、主線程永遠沒有、拿它判斷而不是 agent_type」。判準可以直接是:
不需要造任何身份、不需要戳記、不需要時間窗——這個欄位是宿主行程自己按呼叫語境填的,
subagent 的 shell 環境改不動它(跟
CLAUDE_AGENT_NAME那種「读自己环境变量」的做法完全不同性質,後者的值來源是 subagent 自己的行程環境,前者是宿主在建 hook payload 時的語境判斷)。
實作位置找到了:
Ws(session, cwd, ...)這個函式組出上面的 base schema,裡面就是agent_id: r?.agentId(r是「目前是否在某個 agent context 裡執行」——是 subagent 就有r,主線程就是
undefined)。SubagentStart/SubagentStop兩個事件的 schema 把agent_id訂成必填(不是 optional),因為那兩個事件定義上只會在 subagent 身上發生。
2️⃣
CLAUDE_AGENT_NAME什麼時候有值?—— 查證結果:它從來沒有值,因為 Claude Code 根本不會設定這個變數對這顆執行中的二進位(
/Users/.../claude-code/2.1.246/.../claude,跟這個 session 的CLAUDE_CODE_EXECPATH完全同一顆)整份strings掃描,CLAUDE_AGENT_NAME零命中。在整個 InkStoneCo repo 裡,這個字串只出現在
main-and-prod-push-guard.sh自己身上(第 208、214、257 行,
${CLAUDE_AGENT_NAME:-未署名})——這支 hook 假設 Claude Code會把 Agent 工具的
name參數灌進 subagent 的 shell 環境變數,但這個假設從沒被驗證過,而且是錯的。
實測佐證(這個分身自己的環境):這條線是總管用
name: "isep61-push-identity"派出來的,env | grep -iE 'name|agent'逐行看過(55 個變數全部列出),沒有任何一個變數帶著isep61-push-identity這串字,也沒有CLAUDE_AGENT_NAME這個 key。這就是為什麼pending-main-push/底下五份全部是unnamed——署名機制打從第一天就打不中目標,不是後來壞掉的。
name參數本身確實有被宿主追蹤(在agent.spawn事件的權限規則比對欄位清單裡:["prompt","tool_use_id","description","subagentType","parentModel","permissionMode", "background","fork","name","cwd"]),也有另一條完全不同的路徑——背景/teammate("agentteams")機制會把
agent_id/name一起回給發起端當 tool_result(agent_id: ${teammate_id} \nname: ${name})。但那是「總管拿到一段文字回報」的管道,不是「灌進 subagent 自己 shell環境變數」的管道——兩者不是同一件事,沒有任何一條路徑會讓 subagent 的 Bash 行程看到
$CLAUDE_AGENT_NAME。3️⃣ 自訂 agent(
.claude/agents/*.md)frontmatter 支援哪些欄位、hook 認不認得出來?現場四個檔案(
arcrun-engineer.md/isep-guard.md/rag-collector.md/inkstone-scribe.md+內建的
claude-code-guide.md)只用了name+description(claude-code-guide.md多用了tools: Read, WebFetch, WebSearch收窄工具集)。Agent 工具自己的官方說明文字也證實model是受支援但這五個檔案都沒用到的欄位:「Each agent type's model, reasoning effort,and tools come from its definition (
.claude/agents/*.mdfrontmatter or SDKagents)」。這個
name會變成 hook payload 的agent_type——2️⃣ 那條 schema 說明文字舉的例子剛好就是
"code-reviewer"(Claude Code 內建示範用的自訂 agent類型名稱),跟這裡的arcrun-engineer/isep-guard是同一種東西。所以如果自訂 agent 真的被載入、真的用
subagent_type: "isep-guard"派工,它底下每一次 Bash 呼叫的 hook payload會帶
agent_type: "isep-guard"——這條路徑理論上走得通。但本 session 已經實測撞到 leo 提醒的那個洞:
.claude/agents/*.md只在 session啟動時載入一次。這個 session 啟動時
claude-code-guide.md已存在(進得了可用清單),但
arcrun-engineer/rag-collector/isep-guard/inkstone-scribe四個檔案是總管session 中途才建的,於是
subagent_type: "isep-guard"直接回Agent type not found——想用這條路徑,那次派工必須是全新啟動的 session,不能是「總管臨時建好馬上用」。
2️⃣ 再定做法:不需要造身份,
agent_id直接解決問題leo 要的兩件事「總管推 main 全自動、subagent 推 main 仍擋住、且不需要任何人先做手動動作」——
agent_id這個官方欄位原生就是這個判準,比「建一個具名 agent、總管派工一律經由它」更省一層:
不會被 2026-08-11 那種「戳記被平行 subagent 拿去用」的方式穿透——因為這個欄位不是
「總管留下讓誰都撿得到的鑰匙」,是宿主每次呼叫都重新判斷語境填的,subagent 自己的
shell 環境改不了它。也不需要自訂 agent 身份、不受「
.claude/agents/*.md只在啟動時載入」這個限制影響。
自訂 agent 身份(leo 提的方向)仍然有價值,但用在另一件事上:分辨「哪一個 subagent」
(
agent_type會是isep-guard/arcrun-engineer/general-purpose…),而不是本票要解的「總管 vs subagent」二元判斷——那件事
agent_id一個欄位就夠。3️⃣ 順手修掉署名虛設
main-and-prod-push-guard.sh目前讀${CLAUDE_AGENT_NAME:-未署名}那三處(208/214/257 行)要改讀 hook stdin JSON 的
agent_type(有意義時,例如日後真的走自訂 agent 路徑)與
agent_id(留痕用),不要再指望環境變數——那個環境變數永遠不會有值。沒查到 / 沒動手的部分(誠實標記,不是交付)
hook 去 dump 實際跑過的 stdin JSON 來雙重驗證——因為本 session 目前有多條 subagent
並行(
baton/gate30/isep60-drop-worksheet…都在跑),改動共用的settings.json/settings.local.json掛新 hook 有干擾到別條線的風險,判斷不值得為了雙重驗證去冒這個險。上面的結論建立在「這顆二進位的原始碼字串裡,那份 schema 帶著
官方撰寫的
.describe()說明文字,且呼叫點Ws(...)的邏輯與說明文字一致」這個證據上,信心程度高但不是 100% 的執行期複驗。
main-and-prod-push-guard.sh本體——inkstone/ISEP#60正在同一個 repo動另外兩支 hook,怕撞車;且 ISEP 的「真身」repo 這條分身沒有找到本機 checkout 路徑
(現場只看到
.claude/plugins/cache/inkstone/isep/這個唯讀快取與 InkStoneCo 這邊的同步副本),需要總管指定去哪個 checkout 動手。
📌 補票:這些原本被總管寫在派工單裡,違規(leo 2026-08-27 當場抓到)
規則早就收緊成派工單只有一行票號,沒有第二行——而「票上還沒有」不是寫進 prompt 的理由,它是「去把它寫上票」的指令。任務只活在 prompt 裡,那條線被停掉或換人接手就消失。
總管今天派了八條線,八條全犯。以下把該線 prompt 裡的內容原樣補回票上。
InkStoneCo/.claude/agents/建了四個自訂 agent 身份(arcrun-engineer/rag-collector/isep-guard/inkstone-scribe)subagent_type: isep-guard當下回Agent type not found,只認得內建六個 ⇒.claude/agents/只在 session 啟動時載入(這是本票第 3 點要的實測資料之一)env | grep CLAUDE都被權限分類器擋,主 session 那一半的實測要由該線設法取得inkstone/ISEP#60正在同一個 repo 移除另外兩支 hook,各自開 worktree