允許規則留住了密碼
11 月 6 日,Apiiro 指出 Claude Code 的「Yes, and don't ask again」按鈕會把核准的指令連同其中的機密存進 .claude/settings.local.json,並發現 1,122 個公開 GitHub 儲存庫把這個檔案連同看似有效的密碼、權杖和金鑰一起提交了。兩款常用的機密掃描器漏掉了其中 215 個。在 Bromure Agentic Coding 中,代理能打出的每一個機密都是替身,所以存下來的規則裡只有一個在哪裡都用不了的值,而每一次 push 都會在 Mac 上等你回應。
你按下「Yes, and don't ask again」,是為了不再看到同一個提示。Claude Code 為了下次認得它,會把整條指令連同密碼,存進你專案裡的一個檔案。
11 月 6 日,Apiiro 發表了關於編碼代理透過「don't ask again」洩漏機密的研究。The Hacker News 隔天早上做了報導。
機制很平常。當 Claude Code 要求執行一條 shell 指令,而你選了 Yes, and don't ask again,它就會在專案的 .claude/settings.local.json 裡寫入一條允許規則。有時規則是像 npm test * 這樣的模式。有時是一字不差的指令本身,而如果那條指令在 PGPASSWORD=、sshpass -p 或 mysql -p 後面帶著密碼,這個密碼現在就躺在你程式碼旁邊的 JSON 檔案裡。
Apiiro 在公開的 GitHub 上搜尋這個檔案,找到:
- 103,617 個儲存庫中有 104,185 份副本;
- 1,122 份副本至少含有一個機密,總計 1,379 個不同的機密;
- 379 組資料庫密碼、43 組公開伺服器的 SSH 登入、42 個 GitHub、GitLab 和 npm 權杖,以及 23 把 AWS 金鑰;
- 屬於 63 個組織的機密,其中包括一家資料庫廠商具管理員權限的雲端金鑰,以及一個醫療紀錄資料庫的擁有者密碼。
Apiiro 檢查時,這些檔案全都還在預設分支上。在一半的儲存庫裡,它們已經待了至少 217 天。較舊的提交在 509 個儲存庫中還藏著另外 734 個機密,74 個儲存庫的 211 個分叉也帶著副本。把共用的 .claude/settings.json、歷史紀錄和保存下來的代理對話紀錄都算進去,Apiiro 在 1,994 個儲存庫中共數到 2,539 個機密。
為什麼它一路暢通
Claude Code 確實試過把這個檔案擋在 git 之外。它第一次寫入 settings.local.json 時,會把檔案加進你的全域 git 排除設定。但這條規則只存在於一台電腦上。同事的筆電、CI 執行器或新的複本都沒有它,對已經提交的檔案也毫無作用。Claude Code 從不把這個檔案加進專案自己的 .gitignore。在 Apiiro 比較的 16 款編碼代理中,Claude Code 是唯一一款被確認預設會把完整的核准指令存進專案檔案的。
機密掃描器也好不到哪裡去。Apiiro 用 gitleaks 和 TruffleHog 掃過每一份副本。在 1,122 個檔案中,gitleaks 標出 564 個,TruffleHog 標出 643 個,有 215 個兩者都沒抓到。這 215 個大多是打在指令裡的密碼。掃描器找的是 AKIA… 或 ghp_… 這類已知的金鑰格式,而 sshpass -p hunter2 哪一種都不符合。
使用者一年多前就回報過這個問題。2025 年 8 月的 issue anthropics/claude-code#5544 指出,Claude Code 會把含有 API 金鑰的核准指令原封不動地存進這個檔案。它在 60 天沒有動靜後被機器人以「not planned」關閉。另外四份相關回報也遇上同一個機器人,最近的一份就在 The Hacker News 文章的前一天。
機密在指令裡,是因為它在代理手上
這 1,379 個機密,每一個都是代理打得出來的值。可能是開發者貼進了對話,或留在 .env 檔案裡,或是代理從 ~/.aws/credentials 或 ~/.pgpass 讀出來,直接嵌進它想執行的指令。允許規則只是把代理原本就有的東西記了下來。
Apiiro 對未來的建議也指向同一個方向:讓機密遠離指令,改為引用機密管理工具中的變數,讓存下來的規則只含 $DB_URL。前提是每位開發者和每個代理每次都照做。一個想讓失敗指令跑起來的代理,會把變數展開。Bromure 走的是另一條路:讓真正的值完全不進入代理的環境。
同樣一下點擊,在 Bromure Agentic Coding 裡
Bromure Agentic Coding 在你 Mac 上的 Ubuntu VM 裡執行 Claude Code、Codex、Grok、Kimi 和 omp。代理透過我們所說的線上邊界取得憑證:在 VM 裡,每一個機密都是替身,真正的值加密存放在 Mac 上,主金鑰在 macOS 鑰匙圈裡。Mac 上的代理伺服器在線路上把替身換成真正的值,而且只在送往該憑證所屬主機的請求上這麼做。
所以當代理要求執行一條帶著權杖的指令,而你選了 Yes, and don't ask again,Claude Code 照樣把指令存下來。只是其中的權杖是替身。
代理能打出的每一個機密都是替身
替身保留工具預期的格式,所以專案裡什麼都不用改:
| Apiiro 發現外洩的 | 代理在 Bromure 機器裡拿到的 |
|---|---|
| GitHub 和 GitLab 權杖 | ghp_ + 36 個字元、glpat- + 20 個,放在 GH_TOKEN、GITLAB_TOKEN 和 ~/.git-credentials |
| npm 權杖 | NPM_TOKEN 中的 brm_… 值,來自限定 registry.npmjs.org 的 Other API key 項目 |
| AWS 金鑰 | 真的存取金鑰 ID 配上假的秘密金鑰;Mac 用真的金鑰重新簽署每個請求 |
| 資料庫密碼 | 你指定的變數中的 brm-db-… 值,適用 ClickHouse、Mongo Data API 和 Elasticsearch |
| SSH 登入 | 完全沒有密碼:只有由留在 Mac 上的金鑰產生的簽章 |
以代理可能對 ClickHouse 端點執行的一條指令為例:
curl -u default:brm-db-7Qk2…Xw9 \
'https://analytics.example.com:8443/?query=SELECT+count()+FROM+events'
把它永久核准,允許規則裡存的就是 brm-db-7Qk2…Xw9。代理伺服器會在送往 analytics.example.com 的請求中替換替身,Basic-auth 標頭和請求本文都會換,因為資料庫機密常常夾帶在 SQL 和 JSON 裡。換到其他任何地方,這串字都毫無用處。
至於 AWS,機密根本不會進到 VM。代理的 ~/.aws/config 指向一個輔助程式,它交出真的存取金鑰 ID 和一組 40 個字元的假秘密。AWS CLI、boto3 和 terraform 用假的簽署,Mac 則在請求送出前拿掉這個簽章,再用真的秘密重新簽署。真的秘密金鑰從不存在於 VM 中,所以代理寫出的任何指令都不可能含有它。
Apiiro 統計中的 43 組伺服器登入屬於 SSH。Bromure 機器的 SSH_AUTH_SOCK 是通往 Mac 的橋接:VM 可以請求簽章,但私鑰的位元組永遠不會跨過來。由 YubiKey、Secretive 或 1Password 保護的金鑰可以逐一提供,觸碰確認會出現在你的 Mac 上。代理從不需要 sshpass -p,所以也沒有密碼可讓允許規則存下。
GitHub 上的替身什麼也打不開
假設檔案真的到了公開儲存庫、一個分叉和十幾個舊提交裡。把它抓走的人拿到的是 ghp_ 加上 GitHub 從未發出的 36 個字元。從他自己的筆電送出,驗證就會失敗。只有你 Mac 上的代理伺服器知道它對應哪個真正的權杖,而那台代理伺服器只服務你機器的 VM。
如果替身哪天從機器內部被送往錯誤的主機,代理伺服器會把它當成外洩企圖。它以 HTTP 451 擋下請求、暫停 VM,並跳出標題為 This environment may have been compromised 的警示,列出該憑證、它所屬的主機,以及它被送往的主機。因此每個替身不只是佔位符,也是一條絆線。
輪替只需在一處完成
Apiiro 的第一條指示是:在清理任何東西之前,先輪替檔案裡曾經出現過的每一組密碼、權杖和金鑰。當真正的機密散落在 .env 檔案、shell 歷史紀錄和代理設定裡,這就成了一場尋寶。在 Bromure 中,真正的值只放在一處:Mac 上機器的 Credentials 面板。把新權杖貼進去,VM 裡的替身維持不變,機器內的允許規則、指令碼和設定都能繼續運作。
Other API key 也可以是一個 1Password 參照,例如 op://Engineering/npm/token。Bromure 在 Mac 上解析它,並每 2 分鐘重新解析一次,所以在 1Password 裡輪替的金鑰幾分鐘內就會生效,不用重新啟動,VM 裡也沒有東西要改。
把機密移進這個面板只要一步。Import env file… 會讀取現有的 .env 或 ~/.bashrc,以遮蔽的值列出變數,再把它們轉成憑證。專案資料夾從此不再需要那個代理用來讀密碼的 .env 檔案。
push 會等你
在 Apiiro 追溯的儲存庫中,有 539 個是由一個把 Claude 列為共同作者的提交改動了這個檔案。代理在完成任務的過程中會提交並 push。
在新的 Bromure 機器上,Guardrails 面板裡的每個程式碼託管服務一開始都是 Prompt before write。讀取照常通過。透過 HTTPS 的 git push 會停在 Mac 上一個顯示該操作的對話框,在你回應之前什麼都不會送出。這項政策在代理伺服器中執行,所以 VM 裡沒有任何東西能把它關掉。
會讀那一行的審查者
沒有任何掃描器抓到的 215 個機密,都是指令裡的密碼。一個模型在已提交的 JSON 檔案中讀到 PGPASSWORD=… psql -h db.example.com,就知道自己在看什麼。
Bromure 的程式碼審查會用你某台機器上的代理監看一個 GitHub 儲存庫,時機可選 Every new commit、Every new pull request 或每週排程,Bromure 也會跨次掃描去除重複的發現。對應 Apiiro「搜尋整個組織,包括歷史紀錄」這一步,Run Baseline Now 會對整個儲存庫啟動一次性審查。GitHub 由 Mac 輪詢,審查代理用的也是和其他代理一樣的替身。
對話紀錄裡留下的也是替身
Apiiro 的統計包含保存下來的代理對話紀錄。VM 裡 Claude Code 自己的工作階段檔案記錄了代理看到的內容,而在 Bromure 機器裡,那就是替身。Mac 上 Bromure 的追蹤紀錄把權杖替換保存成像 sk-a…f9q3 這樣的簡短預覽,真正機密的位元組從不進入任何追蹤紀錄。如果流量中還是出現了看似真實的機密,例如有人手動把它貼進 VM,追蹤紀錄會把它標為可能的外洩,bromure-cli trace leaks 則會全部列出。在 Guardrails 中開啟 Only allow credentials Bromure injects,任何帶著非 Bromure 注入之憑證的請求都會被擋下,並觸發入侵警示。
每條指令裡都是替身
VM 中的權杖、資料庫機密和 AWS 金鑰都是替身。允許規則存下的值,只有透過你的 Mac 才用得了。
SSH 不需要密碼
VM 向 Mac 請求簽章。私鑰永遠不會跨過來,也從不需要 sshpass -p。
push 在 Mac 上等待
新機器預設就是 Prompt before write。push 會等你回應,由代理伺服器強制執行。
輪替一次就好
真正的值放在一個面板裡,或在 1Password 中。在那裡改,每個替身都照常運作。
這週該做的事
摘自 Apiiro 的清單,適用所有使用編碼代理的團隊:
- 在你的 GitHub 組織中搜尋
.claude/settings.local.json和.claude/settings.json,包括歷史紀錄。 - 輪替這些檔案曾經含有的每一個機密,然後再清理。
- 執行
git rm --cached .claude/settings.local.json,並把它加進專案的.gitignore和組織的範本。 - 在 pre-commit 和 CI 掃描中,把
.claude/、.cursor/、.gemini/和.specstory/當成.env檔案看待,並確認掃描器抓得到指令裡的密碼。
接著,把真正的機密移出代理碰得到的範圍。在 Bromure 機器裡,不論開發者那天有沒有記得 Apiiro 的規則,下一次「don't ask again」存下的都是替身。
關於代理把機密複製進歷史紀錄和日誌的更廣泛模式,請見權杖留在了對話紀錄裡。想了解替換的詳細運作方式,請閱讀手冊的 Credentials 章節。