TL;DR
The Claude Code source leak exposed a 512,000-line TypeScript codebase on March 31, 2026. The architecture boils down to a loop that calls the Claude API, dispatches tool calls, and feeds tool results back into the conversation. You can recreate the core behavior with Python, the Anthropic SDK, and a small set of filesystem tools.
Introduction
On March 31, 2026, Anthropic shipped a 59.8 MB source map file inside version 2.1.88 of the @anthropic-ai/claude-code npm package. Source maps map minified JavaScript back to original source. Because Anthropic’s build tool generated them by default, the TypeScript source was recoverable.
Developers quickly mirrored and analyzed the codebase. The most useful takeaway was not the leaked implementation itself, but the architecture: a model loop, a tool system, context compaction, project memory, and permissions.
This guide shows how to build a minimal Claude Code-style coding agent from scratch. The examples use Python and the Anthropic SDK, but the same design works with any model that supports tool/function calling.
What the leak revealed about Claude Code’s architecture
Codebase structure
Claude Code, internally codenamed “Tengu,” spans about 1,900 files. Its modules break down into clear layers:
cli/ - Terminal UI using React + Ink
tools/ - Tool implementations
core/ - System prompts, permissions, constants
assistant/ - Agent orchestration
services/ - API calls, compaction, OAuth, telemetry
The CLI is a React app rendered in the terminal with Ink. It uses Yoga for layout and ANSI escape codes for styling.
You do not need that level of UI complexity for your own agent. A simple REPL is enough to implement the core behavior.
The master agent loop
At its core, Claude Code is a loop:
- Send the system prompt, tool definitions, and messages to the Claude API.
- Receive text and/or
tool_useblocks. - Execute requested tools using a name-to-handler dispatch map.
- Append tool results to the conversation.
- Repeat until the model returns text with no tool calls.
Here is the minimal Python version:
import anthropic
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
def agent_loop(system_prompt: str, tools: list, messages: list) -> str:
"""Run the agent until Claude stops requesting tools."""
while True:
response = client.messages.create(
model=MODEL,
max_tokens=16384,
system=system_prompt,
tools=tools,
messages=messages,
)
messages.append({
"role": "assistant",
"content": response.content,
})
if response.stop_reason != "tool_use":
return "".join(
block.text
for block in response.content
if hasattr(block, "text")
)
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
messages.append({
"role": "user",
"content": tool_results,
})
That loop is the foundation. Most of the engineering work lives in the tools, context management, memory, and permissions.
Building the tool system
Use dedicated tools instead of one generic shell tool
Claude Code uses dedicated tools for file and search operations:
-
Readinstead ofcat -
Editinstead ofsed -
Grepinstead of rawgrep -
Globinstead offind
That design has practical advantages:
- Structured output: the model receives predictable results.
- Safety: dangerous shell behavior is easier to restrict.
- Token efficiency: tool output can be truncated or formatted before being sent back to the model.
For a small agent, start with five tools:
read_filewrite_fileedit_filerun_commandsearch_code
Define the tools
TOOLS = [
{
"name": "read_file",
"description": "Read a file from the filesystem. Returns contents with line numbers.",
"input_schema": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Absolute path to the file",
},
"offset": {
"type": "integer",
"description": "Line number to start reading from, 0-indexed",
},
"limit": {
"type": "integer",
"description": "Maximum lines to read. Defaults to 2000.",
},
},
"required": ["file_path"],
},
},
{
"name": "write_file",
"description": "Write content to a file. Creates the file if it does not exist.",
"input_schema": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Absolute path",
},
"content": {
"type": "string",
"description": "File content to write",
},
},
"required": ["file_path", "content"],
},
},
{
"name": "edit_file",
"description": "Replace a specific string in a file. The old_string must be unique.",
"input_schema": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Absolute path",
},
"old_string": {
"type": "string",
"description": "Text to find",
},
"new_string": {
"type": "string",
"description": "Replacement text",
},
},
"required": ["file_path", "old_string", "new_string"],
},
},
{
"name": "run_command",
"description": "Execute a shell command and return stdout/stderr.",
"input_schema": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Shell command to run",
},
"timeout": {
"type": "integer",
"description": "Timeout in seconds. Default 120.",
},
},
"required": ["command"],
},
},
{
"name": "search_code",
"description": "Search for a regex pattern across files in a directory.",
"input_schema": {
"type": "object",
"properties": {
"pattern": {
"type": "string",
"description": "Regex pattern",
},
"path": {
"type": "string",
"description": "Directory to search",
},
"file_glob": {
"type": "string",
"description": "File pattern filter, for example '*.py'",
},
},
"required": ["pattern"],
},
},
]
Implement tool dispatch
The model returns a tool name and JSON input. Your agent maps that name to a Python function.
import os
import subprocess
def execute_tool(name: str, params: dict) -> str:
"""Dispatch tool calls to their handlers."""
handlers = {
"read_file": handle_read_file,
"write_file": handle_write_file,
"edit_file": handle_edit_file,
"run_command": handle_run_command,
"search_code": handle_search_code,
}
handler = handlers.get(name)
if not handler:
return f"Error: Unknown tool '{name}'"
if not check_permission(name, params):
return f"Error: Permission denied for tool '{name}'"
try:
return handler(params)
except Exception as e:
return f"Error: {str(e)}"
Implement file tools
def handle_read_file(params: dict) -> str:
path = params["file_path"]
offset = params.get("offset", 0)
limit = params.get("limit", 2000)
with open(path, "r") as f:
lines = f.readlines()
selected = lines[offset:offset + limit]
numbered = [
f"{i + offset + 1}\t{line}"
for i, line in enumerate(selected)
]
return "".join(numbered)
def handle_write_file(params: dict) -> str:
path = params["file_path"]
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as f:
f.write(params["content"])
return f"Successfully wrote to {path}"
def handle_edit_file(params: dict) -> str:
path = params["file_path"]
with open(path, "r") as f:
content = f.read()
old = params["old_string"]
new = params["new_string"]
count = content.count(old)
if count == 0:
return f"Error: target string not found in {path}"
if count > 1:
return f"Error: target string matches {count} locations. Be more specific."
new_content = content.replace(old, new, 1)
with open(path, "w") as f:
f.write(new_content)
return f"Successfully edited {path}"
Implement command execution
Keep command execution behind explicit approval. Also truncate large outputs before sending them back to the model.
def handle_run_command(params: dict) -> str:
cmd = params["command"]
timeout = params.get("timeout", 120)
blocked = [
"rm -rf /",
"mkfs",
"> /dev/",
]
for pattern in blocked:
if pattern in cmd:
return f"Error: Blocked dangerous command pattern: {pattern}"
result = subprocess.run(
cmd,
shell=True,
capture_output=True,
text=True,
timeout=timeout,
cwd=os.getcwd(),
)
output = ""
if result.stdout:
output += result.stdout
if result.stderr:
output += f"\nSTDERR:\n{result.stderr}"
if not output.strip():
output = f"Command completed with exit code {result.returncode}"
if len(output) > 30000:
output = (
output[:15000]
+ "\n\n... [truncated] ...\n\n"
+ output[-15000:]
)
return output
Implement code search
def handle_search_code(params: dict) -> str:
pattern = params["pattern"]
path = params.get("path", os.getcwd())
file_glob = params.get("file_glob", "")
if file_glob:
cmd = ["grep", "-rn", "--include", file_glob, pattern, path]
else:
cmd = ["grep", "-rn", pattern, path]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30,
)
if not result.stdout.strip():
return f"No matches found for pattern: {pattern}"
lines = result.stdout.strip().split("\n")
if len(lines) > 50:
return (
"\n".join(lines[:50])
+ f"\n\n... ({len(lines) - 50} more matches)"
)
return result.stdout
Context management
Why context matters
The leaked source shows Claude Code spends significant engineering effort on context management. For a practical DIY agent, implement two patterns first:
- Auto-compaction: summarize long conversations before they exceed the context window.
- Project context re-injection: reload project instructions on every turn.
Add conversation compaction
This example uses a rough character-based token estimate.
def maybe_compact(
messages: list,
system_prompt: str,
max_tokens: int = 180000,
) -> list:
"""Compact conversation when it gets too long."""
total_chars = sum(
len(str(message.get("content", "")))
for message in messages
)
estimated_tokens = total_chars // 4
if estimated_tokens < max_tokens * 0.85:
return messages
summary_response = client.messages.create(
model=MODEL,
max_tokens=4096,
system=(
"Summarize this conversation. Keep all file paths, "
"decisions made, errors encountered, and current task state. "
"Be specific about what changed and why."
),
messages=messages,
)
summary_text = summary_response.content[0].text
compacted = [
{
"role": "user",
"content": f"[Conversation summary]\n{summary_text}",
},
{
"role": "assistant",
"content": (
"I have the context from our previous conversation. "
"What should I work on next?"
),
},
]
compacted.extend(messages[-4:])
return compacted
Re-inject project guidelines every turn
Claude Code reads project instructions such as .claude/CLAUDE.md and keeps them available. Rebuild the system prompt before each agent loop.
def build_system_prompt(project_dir: str) -> str:
"""Build a system prompt with project-specific context."""
base_prompt = """You are a coding assistant that helps with software engineering tasks.
You have access to tools for reading, writing, editing files, running commands, and searching code.
Always read files before modifying them.
Prefer edit_file over write_file for existing files.
Keep responses concise and focus on the code."""
claude_md_path = os.path.join(project_dir, ".claude", "CLAUDE.md")
if os.path.exists(claude_md_path):
with open(claude_md_path, "r") as f:
project_context = f.read()
base_prompt += f"\n\n# Project guidelines\n{project_context}"
root_md_path = os.path.join(project_dir, "CLAUDE.md")
if os.path.exists(root_md_path):
with open(root_md_path, "r") as f:
root_context = f.read()
base_prompt += f"\n\n# Repository guidelines\n{root_context}"
return base_prompt
Add memory
Claude Code uses a layered memory architecture. You can implement a smaller version with:
- An always-loaded memory index.
- Topic files loaded when relevant.
- Session logs that are searched but not loaded wholesale.
Example memory index
- [User preferences](user-prefs.md) - prefers TypeScript, uses Vim keybindings
- [API conventions](api-conventions.md) - REST with JSON:API spec, snake_case
- [Deploy process](deploy.md) - uses GitHub Actions, deploys to AWS EKS
Minimal memory implementation
import os
MEMORY_DIR = ".agent/memory"
def load_memory_index() -> str:
"""Load the memory index for system prompt injection."""
index_path = os.path.join(MEMORY_DIR, "MEMORY.md")
if os.path.exists(index_path):
with open(index_path, "r") as f:
return f.read()
return ""
def save_memory(key: str, content: str, description: str):
"""Save a memory entry and update the memory index."""
os.makedirs(MEMORY_DIR, exist_ok=True)
filename = f"{key.replace(' ', '-').lower()}.md"
filepath = os.path.join(MEMORY_DIR, filename)
with open(filepath, "w") as f:
f.write(
f"---\n"
f"name: {key}\n"
f"description: "{description}\n\""
f"---\n\n"
f"{content}"
)
index_path = os.path.join(MEMORY_DIR, "MEMORY.md")
index_lines = []
if os.path.exists(index_path):
with open(index_path, "r") as f:
index_lines = f.readlines()
new_entry = f"- [{key}]({filename}) - {description}\n"
updated = False
for i, line in enumerate(index_lines):
if filename in line:
index_lines[i] = new_entry
updated = True
break
if not updated:
index_lines.append(new_entry)
with open(index_path, "w") as f:
f.writelines(index_lines)
You can expose save_memory as another tool so the agent can persist project knowledge between sessions.
Add a permission system
The leaked source shows multiple permission modes, including interactive prompts and bypass modes. For a DIY agent, start with risk levels:
RISK_LEVELS = {
"read_file": "low",
"search_code": "low",
"edit_file": "medium",
"write_file": "medium",
"run_command": "high",
}
Then require approval for medium- and high-risk operations:
def check_permission(
tool_name: str,
params: dict,
auto_approve_low: bool = True,
) -> bool:
"""Ask the user before running risky tool calls."""
risk = RISK_LEVELS.get(tool_name, "high")
if risk == "low" and auto_approve_low:
return True
print(f"\n--- Permission check ({risk.upper()} risk) ---")
print(f"Tool: {tool_name}")
for key, value in params.items():
display = str(value)[:200]
print(f" {key}: {display}")
response = input("Allow? [y/n/always]: ").strip().lower()
if response == "always":
RISK_LEVELS[tool_name] = "low"
return True
return response == "y"
This protects users from accidental file rewrites and arbitrary command execution.
Testing your agent’s API calls with Apidog
Building a coding agent means sending many multi-turn API requests with tool definitions, tool calls, and tool results. Raw logs become hard to inspect quickly.
Use Apidog during development to test and replay requests to the Anthropic Messages API.
Capture and replay API requests
Create an Apidog project for your agent and add the endpoint:
POST https://api.anthropic.com/v1/messages
Then configure the request body with:
modelmax_tokenssystemtoolsmessages
This lets you replay one agent turn without running your full REPL. When the model returns an unexpected tool call or invalid parameter, edit the request body and resend it.
Debug multi-turn conversations
Agent bugs often depend on conversation state. Use environment variables in Apidog to store message snapshots:
- Save the full
messagesarray after a turn. - Replay from a specific conversation state.
- Compare tool results across runs.
- Test how different system prompts affect tool selection.
Validate tool schemas
Tool schemas control what the model can call. If your schema is malformed or too vague, the model may skip the tool or pass bad arguments.
Import your tool definitions into Apidog and validate the JSON Schema before sending it to the model.
Put everything together: complete REPL
Here is the agent tied together as a working command-line REPL:
#!/usr/bin/env python3
"""A minimal Claude Code-style coding agent."""
import anthropic
import os
import subprocess
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
PROJECT_DIR = os.getcwd()
def main():
messages = []
print("Coding agent ready. Type 'quit' to exit.\n")
while True:
user_input = input("> ").strip()
if user_input.lower() in ("quit", "exit"):
break
if not user_input:
continue
messages.append({
"role": "user",
"content": user_input,
})
current_system = build_system_prompt(PROJECT_DIR)
memory = load_memory_index()
if memory:
current_system += f"\n\n# Memory\n{memory}"
messages = maybe_compact(messages, current_system)
result = agent_loop(
system_prompt=current_system,
tools=TOOLS,
messages=messages,
)
print(f"\n{result}\n")
if __name__ == "__main__":
main()
With the previous sections included, this gives you a small but functional coding agent that can:
- Read files
- Edit files
- Write files
- Run commands with approval
- Search a codebase
- Compact long conversations
- Load project instructions
- Persist memory across sessions
What to build next
Once the core loop works, add these features.
Sub-agents for isolated work
Claude Code can spawn sub-agents for independent tasks. The pattern is simple:
- Copy the parent context.
- Give the sub-agent a focused task.
- Limit its tools if needed.
- Run a separate
agent_loop(). - Return the result to the main conversation.
This keeps exploratory work from polluting the main context.
File-read deduplication
Track files the agent has read, along with modification times. If a file has not changed, avoid re-reading it and return a short message such as:
File unchanged since last read.
This saves tokens during long sessions.
Better output truncation
Large command outputs can consume the entire context window. Add truncation to every tool that can return large data:
grep- test runners
- build logs
- package manager output
- generated files
Always tell the model when output was truncated.
Auto-compaction with file re-injection
After compaction, re-inject recently accessed files. This helps the model retain working knowledge of the current codebase even after old conversation turns are summarized.
Key takeaways
The Claude Code leak confirmed several practical agent-building patterns:
- The core loop is simple. The basic agent loop fits in a few dozen lines.
- Dedicated tools are better than raw shell access. They produce safer and more predictable output.
- Context management matters more than prompt length. Compaction and re-injection make long sessions viable.
- Memory needs layers. Keep a small index always loaded and load detailed files only when needed.
- Permissions are mandatory. Never let an agent write files or execute commands without guardrails.
- The harness is the product. The model provides reasoning, but the harness provides perception, action, memory, and safety.
If you are building your own agent, debug the API layer early. Multi-turn tool-use conversations are easier to inspect when you can replay exact requests, validate schemas, and compare responses outside your application loop.
FAQ
Can I legally use patterns from the Claude Code leak?
The leak exposed architectural patterns, not proprietary algorithms. A loop that calls a model, dispatches tools, and feeds results back is a standard agent pattern documented in model API docs. Do not copy proprietary source code verbatim. Reimplementing the architecture with your own code is the safer path.
What model should I use for a DIY coding agent?
Claude Sonnet 4.6 offers a balance of speed and capability for coding tasks. Claude Opus 4.6 can be better for complex architecture work but costs more and is slower. Claude Haiku 4.5 can work for simpler edits and searches.
How much does it cost to run your own coding agent?
A typical coding session with 30-50 turns using Claude Sonnet 4.6 can cost around $1-5 in API fees. The biggest cost driver is context size. Compaction and output truncation help control cost.
Why does Claude Code use React for a terminal app?
Ink lets a terminal UI use React’s component model and state management. That helps with permission dialogs, streaming output, and tool-call displays. For a DIY build, input() and print() are enough.
What is the most important feature after the core loop?
Build the permission system first. Without it, the model can overwrite files or run arbitrary commands without user oversight.
How should tool errors work?
Return tool errors as text in the tool_result. The model can then decide whether to retry, use another tool, or ask the user for help.
Can I use this pattern with models other than Claude?
Yes. The architecture works with any model that supports function calling or tool use. You will need to adapt the API request and response format, but the loop, tools, memory, and permissions are model-agnostic.
How do I prevent dangerous commands?
Use multiple layers:
- Block known dangerous patterns.
- Require approval for all shell commands.
- Run commands in the project directory, not arbitrary paths.
- Prefer dedicated tools over shell commands.
- Add risk levels for every tool.

Top comments (0)