Claude Agent SDKclaude-agent-sdk 0.2.157 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
46 small wins to finish your pathNext lesson →

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

SourceWhat it loadsWhere 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

python
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.

Exampleshop/CLAUDE.md
# 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.

Exampleshop_desk.py, a new method
    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"]
Exampleshop_desk.py, a second new method
    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())
Exampleshop_desk.py, the two lines that end rules now
        prompt = prompt if isinstance(prompt, str) else prompt.get("append", "")
        return prompt + "\n" + self.project_notes()
Exampleshop_desk.py, the imports now
import re
from pathlib import Path

from claude_agent_sdk import ClaudeAgentOptions, ProcessError, Transport

Two runs, one source apart

python
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)
Example
await watch("Please refund A17", desk(["project"]))
Example
await watch("Please refund A17", desk([]))
  • 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.

Example
print(flags(ClaudeAgentOptions(add_dirs=["/srv/shared"], cwd="shop")))
print(flags(ClaudeAgentOptions(settings='{"includeGitInstructions": false}')))

cwd 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 to query().
  • 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.

Examplewith ANTHROPIC_API_KEY set
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.

Watch out. An empty list does not isolate a session by itself. Managed policy settings, the global ~/.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.
Try it yourself
  • Pass setting_sources=["local"] and rename the file to shop/CLAUDE.local.md. Does the refusal come back?
  • Move the file to shop/.claude/CLAUDE.md and run the project source again.
  • Print ShopDesk(desk([])).sources() and ShopDesk(desk(None)).sources().

Little by little, you're building something great.