寫於 2026 年 8 月 16 日(補 8 月 15 日的排程),9 月才上線(部落格的發佈額度 8 月 10 日就用完了,這批稿子要等到九月才發得出去)。文中對 Goose 與 Claude Code 的描述以 8 月 16 日查到的官方文件為準,你讀到時文件可能已經改版。

2023 年那陣子,「分享一個 AI 工作流」的意思就是把一段 prompt 貼進 Slack。

貼過的人都知道結局。同事複製走,跑出來的東西跟你的完全不一樣。你以為問題出在模型隨機性,其實多半不是。你那段 prompt 裡寫著「讀一下專案的測試設定」,而你的環境裡剛好掛著一個能讀檔的工具,同事那邊沒有。同一段字,兩種宇宙。

那時候的補救方式很土:在訊息下面追一句「喔對,你要先裝 X」。講究一點的會開個 Notion 頁面,最上面寫「前置需求」。環境依賴在那個年代一直是用散文寫的,而散文會被跳過。

prompt 搬進 repo 之後,欄位長出來了

再往後兩年,工作流從聊天室搬進了版控。Claude Code 的 skill、Cursor 的 rules、AGENTS.md,形式各異,共同點是把那段字變成 repo 裡的檔案,附一段 YAML frontmatter。

Claude Code 這邊的 frontmatter 現在長得相當茂盛。官方 skills 文件的欄位表列出二十個:namedescriptionwhen_to_useargument-hintargumentsdisable-model-invocationuser-invocableallowed-toolsdisallowed-toolsmodeleffortcontextagentbackgroundhookspathsshellmetadatalicensecompatibility。二十個欄位,看起來什麼都想到了。

把它們照功能分一分,會發現一件有意思的事。descriptionwhen_to_usepathsdisable-model-invocation 這幾個管的是「什麼時候該叫我出場」。modeleffort 管的是「用多貴的腦袋跑我」。context: forkagentbackground 管的是「跑在哪個上下文裡」。hooksshellarguments 管的是執行細節。

沒有一個欄位在回答「我需要什麼才跑得起來」。

allowed-tools 不是我以為的那個意思

看到 allowed-tools 的時候,我原本以為那就是依賴宣告了。畢竟名字裡有 tools。

官方文件寫得很直白,它既不是宣告依賴,也不是限制權限:

The allowed-tools field grants permission for the listed tools during the turn that invokes the skill, so Claude can use them without prompting you for approval. The grant clears when you send your next message… It does not restrict which tools are available: every tool remains callable, and your permission settings still govern tools that are not listed.

翻成人話:它是一張「這一輪先別問我」的預先授權票。列進去的工具在叫用這個 skill 的那一輪不會跳權限確認,你送出下一則訊息,授權就失效。它不會讓沒列到的工具消失,每個工具都還是叫得動。真正會把工具從池子裡拿掉的是 disallowed-tools,那是另一個欄位。

所以 allowed-tools: Read Grep 這行的意思是「Read 跟 Grep 這輪不用問我」,不是「這個 skill 需要 Read 跟 Grep」。長得很像,用途差很遠。你把一個沒有 Read 的環境餵給它,這行完全不會抗議,因為它從一開始就不是在檢查什麼。

真正接近依賴宣告的欄位其實存在,叫 compatibility。文件說它裝的是「Environment requirements for the skill, such as intended products or system prerequisites」,屬於 Agent Skills 規格的一部分,上限五百字元。同一段文件接著補了一句:Claude Code accepts the field but doesn’t act on it。

欄位在,語意也對,執行期不讀。這件事比「沒有這個欄位」更耐人尋味。

有人把「要開哪些工具」寫進了同一個檔案

Block 開源的 Goose 選了另一條路。它的可分享單位叫 recipe,一份 YAML。

翻 GitHub 上的原始碼,Recipe 這個 struct 目前有十三個頂層欄位:versiontitledescriptioninstructionspromptextensionssettingsactivitiesauthorparametersresponsesub_recipesretrytitledescription 必填,version 沒寫預設 1.0.0instructionsprompt 至少要有一個。

關鍵在 extensions。Goose 管 MCP server 與內建工具叫 extension,而這份清單就跟提示詞、參數躺在同一個檔案裡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
version: 1.0.0
title: 週報產生器
description: 撈這週的 commit,整理成週報

extensions:
- type: builtin
name: developer
timeout: 300
bundled: true
- type: stdio
name: github
cmd: npx
args: ["-y", "@modelcontextprotocol/server-github"]
env_keys: ["GITHUB_TOKEN"]
timeout: 300

parameters:
- key: repo
input_type: string
requirement: required
description: 要撈的 repo 路徑

prompt: |
{{ repo }} 這週的 commit,整理成週報。

type 目前官方 reference 列了六種:stdiobuiltinplatformstreamable_httpfrontendinline_python。每一項還可以帶 env_keysavailable_toolstimeout 這些細節,其中 available_tools 能把一個 extension 裡用得到的工具再收窄。

recipe 還能叫 recipe。sub_recipes 底下每一項有 namepathvaluessequential_when_repeateddescription,父層可以把參數預先填好塞給子層。分享方式也做進了 CLI,goose recipe deeplink 把整份 recipe 編碼成一個連結,goose recipe validate 讓你在丟出去以前先驗過。

有個小細節透露了這套設計的態度。原始碼裡有兩個函式在做自動補件:recipe 只要有 sub_recipes,就會自動把 summon 這個 platform extension 補進 extensions;如果你寫了內建的 developer 卻沒寫 analyze,它也會幫你補上。工具依賴在這裡是被當成一等公民在維護的東西,不是註解。

出事的時候,兩邊長得不一樣

現在來問那個真正重要的問題:recipe 裡列的 extension 沒起來,會怎樣?

我原本猜是硬性報錯、直接中止。答案不是。crates/goose-cli/src/session/builder.rs 裡那段開場流程,會先開一個 spinner 顯示「starting N extensions」,把所有 extension 平行拉起來,失敗的收進一個 failed 清單,然後印出黃字警告:

1
2
Warning: Failed to start extension '<名字>' (<錯誤>), continuing without it
Hint: once the session starts, ask goose to help debug the '<名字>' extension

continuing without it。Session 照開,工作照跑。

所以兩邊其實都不會攔你。Goose 缺件會繼續,Claude Code 缺件也會繼續。差別在第 0 秒你手上有什麼。Goose 在你的任務還沒開始跑之前,就用名字告訴你哪一個沒起來、錯在哪、接下來可以問誰。Claude Code 這邊沒有東西可以點名,因為從頭到尾沒有人宣告過任何東西。

這就是我想講的那個差別。它不在功能多寡,在失敗的形狀。一邊的失敗是一行黃字,另一邊的失敗長得跟「正在努力工作」一模一樣。

我自己踩的那個坑

今年 8 月 5 日,我派了兩個 subagent 去做探索工作,prompt 裡寫得很清楚:把結果寫到檔案,每完成一節就 append,不要等全部做完才寫。這句話是我自己訂的規矩,為的是讓我隨時能用 ls 看它有沒有在動。

我選的 agent type 是 Explore

二十分鐘過去,目錄裡一個檔案都沒有。我催了一次,沒回應。再催一次,還是沒回應。那時候我的判斷是 agent 系統掛了,準備自己下場做。

Explore 的工具集裡沒有 Write。那個型別的系統提示還另外禁止用 Bash 寫檔,所以我後來給的 heredoc 變通方案,它也(很正確地)拒絕了。我要求它做一件它物理上做不到的事,而它沒有任何管道可以告訴我這件事。換成 general-purpose 之後,八分鐘就落檔了。

這件事最刺人的地方在於,規矩是我自己寫的,agent type 的能力表也是我自己寫在規則檔裡的,兩份東西就差三行,我照樣把它們兜錯。派工單裡如果有一個欄位長得像 extensions,寫著「我需要 Write」,這個坑在第 0 秒就會被擋下來,根本輪不到我在第二十分鐘懷疑人生。

靜默失敗的特徵就是這樣。它不報錯。它只是什麼都不做,而「什麼都不做」跟「正在做」在外面看起來完全一樣。

這篇我實際做了什麼、沒做什麼

該講清楚的部分。

我沒有安裝過 Goose,沒有跑過任何一份 recipe,也沒有故意拔掉一個 extension 去看它印什麼。上面所有關於 recipe 的敘述,來源是官方文件的 recipe reference 頁,以及 GitHub 上 block/goose 的原始碼。那段 continuing without it 的警告文字是我從 builder.rs 的字串常數讀出來的,不是我在終端機裡看到的輸出。程式碼路徑會變,我讀的是 main 分支 8 月 16 日的狀態。

Claude Code 那邊的欄位語意來自官方 skills 文件,這個工具是我每天在用的,Explore 那個坑也是真的發生在我機器上。

十三個欄位跟二十個欄位這兩個數字,我是照著清單數的,沒有憑印象補齊常見選項。這種列舉最容易生出一個看起來很合理、實際不存在的項目,所以我寧可回去數兩次。

接下來會往哪裡走

依賴一旦變成欄位,就會有人問下一個問題:那能不能自動處理?

Goose 已經走了半步。CLI 裡有一個叫 secret_discovery 的模組,載入 recipe 的時候會把所有 extension 宣告的 env_keys 掃一遍,而且會遞迴走進 sub_recipes,把整棵樹需要的秘密先湊齊。這是「宣告了就能預檢」最直接的紅利,缺什麼在開跑前就問完,不是跑到一半才炸。

Claude Code 這邊,compatibility 那個欄位已經躺在 frontmatter 裡了。文件說它裝的是環境需求,也說 Claude Code 現在不讀它。它哪天開始生效,skill 就從一份文件變成一個套件,而套件是有安裝失敗這種狀態的。

那一天之前,工作流的環境依賴還是活在你的記憶裡。你記得,它就跑得起來。