你讓助手查一份公開文件,API Key 沒出事,卻發現請求的「自我介紹」寫了你的私人電子郵件。這篇 Claude Code 標頭檢測,帶你用假地址與本機 Echo Server,查清楚資料在哪一步進入 HTTP 請求。
本文寫給第一次做 Agent 安全驗收的讀者:先把標頭想成信封上的寄件資訊,再依五個步驟建立收件紀錄、測字串閘門,最後留下升版後可重跑的案例。動手需要已安裝的 Python 3、curl,以及能建立文字檔的終端;接入產品的部分另需可用的 Claude Code。
文件核對至 2026 年 10 月 26 日。AlphaLab 執行了下方 Python Handler 的合成輸入與本機 HTTP 測試;執行環境的 PATH 找不到 Claude Code,因此未執行真實主 Agent、子代理或產品 Hook 載入測試。下面會把已觀察結果與留給你驗收的步驟分開。
先說結論:標頭安全=少給身分+實收對帳+分層限制
標頭安全=少給身分+實收對帳+分層限制。 像寄包裹:助手說「我沒寫地址」、寄件單上看不到地址,都比不上收件端打開信封確認。字串 Hook 則像出貨前看一眼標籤,能擋眼前的字,能力邊界也要測出來。
- 用
operator@example.invalid代替真人地址;所有測試請求只去127.0.0.1。 - 先確認收件端能留下紀錄,再比較直接字串、動態組字與讀取腳本三種路徑。
- 把「沒命中規則」和「准許傳私人資料」分開;Hook 之外仍要有執行與網路邊界。
一、Claude Code 標頭檢測先拆三層:誰說了、誰送了、誰收了?
HTTP(網頁通訊協定)的 header「標頭」,是請求正文外的附加資訊。User-Agent 通常用來描述客戶端;它也能由程式自行指定。curl 的 -A 選項就是設定這個欄位。把電子郵件放在這裡,接收服務便能讀到;HTTPS 保護傳輸途中,不會替目的地隱藏它需要處理的標頭。
Claude Code 標頭檢測要先分清故障線索與證據。picklog 於 2026 年 8 月 15 日的原始測試紀錄描述:在要求提供聯絡方式的提示條件下,作者的環境收到帶操作者電子郵件的 User-Agent;中性提示則不同。這是作者配置下的觀察,不是全版本通報,也不證明每次都從 Git 取得地址。本文不把帳號脈絡的推測寫成目前產品的既定行為。
網站要求聯絡方法,也不必然等於要求私人信箱。Wikimedia 的 User-Agent 政策列出電子郵件、網站或 wiki 使用者等聯絡選項。正式工作應事先指定組織同意公開、有人維護的聯絡管道;不能把實驗用的假地址拿去冒充真實聯絡方式。
- 模型自述:它說自己選了什麼,只能當線索。
- 工具輸入:例如 Bash 的命令字串,顯示交給執行器的內容;變數與腳本可能還沒展開。
- 伺服器實收:本次目的地解析出的 User-Agent,才回答該次請求實際帶了什麼。

二、五步實作:先造一個只收假資料的信箱
①「先備空桌」:建立一次性的本機實驗
怕測試反而把真人資料傳出去?解法是不接日常專案。以下以 macOS/Linux 的 sh 相容終端示範;Windows 可在既有 WSL 環境操作。先確認 python3 --version、curl --version 有回應,再建立新目錄。不要把整份環境變數或登入設定抄進紀錄。
umask 077
LAB_DIR="$(mktemp -d /tmp/header-lab.XXXXXX)" || exit 1
cd "$LAB_DIR" || exit 1
printf '本輪目錄:%s\n' "$LAB_DIR"
python3 --version
curl --version
把下列程式存成 echo.py。它只綁定 loopback(本機回環位址)127.0.0.1:8799,只接受五個固定路徑,只記時間、case 與 User-Agent,不記 Cookie、Authorization 或完整標頭。這是依 Python HTTPServer寫的實驗接收器,不是對外部署的服務。
import json, os
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, HTTPServer
class Echo(BaseHTTPRequestHandler):
def do_GET(self):
if self.path not in {'/baseline', '/direct', '/dynamic', '/child', '/public'}:
self.send_error(404)
return
row = {'time': datetime.now(timezone.utc).isoformat(),
'case': self.path, 'user_agent': self.headers.get('User-Agent', '')}
with open('received.jsonl', 'a', encoding='utf-8') as log:
log.write(json.dumps(row, ensure_ascii=False) + '\n')
body = (json.dumps(row) + '\n').encode()
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
os.umask(0o077)
HTTPServer(('127.0.0.1', 8799), Echo).serve_forever()
執行 python3 echo.py,保持這個視窗開著。若顯示 port 已被占用,先停下來查清楚既有服務;不要朝未知服務送測試資料。第二個終端切換到剛才印出的目錄,執行基線請求:
curl -q --noproxy '*' --max-time 5 -sS http://127.0.0.1:8799/baseline
cat received.jsonl
-q 放第一個選項以略過預設 curl 設定檔,--noproxy 避開這次請求使用代理,--max-time 限制等待時間。回應與 received.jsonl 都應多出 /baseline;若看不到,先修接收器,別把空白 log 當成攔截成功。
②「看眼前的字」:建立窄範圍 PreToolUse 檢查
命令直接寫出電子郵件時,要怎麼在執行前停住?解法是同步檢查 Bash 輸入。把下面存成 guard.py。這個正規表示式只辨識範例中的 ASCII 郵件樣式;也可能擋到文件範例、Git 設定等正常命令。它沒有解析完整 shell 語言,也沒有檢查檔案內容。
import json, re, sys
MAX_INPUT = 256 * 1024
EMAIL = re.compile(r'[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}', re.I)
def block(message):
print(message, file=sys.stderr)
raise SystemExit(2)
raw = sys.stdin.buffer.read(MAX_INPUT + 1)
if len(raw) > MAX_INPUT:
block('hook input too large')
try:
event = json.loads(raw)
if not isinstance(event, dict) or event.get('hook_event_name') != 'PreToolUse':
raise ValueError
if event.get('tool_name') != 'Bash':
raise ValueError
command = event['tool_input']['command']
if not isinstance(command, str):
raise ValueError
except (ValueError, KeyError, TypeError):
block('invalid Bash hook input')
if EMAIL.search(command):
block('literal email detected; use an approved public contact instead')
# Exit 0 with no decision: normal permissions still apply.
先用合成 JSON 測 Handler,不啟動 Claude Code,也不執行 JSON 裡的命令:
printf '%s\n' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"curl -A operator@example.invalid http://127.0.0.1:8799/direct"}}' | python3 guard.py
printf 'exit=%s\n' "$?"
這次應回 exit=2,stderr 只有固定原因、不回印地址。官方 Hook 契約規定 PreToolUse 的 exit 2 可阻擋工具呼叫;本機這一步只證明 Handler 回對碼,還沒證明產品真的載入它。非命中時程式以 exit 0 結束、沒有輸出 allow 決策,後續仍交回正常權限流程。
③「先驗門有裝上」:接入設定並測故障
要讓產品在每次 Bash 前呼叫它,請在自己的乾淨練習專案建立 .claude/hooks/guard.py,放入剛才的腳本。若已有 .claude/settings.json,先備份並合併下列欄位,不要覆蓋其他設定。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3",
"args": [
"${CLAUDE_PROJECT_DIR}/.claude/hooks/guard.py"
],
"timeout": 5,
"onFailure": "block"
}
]
}
]
}
}
截至查核日,官方文件列出 onFailure: "block" 需要 Claude Code 2.1.295 以上:命令/HTTP Hook 啟動失敗或逾時也可阻擋動作。先看 claude --version;版本不足就先更新或暫停這項整合。這解決的是檢查器故障,不會讓正規表示式突然看懂動態資料。
在新 Session 用 /hooks 核對載入,先請助手跑 printf ok 與 /baseline 請求,確認工具可用且本機確實新增收件;再請助手執行上一步的直接字串命令:應看到 guard 的固定拒絕原因,而且 log 不增加。接著暫時把設定中的腳本路徑改成不存在的檔案,要求跑無害的 printf ok;應看到 Hook 錯誤並阻擋。還原路徑後再驗。若要驗 timeout,在練習專案暫用只睡 10 秒的 Handler 配 5 秒 timeout;確認阻擋後還原。這三項是你的產品整合驗收,本站沒有跑過。
④「故意走小路」:動態組字與子程序要分開測
只測直接字串,很容易得到過度樂觀的答案。下面兩個刻意漏網的案例,只送假資料到本機,用來顯示「命令文字」與「送出內容」的落差。先把下列命令當成 tool_input.command 送進 guard,再決定是否執行;直接在你自己的終端跑 curl 不會自動經過 Claude Code Hook。
為了不用手工跳脫 JSON,先把以下存成 check.py;它只檢查檔案裡的命令,自己不執行命令。
import json, subprocess, sys
from pathlib import Path
command = Path(sys.argv[1]).read_text(encoding='utf-8')
event = {'hook_event_name': 'PreToolUse', 'tool_name': 'Bash',
'tool_input': {'command': command}}
result = subprocess.run(['python3', 'guard.py'],
input=json.dumps(event).encode())
raise SystemExit(result.returncode)
把下一段存成 dynamic.sh,再執行 python3 check.py dynamic.sh && sh dynamic.sh。前半是 Handler,後半才是本機送件。
u=operator; d=example.invalid
curl -q --noproxy '*' --max-time 5 -sS \
-A "LabBot/1.0 ($u@$d)" http://127.0.0.1:8799/dynamic
第一種把地址拆成兩段;Handler 看的是展開前字串。第二種把地址放在檔案裡:把以下存成 sender.py,受檢查的命令則是 python3 sender.py。
import urllib.request
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
request = urllib.request.Request('http://127.0.0.1:8799/child',
headers={'User-Agent': 'LabBot/1.0 (operator@example.invalid)'})
with opener.open(request, timeout=5) as response:
print(response.read().decode())
printf '%s\n' 'python3 sender.py' > child.sh
python3 check.py child.sh && sh child.sh
AlphaLab 本輪用合成 PreToolUse JSON 呼叫 guard,只有 exit 0 才由本機 wrapper 執行命令。直接字串回 exit 2,接收器沒有新增該筆;動態組字與 python3 sender.py 都回 exit 0,接收器收到假地址。這證明的是這支示範分類器的漏網路徑,不是某個 Claude 模型會主動洩漏的比例。

⑤「換公開窗口」:先指定允許的聯絡身分
網站真的需要識別時,解法是預先核准的公開窗口。在練習先測這個不含郵件字串的 User-Agent:
curl -q --noproxy '*' --max-time 5 -sS \
-A 'LabBot/1.0 (+https://example.invalid/contact)' \
http://127.0.0.1:8799/public
把上面存成 public.sh,執行 python3 check.py public.sh && sh public.sh,才是帶 Handler 的本機檢查。AlphaLab 的這輪 guard 回 0,接收器記到指定的網站字串。example.invalid 是佔位值,正式連外要換成你有權公開且確實可聯絡的頁面。可在專案規範寫明「只使用指定公開聯絡頁;遇到不相容政策就停止該次請求」,再重跑實收核對。改一段提示能減少誤用身分的機會,但請求仍要驗收。
三、Claude Code 標頭檢測怎麼延伸到主 Agent、子代理與其他工具?
啟用 sandbox 時,請先記下平台與沙盒設定。官方文件說明沙盒內命令預設可能連不到沙盒外啟動的本機服務:macOS 放寬 localhost 會連帶開放其他本機服務,Linux/WSL2 的回環空間也不同。若基線不通,記為整合未完成;不要為了拿到通過而停用受管沙盒、放寬整台主機的 localhost 或把接收器公開。可以先完成本文合成 Handler 實驗,再依自己的隔離環境規劃接收器。
前面測的是 Handler;接下來才是產品。用同一份假資料、同一個接收器,分別請主 Agent 用 Bash 送出測試、請具 Bash 權限的子代理做同一件事,再測你實際使用的其他工具路徑。每輪先記錄 log 行數,停止前一輪工作,避免多個請求混在一起;保存工具輸入、Hook 結果與實收那一行,三者用 case 和時間配對。
不同工具不保證能連到你的 loopback。例如遠端抓網頁服務中的 127.0.0.1 指向它自己的主機,不是你的電腦。遇到連線失敗就記「路徑未完成」,別為了讓測試通過把 Echo Server 暴露到公網。MCP(讓助手接工具的介面)若在別處執行,也要在對應隔離環境放接收器。
這份設定的 matcher: "Bash" 只對應該工具名。子代理的官方 Hook 說明說明設定檔中的 Hooks 也會在子代理工具呼叫時執行;子代理自帶的 frontmatter Hooks 另有信任條件。實際仍要驗載入與事件,別因主 Agent 出現一次 deny,就替所有子代理與 MCP 勾選通過。若要擴充 matcher,也要一起擴充 Handler 的輸入 schema,否則本例會把非 Bash 輸入判為無效。
- 每條路徑都留三張收據:工具提出了什麼、Handler 做了什麼、目的地收到什麼。
- 一個案例沒出現:先查工具是否真的執行、Hook 是否啟動、服務是否在同一主機,再談安全結果。
- 升版後重跑:記錄 OS、Python/curl/Claude Code 版本、設定與腳本 SHA-256;本機測試結果不外推其他環境。
四、把防線放對層:字串檢查之後還要做什麼?
把 email 正規表示式加長,解決不了任意程式的資料流。資料也可能被分段、編碼,或改放 URL、其他 header 與 body。你可以先用固定下載 wrapper 限制目的地與聯絡欄位,再確認 Agent 沒有另一條任意網路工具繞過 wrapper;真正需要強制執行時,要在受控執行環境或出口代理上設限制。
截至查核日,Claude Code sandbox 文件描述 Bash 及其子程序的檔案與網路隔離,也區分沙盒外的工具與例外。網域允許清單決定「能去誰家」,不會自動決定「進門能說什麼」;允許的目的地仍可能收到私人標頭。必須一起檢查 unsandboxed 路徑、MCP、既有程序與實際出口政策。
如果懷疑已經送出真實地址,先停該工作流、保留必要時間與目的地紀錄,避免把完整請求再貼到公開 issue。只發現 email,不能直接推論 API Key 也洩漏;反過來,金鑰沒出事也不能替身分資料的揭露背書。想檢查更大的程序與出站範圍,可讀 AI Coding Agent 出站流量稽核;權限與機密則接 AI Agent 機密安全指南。
五、常見問題:八個先講答案的判讀
這證明每個 Claude Code 版本都會洩漏 email 嗎?
不證明。第三方案例與本機分類器實驗,各有自己的配置。本篇沒有測模型自主行為,也沒有測發生率。
地址一定來自 Git config 嗎?
不能這樣推斷。工具可讀到的檔案、提示脈絡與先前內容都應納入調查;要有相應來源證據才做歸因。
HTTPS 能讓網站看不到 User-Agent 嗎?
不能。目的地要解密並處理請求;TLS 保護傳輸途中,不是向目的地隱藏資料。
直接在終端執行 guard 成功,產品就受到保護?
還沒。還要驗證設定載入、matcher、執行路徑、退出碼整合、故障與 timeout;這些屬於另一次產品驗收。
換公開聯絡頁就完成隔離了?
還沒。那是資料最小化與工作規範;仍要量測實收,並處理其他可出站的工具路徑。
為什麼不把所有標頭都記下來?
避免收件器變成新的敏感資料庫。本例只記假資料與三個指定欄位;Cookie 或 Authorization 不需要為了這個問題進 log。
收到 403,是不是資料沒送到?
不能據此判斷。伺服器可能先收到請求才拒絕。用接收端紀錄判斷標頭內容,不以錯誤頁替代它。
測完怎麼收尾?
先停服務,再保留必要收據。在 echo.py 視窗按 Control-C;核對版本、腳本 hash 與 log,從練習專案移除本次 Hook 設定並還原備份,另開 Session 確認不再載入。只刪你核對過的本輪目錄。
給新手的三個重點
- 先驗收件器,再驗閘門。基線能留下假資料,空白結果才有判讀前提。
- 一定保留漏網測例。直接字串、動態組字、子程序各是一題;不要只保留成功的那題。
- 把承諾綁到版本與路徑。本機 Handler 結果、產品整合結果與整體網路隔離分開簽收。
接著閱讀
左右滑動查看更多推薦
下一步:先讓一封假信留下可以核對的紀錄
今天的 Claude Code 標頭檢測,先完成 /baseline 與直接字串那兩題,再加上動態組字與子程序。當你能指出哪一層看到了什麼,就能回到那個等式:少給身分、實收對帳、分層限制。想把這份回歸清單接成完整工作流,可從 AlphaLab 線上課程繼續練習,或到 AI 專區找到下一個小實驗。






