你沒叫它讀,它還是讀了:Agent SDK 的 settingSources 與關不掉的五樣東西
你用 Agent SDK 寫的那支 agent,這一刻很可能正在讀你的 ~/.claude/CLAUDE.md。你沒有叫它讀。
平常不會有人發現這件事。在自己的機器、自己的 repo 裡,agent 的行為跟你預期的一樣,因為你的預期本來就是那台機器上的設定養出來的。要等到把同一份程式碼交給第二個人跑、或是包成服務丟上另一台機器,行為才開始飄。那時候大家第一個翻的是 prompt 跟 model,很少人會回頭去看它是從哪個目錄啟動的。
反方向的抱怨也一樣常見:專案裡明明放了 CLAUDE.md、.claude/skills/、hooks,agent 卻像沒看到一樣。
兩種抱怨的方向完全相反,管它們的是同一個選項:settingSources。
省略它,等於你寫了三個值
文件講得很白:省略 settingSources 時,query() 會讀跟 Claude Code CLI 一樣的檔案系統設定,包含 user、project、local 設定、CLAUDE.md 檔案,以及 .claude/ 底下的 skills、agents、commands。省略等同於 ["user", "project", "local"]。
要完全不讀這些,傳 settingSources: [],agent 就只剩你用程式設定的東西。
心理落差就在這裡。寫 SDK 的時候,腦子裡的模型通常是「我建一個物件,我傳什麼它就有什麼」。但一支跑在你機器上的 agent 比較像租屋的房客:你可以決定自己搬哪些家具進去,房子本來就有的水電管線、門鎖、還有房東貼在牆上那張公告,不會因為你沒提就消失。
1 | import { query } from "@anthropic-ai/claude-agent-sdk"; |
Python 那邊寫成蛇形命名的 setting_sources=["user", "project"],放進 ClaudeAgentOptions,值完全一樣。
合法值就三個,沒有第四個:
| 來源 | 載入什麼 |
|---|---|
"project" |
專案 settings.json 與 hooks;專案 CLAUDE.md 與 .claude/rules/*.md;專案 skills、commands、subagents |
"user" |
使用者 settings.json;使用者 CLAUDE.md 與 ~/.claude/rules/*.md;使用者 skills、commands、subagents |
"local" |
CLAUDE.local.md、.claude/settings.local.json |
同一個 "project",兩套找檔案的規矩
同樣掛在 "project" 底下的東西,往上找的距離不一樣。
專案 settings.json 與 hooks 只從 <cwd>/.claude/ 載入,沒有父目錄 fallback。CLAUDE.md 與 rules 有:<cwd> 加上每一層父目錄都會看。skills、commands、subagents 則是從 <cwd> 往上找到 repo 根為止,另外再加上你用 additionalDirectories(Python 是 add_dirs)傳進去的每個目錄底下的 .claude/skills/、.claude/commands/、.claude/agents/。
monorepo 的人最容易在這裡撞牆。你在 packages/api/ 底下啟動 SDK,repo 根的 CLAUDE.md 照樣被讀進來,根目錄那組 hooks 卻一個都沒掛上。字串是同一個 "project",行為分成兩套。
先說清楚這篇的邊界:以下所有選項名、合法值、預設行為都是從官方文件抄回來的,我只讀了文件,沒有實際跑過。那條父目錄的不對稱我特別在意,但我沒有真的開一個 monorepo 從子目錄啟動 SDK 去撞它。要拿去做佈署決策的話,麻煩自己先在測試專案驗一次。
轉到底也關不掉的那五樣
settingSources: [] 這一行看起來像把門關死了。文件列了五樣東西,不論 settingSources 設什麼都會被讀進來。
| 輸入 | 行為 | 怎麼關掉 |
|---|---|---|
| Managed policy settings | 端點管理政策(MDM plist、registry policy、managed settings 檔)從主機載入;組織下發的 server-managed settings 在憑證合格時會被抓下來 | 端點政策要從主機移除那個檔;server-managed 的部分由你組織的 Owner 控制,你從 SDK 關不掉 |
~/.claude.json 全域設定 |
永遠讀 | 用 env 裡的 CLAUDE_CONFIG_DIR 搬走 |
| Auto memory | session 開始時載入系統提示 | autoMemoryEnabled: false,或 env 設 CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 |
| claude.ai MCP connectors | session 用你的 claude.ai 登入驗證時會載入 | strictMcpConfig: true 或 disableClaudeAiConnectors: true,或 env 設 ENABLE_CLAUDEAI_MCP_SERVERS=false |
~/.claude/settings.json 的 sandbox.credentials deny 與檔案 mask |
指令沙箱運作時照樣套用 | 從 ~/.claude/settings.json 把那些項目移掉 |
connector 那一列藏了一條很容易走進去的死路。直覺上,你要關掉外部 MCP server,會去傳一個空的 mcpServers: {}——文件明講這個做法不會壓制那些 connector。空物件的意思是「我沒有要加任何 server」,不是「這個 session 不准有 server」,兩件事在這裡不等價。真的要關,得走上面那三個開關。
agent 寫新記憶用的是標準的 Write 與 Edit 工具,沒有專用的 memory 工具。所以你的 allowedTools 如果沒開那兩個,auto memory 那一列就算讀進來了,記憶還是存不進去。它本來就跟一般檔案寫入共用同一條路,沒有刻意藏起來的意思。
多租戶的人請把下面這句抄進 code review 清單。文件對這件事下了一句硬話:
Do not rely on default
query()options for multi-tenant isolation.
文件給的正確做法是:每個租戶跑在自己的檔案系統,並設 settingSources: [] 加上 env 裡的 CLAUDE_CODE_DISABLE_AUTO_MEMORY=1。而且它把話講死了,組織下發的那份設定在行程用組織憑證驗證時就會被抓下來,檔案系統隔離移除不了它。
我的立場:任何要交出去給第二個人、第二台機器跑的 SDK 專案,settingSources 那一行都該明寫,不管你要的是三個值全開還是空陣列。靠預設值等於把 agent 的行為綁在「誰的機器」上,而這件事不會出現在你的 diff 裡。會讓我收回這個立場的條件也很具體:哪天 SDK 在 session 開始時把實際載入的來源清單印出來,明寫這一行就只剩文件價值。在那之前,它是這份程式碼唯一的自我說明。
「專案的會蓋過使用者的」沒有這回事
CLAUDE.md 的載入位置攤開來有七層:<cwd>/CLAUDE.md、<cwd>/.claude/CLAUDE.md、專案 rules、cwd 之上各層目錄的 CLAUDE.md、cwd 子目錄裡的 CLAUDE.md、CLAUDE.local.md、還有 ~/.claude/CLAUDE.md 跟使用者 rules。其中子目錄那層比較特別,它是 agent 讀到該子樹的檔案時才按需載入,其餘的在 session 開始時就進來了。
重點在後面這句:所有層級是相加的,而且層級之間沒有硬性優先序。指示衝突時,結果取決於 Claude 怎麼詮釋。
這跟很多人腦中的 CSS 模型不一樣。大家預設「愈靠近的設定愈晚套用、所以贏」,但這裡沒有那個機制。文件給的建議只有兩條:寫不衝突的規則,或是在比較具體的那份檔案裡明寫優先序——它給的範例句是「These project instructions override any conflicting user-level defaults」。也就是說,優先序不是系統幫你排的,是你用自然語言拜託它照做的。
順帶一提,中途改 CLAUDE.md 那篇的前提是這份檔案已經被讀進來了,接著才輪到「改了什麼時候生效」。這篇還停在前面一格。
skills 只長在檔案系統上
skills 是透過 settingSources 從檔案系統被發現的。query() 的 skills 選項省略時,已發現的 user 與 project skills 全部啟用、Skill 工具可用,行為跟 CLI 一致。要控制就傳 "all"、skill 名稱清單、或 [] 全關。
設了 skills,SDK 會自動把 Skill 工具加進 allowedTools。你要是另外傳了明確的 tools 清單,這份好意就被蓋掉了,得自己在那份清單裡補一個 "Skill",否則 Claude 叫不動任何 skill。兩個選項各自都是對的,合在一起就會互相抵消。開場講的第二種抱怨,專案裡放了東西卻叫不動,很多時候就死在這裡,跟 settingSources 一點關係都沒有。反過來想繞過檔案系統也不行,skills 必須是檔案系統產物(.claude/skills/<name>/SKILL.md),SDK 沒有以程式註冊 skill 的 API。想把 skill 從資料庫撈出來動態塞進去的,這條路沒有開。落檔,或者不用。
hooks 有兩條路,只有一條歸它管
skills 只有檔案系統一條路,hooks 剛好相反,它有兩條,而且兩條會同時存在。
檔案系統 hooks 是你寫在 settings.json 裡的那些,settingSources 含對應來源時才載入。型別有五種:"command" 跑 shell 指令、"http" POST 到一個端點、"mcp_tool" 呼叫已連線 MCP server 的工具、"prompt" 丟一段提示給 LLM 評估、"agent" 直接生一個驗證 agent 出來。後面那三種比很多人印象中的「hook 就是跑腳本」寬很多。
程式化 hooks 是你傳給 query() 的 callback,跑在你自己的應用程式行程裡,可以回傳結構化決策。這條路不歸 settingSources 管,你設成空陣列它照樣在。
兩條路有個共通點值得記:都會在主 agent 跟它生出來的任何 subagent 裡觸發。程式化那條的 hook input 第一個參數帶 agent_id 與 agent_type,你可以據此分辨是哪個 agent 觸發的。回傳 {} 代表放行;要擋就回傳含 permissionDecision: "deny" 與 permissionDecisionReason 的 hookSpecificOutput 物件,而那個 reason 會被當成 tool result 送回給 Claude,所以它值得好好寫,那是模型唯一看得到的理由。
TypeScript SDK 支援的 hook 事件比 Python 多,文件點名的是 SessionStart、SessionEnd、TeammateIdle、TaskCompleted 這四個。挑語言之前先看一下你要掛的那個事件在不在。
問三個問題,不要問你傳了什麼
判斷一支 agent 讀得到什麼,翻自己的程式碼是查不出來的。程式碼只管得到其中一部分,而且不是最大那部分。
它從哪個目錄啟動?這決定了 project 與 local 兩層看得到什麼,也決定了 monorepo 裡那組 hooks 掛不掛得上。
它用誰的憑證登入?connector 跟著登入走,組織下發的設定也跟著登入走。
它跑在誰的機器上?端點管理政策、~/.claude.json、~/.claude/settings.json 裡那些沙箱限制,都是那台機器的屬性,跟你建的那個 options 物件無關。
settingSources 只動得了第一個問題,而且只動得了一半。剩下兩個問題的答案不在你的程式碼裡,在那台機器上。







































































































































































































