A
frameto help you reason through the assignment — concepts, diagrams, and the API map. It deliberately doesnotcontain the answers or filled-in activity code. Instead it gives you thequestions to ask yourselfand themethodto get there. The work — and the learning — is in building the app with Claude Code, reading the message stream, and writing your own conclusions.Source:
11_Claude_Code/README.md
(markdown-only session —no notebook; you write your Q1–Q4 answers directly in the README). Guides:01_Installing…
,02_Using…
,03_Claude_Agent_SDK.md
. Deliverable: your ownchat-app/
.
| You want to… | Reach for | One-liner |
|---|---|---|
| Start Claude Code in a project | claude (CLI) |
|
| Launches the agent loop in the current directory | ||
| Explore/scaffold before changing anything | plan mode (Shift+Tab, or --permission-mode plan ) |
|
| Read-only recon; nothing executes until you approve | ||
| Give the agent durable project memory | CLAUDE.md |
|
| Auto-loaded into context at the start of every session | ||
| Trim / reset the context window | /compact , /clear |
|
| Manual context-budget control (the Session 3 middleware, by hand) | ||
| Run the SDK agent loop | query(prompt=…, options=…) |
|
| Returns an async stream of typed messages until done | ||
| Configure the agent | ClaudeAgentOptions(…) |
|
| allowed_tools · permission_mode · max_turns · cwd · resume · mcp_servers | ||
| Constrain what the agent can do | allowed_tools=["Read","Glob","Grep"] |
|
| A read-only allowlist = the server-side safety story | ||
| Read the loop's steps | the message stream | SystemMessage (init → session_id) · AssistantMessage · ResultMessage.result |
| Continue a conversation across messages | resume=session_id |
|
Map each browser conversation_id → the SDK session_id |
||
| Add a custom tool | @tool + create_sdk_mcp_server |
|
Wire via mcp_servers ; allowlist as mcp__<server>__<tool> |
||
| Serve the agent to a browser | FastAPI POST /api/chat |
|
The seam: one swappable function calls query() |
Anchor: In the terminal, YOU are the permission gate. On a headless server there is no human to click "approve" — so the tool allowlist + permission mode ARE the gate. Safety comes from what the agent is allowed to do, not from trusting the input.
flowchart LR
subgraph Build["Build it WITH Claude Code (plan → implement → verify)"]
You[You] -->|plan mode| CC[Claude Code]
CC -->|scaffold| Skel[chat-app skeleton\nFastAPI + echo stub + CLAUDE.md]
end
subgraph Run["Run the agent BEHIND the app"]
Browser[Browser chat UI] -->|fetch /api/chat| API[FastAPI /api/chat]
API -->|query()| SDK[Agent SDK loop]
SDK --> Tools[Read / Glob / Grep + your custom tool]
Tools --> SDK
SDK -->|ResultMessage.result| API --> Browser
end
Skel -.->|replace the echo seam| API
ASCII fallback:
BUILD (with Claude Code): you ─plan mode─► Claude Code ─► chat-app skeleton (echo stub + CLAUDE.md)
│ replace the seam
v
RUN (agent behind the app): browser ─fetch─► /api/chat ─query()─► Agent SDK loop ─► Read/Glob/Grep + custom tool
▲ │
└──────────── ResultMessage.result ◄─────┘
Why this shape? Two breakout rooms, one arc. First you build the app with Claude Code (the agent as your pair-programmer, gated by plan mode). Then you put an agent inside the app — the same loop that powers Claude Code, embedded via query()
. The load-bearing design fact: the chat logic lives in one swappable function; /api/chat is the seam where the echo stub becomes a real agent.
claude # first run → authenticate
uv init chat-app && cd chat-app # Python 3.12+, uv
uv add fastapi uvicorn claude-agent-sdk
export ANTHROPIC_API_KEY=... # server-side only; STRIP before committing
uv run uvicorn app:app --reload # serves http://localhost:8000
| Component | Role |
|---|---|
| Claude Code | |
| The builder — scaffolds + extends the app; you gate it with plan/permission modes | |
CLAUDE.md |
|
| Curated project memory, auto-loaded every session | |
FastAPI /api/chat |
|
| The server seam — one swappable function that calls the agent | |
Agent SDK query() |
|
| The runtime — the agent loop embedded in your app | |
| Tools | |
Read /Glob /Grep (read-only built-ins) + ≥1 custom @tool |
Claude Code runs real side-effecting tools (Bash/Edit/Write) and picks the next tool itself — so every action can change the world irreversibly. The permission system (default
/ acceptEdits
/ plan
/ bypassPermissions
) is the human-in-the-loop gate. Plan mode is read-only: propose-before-act. Docs: https://code.claude.com/docs
Auto-loaded into context every session → high value per line. Belongs: the run/test commands, the /api/chat
seam, conventions, gotchas. Not: code-discoverable detail, transient state, secrets. It's the long-term-memory half of Session 3's finite-context discipline (/compact
·/clear
are the S3 summarization middleware, by hand).
from claude_agent_sdk import query, ClaudeAgentOptions
async for message in query(prompt="…", options=ClaudeAgentOptions(allowed_tools=["Read","Glob","Grep"])):
... # the whole loop (call → tool → feed result back → repeat) in one call
Docs: https://docs.anthropic.com/en/api/agent-sdk/overview
SystemMessage
(init — carries session_id
) · AssistantMessage
(text + tool-use blocks) · UserMessage
(tool results) · ResultMessage
(.result
- usage). This anatomy is what makes progress-streaming and grounded answers possible.
POST /api/chat
accepts {message, conversation_id}
→ {reply}
. The stub calls query()
and returns ResultMessage.result
. Read-only allowed_tools
-
permission_mode -
max_turns
are the server-side gate.
Each query()
is a fresh conversation unless you resume it. Capture session_id
from the init SystemMessage
; keep a conversation_id → session_id
dict; pass resume=session_id
so follow-ups carry context.
from claude_agent_sdk import tool, create_sdk_mcp_server
@tool("count_lines", "Count lines in a file", {"file_path": str})
async def count_lines(args): ...
server = create_sdk_mcp_server(name="concierge", version="1.0.0", tools=[count_lines])
On a server there's no human approver. allowed_tools=["Read","Glob","Grep"]
means the agent structurally cannot modify the filesystem, no matter what a user types (prompt injection included). The allowlist is safety, not performance. max_turns
caps runaway loops/cost.
These are the four questions you answer in the README. Below is the
methodand thequestions to ask yourself— not the answers. Reason from the guides and your own build.
Q1 — Why does an agent that runs shell commands need a permission system, and why is plan mode valuable from an empty directory?
Ask yourself: what is the difference between a chat model that gets something wrong and an agent that gets something wrong? What could a tool like Bash
or Edit
actually do? When you started from an empty folder, what did plan mode let you see before anything ran — and why does "empty folder" raise the stakes versus editing an existing repo? (Revisit Guide 2's "Permission Modes".)
Ask yourself: this file is re-read at the start of every session — so what earns a permanent seat, and what's cheaper to let the agent rediscover? What would be actively dangerous to put here? Then connect it to Session 3: what did you learn about the context window as a budget, and which S3 tool do /compact
and /clear
echo?
Ask yourself: list what you had to wire by hand in LangGraph that query()
now hands you for free. Then flip it — name something you could do in a hand-built graph that the fixed SDK loop won't let you do. Is this a strict upgrade, or a trade? For a read-and-answer concierge, which side of the trade matters?
Q4 — Why route through query()
vs a raw chat completion; what new risk, and how did your controls address it?
Ask yourself: what can the agent do that a plain chat completion can't? Now the flip side — those same tools are driven by whatever a stranger types into your chat box, and no human is at the server to approve. What could go wrong? Look at your own allowed_tools
and permission_mode
: what do they make impossible, regardless of the input? (Guide 3's "Why these controls matter (Question #4)" is your anchor.)
Build a FastAPI app where POST /api/chat
routes each message through the Agent SDK query()
, configured with a read-only allowed_tools=["Read","Glob","Grep"]
allowlist; add conversation memory (map conversation_id
→ SDK session_id
, pass resume=
) and at least one custom tool. Build it with Claude Code — plan → implement → verify.
Check yourself: Does the chat answer real questions about a repo, and do the answers actually reflect the files (not a guess)? Ask a follow-up ("what are its dependencies?") — does context carry? Force your custom tool with a targeted question — does it fire? Is your key ever visible in the browser? Does chat-app/CLAUDE.md
name the seam?
Pick one: (a) live progress streaming (SSE) so users see tool calls instead of a spinner; (b) a multi-conversation sidebar, each thread its own SDK session; (c) a second custom tool useful for your target repo. Demo it in your Loom and explain the design decision in one paragraph. Check yourself: does the enhancement visibly work in your demo, and can you justify why you chose it? (Any one, working, is enough.)
If you're curious: connect your Session 8 cat-shop MCP server to your chat app via the SDK's mcp_servers
option, so users can browse/cart/checkout in natural language. It's unscored — no bonus, no penalty — pure exploration; share findings + a demo in your Loom if you try it.