如何把金鑰交給程式設計代理,又不寫進提示詞

代理要跑一條命令,於是向你要一把 Stripe 金鑰或者資料庫密碼。你把它粘進對話,它就落進了磁碟上的會話記錄,進了模型提供商,有時還進了一次提交。我們自己犯過三次錯,最後定下的規矩是:金鑰到達代理的通道只有環境變數,沒有別的。

場面永遠是同一個。代理做任務做到一半,需要呼叫你的 Stripe 測試 API,需要對預發布資料庫跑一次遷移,或者需要推送到一個私有儲存庫。它向你要金鑰。金鑰就在你的密碼管理器裡,代理在等著,而世界上最快的辦法就是把它粘進對話。

那一次貼上,就是金鑰脫離你控制的時刻。不是因為模型心懷惡意,而是因為提示詞是一個會被儲存、會被轉發的東西:它進入你磁碟上的一個會話記錄檔案,它發給模型提供商,每一次重試、每一個繼承對話的子代理都會把它再抄一遍。

我們在做 AgentsRoom,一個執行程式設計代理的桌面應用,所以這個問題我們必須先給自己解決。2026 年 9 月我們錯了三次。下面是我們最後定下的規矩、催生它的那三個錯誤,以及無論你用不用我們的應用都能照做的方法。

簡短的答案

  • 金鑰到達代理的通道是環境變數,除此之外沒有別的。 不是訊息,不是工單,不是技能,也不是命令列引數。
  • 代理寫的是 $NAME,永遠不是值。 命令執行時由 shell 展開。
  • 值只存一次,存在儲存庫之外,由你作業系統的鑰匙串加密,其他地方一律按名稱引用。
  • 過了一遍對話的金鑰就廢了。 先輪換,再收拾。
  • 脫敏是安全網,不是方案。 它抓得住看起來像金鑰的東西,抓不住一個讀起來像單詞的資料庫密碼。

這一粘比看上去糟糕得多

三件事同時發生,而只有第一件是顯眼的。

它發給了模型提供商。 這是人人都看得見的那部分。你的金鑰現在躺在別人基礎設施上的一個請求體裡,而且在一段你無法編輯的對話裡。

它落到了你的磁碟上,而且是明文。 Claude Code、Codex、Antigravity CLI 以及其他 CLI 都會保留一份對話的會話記錄,好讓你能接著聊。那個檔案就在你的主目錄裡,沒有加密,只要 CLI 還留著歷史,它就一直在。你不妨在自己的會話記錄裡搜一次 sk-,看看會跳出什麼。

它會橫著擴散。 對話就是代理的記憶,所以任何複製對話的東西都會把金鑰一起復制:一次恢復的會話、一個 fork 出來的代理、一個帶著父級上下文啟動的子代理、一份被寫進專案檔案的摘要。我們見過一把金鑰從一條聊天訊息走進工單描述,再走進第二個代理的第一條訊息,而這一路上沒有任何人做錯事。

而平時那張安全網兜不住。輸出脫敏靠的是模式:sk-…、glpat-…、JWT 的形狀。它沒法知道 thursday-harbour-71 就是你預發布資料庫的密碼。在我們自己的應用裡,我們也會把匹配到的值從終端輸出裡脫敏掉,但我們始終把它當成高空鞦韆下面的那張網,而不是鞦韆本身。

只開一扇門的規矩

規矩一行寫完:憑證到達代理的通道,是代理所在程序的環境變數,別無他路。

其餘一切都是字面文字。你敲的訊息、工單描述、技能檔案、系統提示詞、命令列引數,這些都要被人或模型讀到,所以寫在那裡的金鑰就是已經公開的金鑰。

命令列值得單寫一句,因為它感覺上比聊天訊息安全,而事實並非如此。--api-key=… 裡的值透過 ps 對機器上的每個程序都可見,它會落進你的 shell 歷史,會被任何記錄已啟動命令的東西寫進日誌,還會出現在那些把程序行回顯給你的診斷工具裡。而單個程序的環境變數,只有該程序和 root 讀得到。暴露面更小,用起來一樣方便。

至於代理,它從來就不需要那個值。它需要寫的是這一行:

curl -H "Authorization: Bearer $STRIPE_SECRET_KEY" https://api.stripe.com/v1/charges

shell 會在執行時展開 $STRIPE_SECRET_KEY。代理寫下的是 17 個字元加一個美元符號。它的會話記錄裡留下的,也是 17 個字元加一個美元符號。

在我們的應用裡是怎麼跑的

機制本身沒有形態重要,但能看到一個實現,形態就更好照搬。

一個金鑰庫,一個檔案。 AgentsRoom 把金鑰放在你主目錄下的單個檔案裡,權限 0600,透過作業系統鑰匙串加密,每次寫入都重新確認一遍。沒有任何內容同步到伺服器,也沒有任何內容跑到另一台電腦上:金鑰屬於持有它的那台機器。如果鑰匙串不可用,儲存會直接失敗,而不是退回到明文。

一個值只儲存一次。 命令與連線 > 機密,任何專案裡都能開啟。一個名稱(字母、數字、點、短橫線、底線,最長 64 個字元,所以可以直接當環境變數名用)和一個值,值輸入在密碼框裡。新出現的那一行會顯示名稱和它的引用 {{secret:STRIPE_SECRET_KEY}},以及一個複製引用的按鈕。這裡沒有顯示按鈕,也沒有複製值的按鈕,因為根本不存在把值返回給介面的程式碼路徑。

你在環境被組裝的地方引用它。 編輯代理 > 高階 > 環境變數,或者專案設定,或者提供商設定,或者一條開發命令列:

STRIPE_SECRET_KEY={{secret:STRIPE_SECRET_KEY}}

啟動時,主程序把值填進它即將建立的那個終端的環境裡。渲染程序看不到它,代理看不到它,手機也看不到它。金鑰管理器頁面把這些介面走了一遍。

代理發現的是名稱,不是值。 接上我們 MCP 工具的代理可以呼叫 secrets_list,拿回這台機器上的名稱列表。要派上用場,這就夠了:它可以說「你儲存過一個 STRIPE_SECRET_KEY,把 STRIPE_SECRET_KEY={{secret:STRIPE_SECRET_KEY}} 加到這個代理的環境裡,我就會用 $STRIPE_SECRET_KEY」。它讀不到值,哪怕正在執行一條用到這個值的命令。

我們發布出去的三個錯誤

上面這條規矩寫下來顯得理所當然。它其實是三個 bug 留下的疤,全都發生在 2026 年 9 月,全都由使用者報告。

工單裡引用的寫法,出來時被解析成了值

9 月 19 日。 Robert 在工單描述裡記了一條對映,就是那條完全合理的 STRIPE_SECRET_KEY={{secret:STRIPE_SECRET_KEY}},好讓接手這張工單的人知道該設什麼。透過 API 讀這張工單,引用原封不動地返回。但是從那張工單啟動的代理,在第一條訊息裡收到的是值。

原因是一處管道上的細節,後果卻是實打實的。工單的簡報會變成代理的第一條提示詞,而它是裝在啟動命令的一個環境變數裡傳過去的。我們的解析器會替換環境裡的每一個值,於是它把提示詞也一起替換了。金鑰上了磁碟,也去了模型提供商那邊。只能輪換,繞不過去。

修復辦法是讓代理的啟動命令保持字面:當環境裡帶有代理 id 時,承載第一條提示詞的那個變數原樣不動。使用同一個變數的開發命令沒有代理 id,仍然照舊解析。我們有意接受了一個副作用:寫進某個代理 CLI 選項裡的引用也不再被解析,而這是對的,反正命令列終究會進 ps、會進日誌。

教訓是:這扇門必須是刻意做窄的。 「哪裡可能有用就在哪裡替換」,正是金鑰最後跑到它絕不該出現的地方的那條路。

粘進訊息裡的引用,永遠不會被解析

9 月 22 日。 Cs040618 報告了相反的問題:「我把引用粘進對話裡,代理搞不懂這個金鑰是什麼。」

這不是 bug。訊息按設計就是字面文字,這恰恰是三天前那個修復本身。但在做這個動作的那一刻,應用裡沒有任何地方說明這一點,而收到訊息裡 {{secret:…}} 的代理,也完全不知道該回什麼。

於是有了三處修復,都是為了告訴人們門在哪裡:在輸入框裡貼上引用時彈出、並指向環境變數的提示;金鑰列表上方的說明文字,點名三個會替換引用的欄位,並寫明「永遠不要寫在訊息裡」;以及代理自身指令裡的一行,告訴它當引用出現在訊息裡時該怎麼回答。

「設定 > 機密」並不存在

9 月 28 日。 Mohammad 的代理提交了一份報告:使用者說他在設定 > 機密裡新增了 OTPIQ_API_KEY,三十分鐘裡儲存了好幾次,而 secrets_list 返回的仍然是同樣那三個舊金鑰。

儲存這條路本身沒問題。金鑰從來沒有到達過金鑰庫,因為設定里根本沒有機密頁面。桌面應用裡的六處幫助文案、本站的一個頁面、以及代理自身的指令,全都把人指向一個不存在的介面。設定裡確實存在的是環境變數那一塊,於是他就把令牌敲在了那裡,還加了花括號,而那並不是一個引用:永遠不會被替換,原樣發給代理,而且會隨專案同步,於是令牌離開了這台機器。

我們改掉了那些文案,讓它們寫出真正的路徑;讓面向代理的工具說清楚金鑰該在哪裡新增;並在環境變數那一塊加了一項檢查,會標出看起來像明文憑證的值、加了花括號卻不是引用的名稱,以及指向本機不存在的金鑰的引用。

這裡的教訓其實和金鑰無關:幫助文案裡寫錯路徑,就是一個安全 bug。 它會把使用者推到最近的那個看起來對的欄位上去。

如果你不用 AgentsRoom

規矩是可以搬走的。下面這些方案,挑一個適合你的;它們最終都落在同一個地方,也就是代理所在程序的一個環境變數。

  • 密碼管理器的 CLI。 1Password 的 op run -- claude,或者 Bitwarden 和 Doppler 裡對應的做法:工具從模板裡讀取引用,把真實的值注入子程序的環境,磁碟上什麼也不寫。
  • direnv 加一個被 gitignore 的 .envrc。 簡單、本地、按目錄生效。在放進第一個值之前就把 .envrc 和 .env* 加進 .gitignore,而不是之後。
  • 一個代理永遠讀不到的 .env 檔案。 把它放在儲存庫之外,在啟動代理的那個 shell 里載入它。如果這個檔案在儲存庫裡,代理遲早會開啟它、在訊息裡引用它,然後你又回到了起點。
  • 雲上的憑證:用短期的。 CI 裡用 GitHub OIDC,而不是長期有效的 AWS 金鑰;用工作負載身份,而不是服務帳號的金鑰檔案。

無論你選哪種,都把這條規矩寫進代理的指令裡,它就不會再問了。在 CLAUDE.md 或 AGENTS.md 裡寫一段就夠:永遠不要索取憑證的值,永遠不要把它寫進檔案,用 $NAME,並告訴使用者該設定哪個變數。我們發布了一個小技能,說的正是這些,一共五行。

如果一把金鑰已經過了一遍對話

按順序來,而且第一步不是可選的。

  1. 輪換它。 發出去的提示詞收不回來。把這個值當成已洩露,重新簽發一個。
  2. 找到那些會話記錄。 這些 CLI 會把對話檔案放在你的主目錄裡。在裡面搜舊值,匹配到的就刪掉或者截斷。
  3. 檢查那些會複製文字的地方。 剪貼簿管理器的歷史、給代理下簡報的那張工單或 issue、代理寫進儲存庫的任何摘要,以及在它持有金鑰期間提交過的話,還有 git log -p。
  4. 然後才去搭那扇門。 新值放到環境變數後面,按名稱引用,並更新代理的指令,讓下一個不再開口要。

第 4 步放在最後,是因為圍著一把已經洩露的金鑰搭一套乾淨機制,只不過是一次更整齊的洩露。

人們真正會問的問題

為什麼 API 金鑰絕不能寫進給 AI 代理的提示詞?

因為提示詞會被儲存,也會被轉發。Claude Code、Codex 和其他 CLI 都會把對話寫進你磁碟上的一個會話記錄檔案,而訊息本身會發給模型提供商。從那一刻起,這把金鑰還會留在你的剪貼簿歷史裡、留在任何原樣記錄這條訊息的日誌裡、留在複製了這段對話的重試和子代理裡。基於模式的脫敏只能抓住形如 sk- 或 glpat- 的東西,抓不住一個看起來像普通單詞的資料庫密碼。

AI 程式設計代理讀不到金鑰,又怎麼用它?

透過環境變數。你把值放進代理所在程序的環境變數裡,代理在命令裡寫 $STRIPE_SECRET_KEY 而不是那個值。命令執行時由 shell 展開。代理從頭到尾沒見過那串字元,所以它們不會進入它的對話,也不會進入它的會話記錄。

我能不能改成用命令列引數傳金鑰?

能,但別這麼做。命令列透過 ps 對機器上的每一個程序都可見,它會進入 shell 歷史、進入程序日誌,也會進入任何讀取已啟動命令的診斷工具。而單個程序的環境變數只有該程序和 root 讀得到,暴露面小得多。

AgentsRoom 把代理要用的金鑰存在哪裡?

存在你主目錄下的一個檔案裡,權限 0600,由你的作業系統鑰匙串加密,只在那台機器上。沒有任何內容同步到伺服器,也沒有任何按鈕能把值再顯示一次。應用顯示名稱,代理看到名稱,而值只在終端或命令啟動的那一刻、在主程序裡被解密。

如果我把金鑰引用粘進發給代理的訊息裡會怎樣?

什麼都不會被替換,這是有意為之。訊息、工單描述、技能和系統提示詞都是字面文字,所以寫在那裡的引用會原樣作為文字到達代理。只有環境變數的值會被替換。這一課我們是付了代價才學會的:以前寫在工單描述裡的引用,到了從該工單啟動的代理的第一條訊息裡,會變成已經解析過的樣子。

一把金鑰已經過了一遍對話,我該怎麼辦?

先輪換它,別做任何別的事,因為發出去的收不回來:它在提供商那邊,也在你磁碟上的會話記錄檔案裡。然後刪除或截斷包含它的會話記錄,檢查你的剪貼簿管理器,再 grep 一遍儲存庫,看代理有沒有把它寫進某個檔案。做完這些,才把新值放到環境變數後面。

簡版

代理不需要讀你的金鑰。它需要的是一個環境裡已經帶著這把金鑰的程序。

值只存一次,存在儲存庫之外,由作業系統加密。在環境被組裝的地方按名稱引用它。讓代理去寫 $NAME。其他所有通道一律保持字面,包括那些如果能替換會更方便的通道,因為方便的通道恰恰就是會被儲存、會被轉發的那些。

至於你出錯的那一天,這一天一定會來,先輪換,再收拾。

下載 AgentsRoom

在一個視窗中執行你所有專案的所有 AI 代理。

免費下載 AgentsRoom

配套應用:隨時隨地監控你的 Agent

使用 Claude、Codex、Antigravity CLI 或其他 AI 提供商。

獲取擴充功能
Chrome Web Store

把 Bug 和需求直接傳送到您的公開待辦清單。

多專案管理
多供應商
多代理執行
即時狀態
檔案差異與提交
行動應用
即時預覽
代理團隊
瀏覽器自動化
Backlog 驅動開發
提示詞庫
技能庫
檢視所有功能

繼續閱讀