Claude Code features in the SDK
setting_sources is the option that decides which files on disk a session reads. CLAUDE.md, project settings, hooks, skills, commands and subagent files all arrive through it.
Last updated: 20 Oct, 2026 · Claude Agent SDK
The system prompt is written in your code. Everything in this lesson is written in files, the same files the Claude Code CLI reads, which is what lets one project serve both.
What each source loads, and from where
| Source | What it loads | Where it reads |
|---|---|---|
"project" | Project settings.json and hooks, project CLAUDE.md and .claude/rules/*.md, project skills, commands and subagents | <cwd>/.claude/ for settings and hooks; <cwd> and its parents for CLAUDE.md; plus the .claude/ folders of every add_dirs entry |
"user" | User settings, user CLAUDE.md and rules, user skills, commands and subagents | ~/.claude/ |
"local" | CLAUDE.local.md and .claude/settings.local.json | <cwd> and <cwd>/.claude/ |
Leaving the option out is the same as passing all three, which is what the CLI does. An empty list turns the three off: claude-code-features.
Reading setting_sources on the command line
from claude_agent_sdk import ClaudeAgentOptions
from flags import flags
# three ways to answer the question, and what each sends to Claude Code
print(flags(ClaudeAgentOptions()))
print(flags(ClaudeAgentOptions(setting_sources=["project"])))
print(flags(ClaudeAgentOptions(setting_sources=[])))- Unset sends no flag at all, and Claude Code reads user, project and local.
- A list sends
--setting-sources=project, and only that one is read. - An empty list sends
--setting-sources=with nothing after it, so no settings file is read.
Putting the shop's rules in CLAUDE.md
CLAUDE.md is where a project keeps the context it wants every session to have: conventions, commands, decisions. The SDK reads it when the matching source is on and puts its content into the conversation, which is why it works with any system prompt.
# The Hilltop shop desk
- Orders ship from the Hilltop warehouse.
- No refunds without a manager.shop_desk.py gains sources and project_notes, and rules is replaced so that the file's text is added to whatever the system prompt said. sources answers the same question the SDK answers when it builds the flag.
def sources(self):
"""Which filesystem settings this session loads."""
chosen = self.options.setting_sources
if chosen is not None:
return list(chosen)
# the SDK narrows an unset list to user and project as soon as skills is set
return ["user", "project"] if self.options.skills is not None else ["user", "project", "local"] def project_notes(self):
"""CLAUDE.md, read only from the sources this session loads."""
root = Path(self.options.cwd or ".")
files = []
# one list of candidates, each added only when its own source is loaded
if "project" in self.sources():
files += [root / "CLAUDE.md", root / ".claude" / "CLAUDE.md"]
if "local" in self.sources():
files.append(root / "CLAUDE.local.md")
if "user" in self.sources():
files.append(Path.home() / ".claude" / "CLAUDE.md")
return "\n".join(path.read_text() for path in files if path.exists()) prompt = prompt if isinstance(prompt, str) else prompt.get("append", "")
return prompt + "\n" + self.project_notes()import re
from pathlib import Path
from claude_agent_sdk import ClaudeAgentOptions, ProcessError, TransportTwo runs, one source apart
from claude_agent_sdk import ClaudeAgentOptions
from shop_tools import shop
from watch import watch
def desk(sources):
# cwd is where the project source looks, so CLAUDE.md is read from shop/
return ClaudeAgentOptions(mcp_servers={"shop": shop}, cwd="shop",
allowed_tools=["mcp__shop__refund_order"],
setting_sources=sources)await watch("Please refund A17", desk(["project"]))system init assistant I cannot refund an order. Email the shop instead. result success: I cannot refund an order. Email the shop instead.
await watch("Please refund A17", desk([]))system init
assistant calls mcp__shop__refund_order {'order_id': 'A17'}
user result: Order A17 has been refunded.
assistant Order A17 has been refunded.
result success: Order A17 has been refunded.- With the project source the desk reads
shop/CLAUDE.md, finds the line about refunds and refuses. - With an empty list the same file is still on disk and is never opened, so the refund goes through.
- No other change. The option alone decided whether the file counted.
Reaching other folders with add_dirs and cwd
cwd is the directory the project source reads from, and every tool runs against it. add_dirs grants access to directories outside it, and the SDK passes each one to Claude Code as --add-dir, which also brings that directory's .claude/skills/, .claude/commands/ and .claude/agents/ into the session.
print(flags(ClaudeAgentOptions(add_dirs=["/srv/shared"], cwd="shop")))
print(flags(ClaudeAgentOptions(settings='{"includeGitInstructions": false}')))--output-format stream-json --verbose --system-prompt '' --add-dir /srv/shared --input-format stream-json
--output-format stream-json --verbose --system-prompt '' --settings '{"includeGitInstructions": false}' --input-format stream-jsoncwd is not a flag: the SDK starts the process in that directory. settings is one, and what it carries outranks the user, project and local files. Only managed policy settings rank above it: configuration.
Where you reach for a file instead of an option
- Conventions a team shares with the CLI and with your agent: CLAUDE.md, loaded by the project source.
- A hook that has to fire for both: a shell hook in
.claude/settings.json, which runs beside the callbacks you pass toquery(). - One service, many customers: an empty list, a working directory per customer, and nothing shared.
Running the same code on the real Claude Code
Every example in this course passes transport=ShopDesk(options). Leave that argument out and the SDK starts the Claude Code binary bundled with the package instead, with the flags printed above, and your tools, callbacks, hooks and message handling stay exactly as they are.
from claude_agent_sdk import ClaudeAgentOptions, query
from shop_tools import shop
from watch import describe
# no transport argument: the SDK starts the real Claude Code process
options = ClaudeAgentOptions(mcp_servers={"shop": shop}, setting_sources=["project"],
allowed_tools=["mcp__shop__lookup_order"])
async for message in query(prompt="Where is my order A17?", options=options):
print(describe(message))That snippet is shown and not run here, because it needs an API key and a real model call, which this course never makes. With a key set, expect the same kinds of message watch has been printing: an init, a request for the shop tool, its result, Claude's own wording of the answer, and a result carrying a real total_cost_usd. cli_path points the SDK at a particular binary when the bundled one is missing.
~/.claude.json config and the auto memory folder are read whatever you pass, so a service that must keep customers apart needs a filesystem per customer as well.Related
- Previous: System prompts
- Next: Skills
- Reference: Claude Code features in the SDK
- Pass
setting_sources=["local"]and rename the file toshop/CLAUDE.local.md. Does the refusal come back? - Move the file to
shop/.claude/CLAUDE.mdand run the project source again. - Print
ShopDesk(desk([])).sources()andShopDesk(desk(None)).sources().
Little by little, you're building something great.