Cheat sheet
The parameters, limits, and code shapes worth having in memory — followed by the anti-patterns that most often show up as plausible-looking wrong answers.
D1Agentic Architecture & Orchestration
Architecture tiers
Single call → workflow (you own the control flow) → agent (model owns it). Pick the simplest tier that meets the need; agents are for open-ended work you cannot enumerate up front.
Manual tool loop
Append the FULL response.content as the assistant turn, then send every tool_result in one user message. Loop until stop_reason is end_turn.
while True:
r = client.messages.create(model="claude-opus-5", max_tokens=16000,
tools=tools, messages=messages)
if r.stop_reason == "end_turn":
break
if r.stop_reason == "pause_turn": # server tool hit its iteration cap
messages.append({"role": "assistant", "content": r.content})
continue
messages.append({"role": "assistant", "content": r.content})
results = [
{"type": "tool_result", "tool_use_id": b.id, "content": run(b.name, b.input)}
for b in r.content if b.type == "tool_use"
]
messages.append({"role": "user", "content": results}) # one message, all resultsTool Runner
The SDK drives the loop over tools you define. Per-turn hooks still allow approval gates, error interception, and result modification — a manual loop is rarely necessary.
runner = client.beta.messages.tool_runner(
model="claude-opus-5", max_tokens=16000,
tools=[get_weather], messages=[{"role": "user", "content": "Weather in Paris?"}],
)
for message in runner:
...Managed Agents flow
Agent once → session per run. model/system/tools/mcp_servers/skills live on the AGENT. The session takes an agent id and an environment id.
agent = client.beta.agents.create(
name="Reviewer", model="claude-opus-5",
system="You are a senior code reviewer.",
tools=[{"type": "agent_toolset_20260401"}],
) # store agent.id — do not re-create per request
session = client.beta.sessions.create(
agent={"type": "agent", "id": agent.id, "version": agent.version},
environment_id=env.id,
)Idle-break gate
Never break on session.status_idle alone — it fires transiently while the agent waits on you.
for event in stream:
if event.type == "session.status_terminated":
break
if event.type == "session.status_idle":
if event.stop_reason.type == "requires_action":
continue # your turn: tool confirmation / custom tool result
break # end_turn or retries_exhaustedStop reasons
end_turn (done) · tool_use (execute and continue) · max_tokens (raise the cap or stream) · stop_sequence · pause_turn (re-send to resume) · refusal (HTTP 200, content may be empty).
D2Tool Design & MCP Integration
Tool definition
Name it for the action, describe WHEN to call it, describe every property, and use enum for fixed value sets. Mark only truly required parameters required.
{
"name": "get_weather",
"description": "Get current weather for a location. Call this when the user "
"asks about present conditions rather than a forecast.",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City and state, e.g. Austin, TX"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["location"]
}
}Strict tool use
`strict` is a top-level field on the TOOL (not on tool_choice). Requires additionalProperties: false plus an explicit required array.
{"name": "book_flight", "strict": True,
"input_schema": {"type": "object",
"properties": {"destination": {"type": "string"}},
"required": ["destination"], "additionalProperties": False}}tool_choice
auto (default) · any (must use some tool) · tool + name (must use that one) · none. Add disable_parallel_tool_use: true to cap at one call per turn.
Tool search
For large libraries: declare the search tool and mark the rest defer_loading: true. Discovered schemas are appended, so the cache survives. At least one tool must stay non-deferred.
MCP connector
Both halves are required — the server definition AND a toolset entry referencing it by name.
client.beta.messages.create(
betas=["mcp-client-2025-11-20"],
mcp_servers=[{"type": "url", "name": "example", "url": "https://example/mcp"}],
tools=[{"type": "mcp_toolset", "mcp_server_name": "example"}],
...
)Vault credentials
mcp_oauth / static_bearer are matched to MCP servers by URL. environment_variable secrets are substituted at egress — the sandbox only ever sees a placeholder.
Server vs client tools
Server-side (no execution loop): web search, web fetch, code execution, tool search. Client-executed: bash, text editor, memory, computer use, and every custom tool.
D3Claude Code Configuration & Workflows
CLAUDE.md
Project-level context checked into the repo; a user-level file applies across all projects. Keep it to durable conventions — situational detail belongs in skills.
settings.json
Harness configuration: permission allow/deny rules, environment variables, and hooks. A git-ignored local variant carries machine-specific overrides.
Hooks
The only way to guarantee an automatic behavior — the harness executes them, not the model. PreToolUse can block a call; PostToolUse reacts after. Hook output is fed back as feedback.
Skills
A folder with a SKILL.md. The description stays in context; the body loads on demand. Write the description around triggers — that is the whole routing signal.
Sub-agents
Use for context isolation and genuinely parallel tracks. Do not spawn one for work that is a few tool calls, and do not use them to double-check ordinary results.
CI usage
Non-interactive mode, prompt supplied up front, permissions pre-scoped to what the job needs. An unattended interactive session deadlocks on the first prompt.
D4Prompt Engineering & Structured Output
Structured outputs
output_config.format with a json_schema. Unsupported: recursion, numeric bounds, string length limits. Incompatible with citations. First call pays a schema compile, cached ~24h.
response = client.messages.create(
model="claude-opus-5", max_tokens=16000,
output_config={"format": {"type": "json_schema", "schema": SCHEMA}},
messages=[...],
)Batch API
50% off; ≤100k requests or 256MB; usually <1h, max 24h; results retained 29 days and returned in ANY order — key by custom_id.
batch = client.messages.batches.create(requests=[...])
# poll batches.retrieve(batch.id) until processing_status == "ended"
results = {r.custom_id: r for r in client.messages.batches.results(batch.id)}Thinking and effort
thinking: {type: 'adaptive'} on current models; budget_tokens is removed. effort lives in output_config (low/medium/high/xhigh/max, default high). display: 'summarized' to surface reasoning.
client.messages.create(
model="claude-opus-5", max_tokens=16000,
thinking={"type": "adaptive", "display": "summarized"},
output_config={"effort": "high"},
messages=[...],
)Removed parameters
temperature, top_p, top_k, budget_tokens, and last-turn assistant prefill all return 400 on current frontier models. Steer with prompting and structured outputs instead.
Token counting
count_tokens with the same model id you will call. Counts are model-specific; tiktoken is the wrong tokenizer and undercounts substantially.
D5Context Management & Reliability
Prompt caching
Prefix match. Render order tools → system → messages. Max 4 breakpoints. Write ≈1.25x (5m) / 2x (1h), read ≈0.1x. Below the model minimum it silently does not cache.
system=[{"type": "text", "text": BIG_PROMPT,
"cache_control": {"type": "ephemeral"}}]
# verify:
r.usage.cache_creation_input_tokens # written this request
r.usage.cache_read_input_tokens # served from cacheCache invalidation tiers
Tool changes or a model switch wipe everything. A system-prompt edit keeps the tools cache. tool_choice and thinking toggles keep tools + system. Appending a message keeps all three.
Compaction
Summarizes earlier context server-side as the window fills. Append the full response.content — compaction blocks must be passed back or state is lost silently.
response = client.beta.messages.create(
betas=["compact-2026-01-12"], model="claude-opus-5", max_tokens=16000,
context_management={"edits": [{"type": "compact_20260112"}]},
messages=messages,
)
messages.append({"role": "assistant", "content": response.content}) # full contentContext editing
Clears rather than summarizes. clear_tool_uses_20250919 for old tool results, clear_thinking_20251015 for thinking blocks, under beta context-management-2025-06-27.
Retries
Retryable: 429, 5xx, connection errors — the SDKs back off automatically (default 2 retries). Not retryable: 400, 401, 403, 404. Catch a chain of typed exceptions, not one base class.
Anti-patterns
27 mistakes that are common enough to be written into exam questions as distractors.
Reaching for an agent by default
Don'tWrapping a classification task in an agent loop with tools.
DoUse the simplest tier that works. Agents are for open-ended work you cannot specify up front.
Splitting parallel tool results
Don'tSending one user message per tool_result.
DoAll results for a parallel batch go in a single user message, or Claude stops issuing parallel calls.
Dropping a failed tool's result
Don'tOmitting the tool_result when a tool throws.
DoReturn it with is_error: true — every tool_use id needs a matching result.
Appending only response text
Don'tExtracting the text and appending that as the assistant turn.
DoAppend the full response.content so tool_use, thinking, and compaction blocks survive the round trip.
Creating an agent per request
Don'tCalling agents.create() at the top of the request handler.
DoCreate once during setup, persist the id, and reference it from every session.
Breaking on the first idle event
Don'tExiting the loop as soon as session.status_idle arrives.
DoContinue when stop_reason is requires_action — the agent is waiting on you.
Send-then-stream
Don'tPosting the user message and then opening the SSE stream.
DoOpen the stream first — it carries only events emitted after it connects.
Descriptions that only say what a tool does
Don't"Searches the knowledge base."
Do"Call this when the answer depends on information not present in the conversation."
strict on tool_choice
Don'tSetting strict inside the tool_choice object.
Dostrict is a top-level field on the tool definition, with additionalProperties: false.
Half an MCP configuration
Don'tDeclaring mcp_servers and expecting tools to appear.
DoAlso add an mcp_toolset entry in tools referencing the server by name.
Secrets in prompts
Don'tPutting an API key in the system prompt so the agent can use it.
DoUse a vault credential or a host-side custom tool. Prompts persist in event history and summaries.
Blocklisting dangerous bash commands
Don'tA regex denying rm and curl.
DoAllowlist permitted executables and reject shell operators; blocklists are trivially bypassed.
Trusting a model-supplied path
Don'tPassing the path straight to open() or writeFile.
DoResolve to canonical form and verify it stays within the project root.
Encoding automation in CLAUDE.md
Don't"Always run the formatter after every edit."
DoA PostToolUse hook — the harness enforces it; prose only suggests it.
Escalating prompt language
Don't"CRITICAL: You MUST use this tool for every request."
DoState the conditions for use. Aggressive phrasing over-triggers on current models.
An ever-growing CLAUDE.md
Don'tThousands of lines of situational rules loaded on every request.
DoMove situational guidance into skills so it loads only when relevant.
Interactive Claude Code in CI
Don'tRunning the default TUI in a pipeline.
DoNon-interactive mode with the prompt up front and permissions pre-scoped.
Prefilling to force JSON
Don'tEnding messages with an assistant turn containing an opening brace.
DoUse output_config.format — prefill returns 400 on current models.
Positional batch matching
Don'tZipping results with the submitted request array.
DoKey by custom_id — batch results arrive in arbitrary order.
budget_tokens on current models
Don'tthinking: {type: "enabled", budget_tokens: 8000}
Dothinking: {type: "adaptive"} plus output_config.effort.
effort at the request root
Don'teffort: 'high' as a top-level parameter.
DoIt is nested: output_config: { effort: 'high' }.
Estimating tokens with tiktoken
Don'tReusing an OpenAI tokenizer for Claude budgeting.
DoCall count_tokens with the model id you will actually use.
A timestamp in the system prompt
Don'tsystem = f"Current date: {datetime.now()}\n..."
DoKeep the system prompt frozen; inject volatile context later in messages.
Breakpoint after the varying question
Don'tMarking the end of the whole prompt when only the preamble is shared.
DoCut at the end of the shared prefix so every request can read it.
Swapping tools to implement modes
Don'tChanging the tool list mid-conversation to switch behavior.
DoTools render at position zero — any change wipes the cache. Pass the mode as message content.
Fanning out before the cache exists
Don'tFiring N identical-prefix requests concurrently at startup.
DoSend one, await its first streamed token, then release the rest.
Reading content before checking stop_reason
Don'tresponse.content[0].text on every response.
DoBranch on stop_reason first — a refusal returns 200 with possibly empty content.