TL;DR(手短に言うと)
Claude Codeのソースコード流出により、2026年3月31日に512,000行のTypeScriptコードベースが公開されました。そのアーキテクチャは、Claude APIを呼び出し、ツール呼び出しをディスパッチし、結果をフィードバックするwhileループに集約されます。Python、Anthropic SDK、および約200行のコードで、このコアロジックを自分で構築できます。このガイドでは、各コンポーネントを分解し、それらを再作成する方法を示します。
はじめに
2026年3月31日、Anthropicは、@anthropic-ai/claude-code npmパッケージのバージョン2.1.88内に59.8 MBのソースマップファイルを公開しました。ソースマップは、縮小されたJavaScriptを元のソースコードに逆変換するためのデバッグ成果物です。Anthropicのビルドツール(Bunのバンドラー)がこれらをデフォルトで生成するため、TypeScriptコードベース全体が復元可能でした。
数時間以内に、開発者たちはそのコードを数十のGitHubリポジトリにミラーリングしました。コミュニティは、マスターエージェントループから「undercover mode(覆面モード)」や偽のツールインジェクションといった隠れた機能まで、あらゆるモジュールを素早く分析しました。
反応は二分されました。Anthropicのセキュリティ対策を批判する者もいれば、そのアーキテクチャに魅了される者もいました。しかし、最も建設的な反応は、「これを自分で作れるか?」と問いかけた開発者たちから来ました。
答えは「はい」です。コアとなるパターンはシンプルです。このガイドでは、各アーキテクチャレイヤーを順を追って分解し、実装の要点とサンプルコードを提供します。また、APIインタラクションのテスト手順としてApidogの活用方法も紹介します。複数ターンのAPI対話をデバッグするのが、curlコマンドよりも圧倒的に容易になります。
Claude Codeのアーキテクチャについて流出が明らかにしたこと
コードベースの概要
Claude Code(内部コードネーム「Tengu」)は約1,900ファイルからなり、以下のような構成です。
cli/ - ターミナルUI (React + Ink)
tools/ - 40以上のツール実装
core/ - システムプロンプト、パーミッション、定数
assistant/ - エージェントのオーケストレーション
services/ - API呼び出し、圧縮、OAuth、テレメトリー
CLIはInk(ターミナル出力用Reactレンダラー)によるReactアプリです。レイアウトはYoga(flexbox)、ANSIエスケープで装飾します。会話ビューやツール呼び出し表示などもすべてReactコンポーネントです。
ただし、DIYプロジェクトにReactベースのターミナルUIは不要です。シンプルなREPLループで十分動作します。
マスターエージェントループ
UIやテレメトリーを除いたClaude Codeのコアは、whileループです(内部名称「nO」)。実装ポイントは下記の通り:
- Claude APIへシステムプロンプト+ツール定義付きでメッセージ送信
- テキストまたは
tool_useブロックを含む応答を受信 - 各ツール呼び出しを名前―ハンドラーマッピングで実行
- 結果をメッセージリストに追加
- 応答にさらにツール呼び出しがあれば1へ戻る
- 単なるテキスト応答ならユーザーへ返す
「ターン」はツール呼び出しがなくなるまで継続します。エージェントパターンはこれだけです。
最小限のPythonサンプル:
import anthropic
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
def agent_loop(system_prompt: str, tools: list, messages: list) -> str:
"""The core agent loop - keep calling until no more tool use."""
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})
約30行でClaude Codeのコアパターンが表現できます。残りの複雑さはツール実装・パーミッション・コンテキスト管理・メモリです。
ツールシステムの構築
なぜ専用ツールが単一のbashコマンドより優れているのか
Claude Codeはbash経由ではなく、ファイル操作に専用ツールを用意しています。例: Readツール(catではない)、Editツール(sedではない)、Grepツール(grepと異なる)など。プロンプトでbashコマンドよりこれらを明示優先させています。
理由は
- 構造化出力: 一貫した形式でモデルが解釈しやすい
- 安全性: BashToolは危険なサブシェル構文をブロック。専用ツールならインジェクションリスクなし
- トークン効率: 結果を切り詰め・サンプリングでき、膨大なcat出力でウィンドウを圧迫しない
必須のツールセット
Claude Code流出情報から、最低限次の5ツールを用意しましょう。
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": "Max lines to read. Defaults to 2000."
}
},
"required": ["file_path"]
}
},
{
"name": "write_file",
"description": "Write content to a file. Creates the file if it doesn't 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, e.g. '*.py'"}
},
"required": ["pattern"]
}
}
]
ツールハンドラーのディスパッチ
ツール名とハンドラー関数のマッピング例:
import subprocess
import os
import re
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}'"
try:
return handler(params)
except Exception as e:
return f"Error: {str(e)}"
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"]
if content.count(old) == 0:
return f"Error: '{old[:50]}...' not found in {path}"
if content.count(old) > 1:
return f"Error: '{old[:50]}...' matches {content.count(old)} locations. Be more specific."
new_content = content.replace(old, params["new_string"], 1)
with open(path, "w") as f:
f.write(new_content)
return f"Successfully edited {path}"
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
def handle_search_code(params: dict) -> str:
pattern = params["pattern"]
path = params.get("path", os.getcwd())
file_glob = params.get("file_glob", "")
cmd = ["grep", "-rn", "--include", file_glob, pattern, path] if file_glob else \
["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
コンテキスト管理:難しい問題
なぜコンテキストはプロンプトエンジニアリングよりも重要なのか
流出コードから分かる通り、システムプロンプトよりもコンテキスト管理が重要です。Claude Codeのコンテキストコンプレッサー(内部名「wU2」)は5つの戦略を持ちます。
DIY実装では
- 自動圧縮(コンテキスト制限の92%到達時に要約+バッファ確保)
-
プロジェクトガイドライン再注入(毎ターン
CLAUDE.mdを再注入)
が必須です。
シンプルなコンプレッサーの構築例
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(m.get("content", ""))) for m 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 was 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
プロジェクトコンテキストの再注入
def build_system_prompt(project_dir: str) -> str:
"""Build system prompt with project context re-injection."""
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. Focus on the code, not explanations."""
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 = os.path.join(project_dir, "CLAUDE.md")
if os.path.exists(root_md):
with open(root_md, "r") as f:
root_context = f.read()
base_prompt += f"\n\n# Repository guidelines\n{root_context}"
return base_prompt
3層メモリシステム
Claude Codeは3層メモリアーキテクチャを採用しています。
レイヤー1:MEMORY.md(常にロード)
インデックスとして機能する軽量ファイル(1行150字以下、最大200行)。
- [User preferences](memory/user-prefs.md) - prefers TypeScript, uses Vim keybindings
- [API conventions](memory/api-conventions.md) - REST with JSON:API spec, snake_case
- [Deploy process](memory/deploy.md) - uses GitHub Actions, deploys to AWS EKS
レイヤー2:トピックファイル(オンデマンド)
インデックスから関連性を示唆されるとロード。詳細な知識や手順、パターンを記載。
レイヤー3:セッショントランスクリプト(検索のみ)
完全なセッションログ。全文ロードせず、必要な箇所のみgrep検索。
最小限のメモリシステム例
import json
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 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"---\nname: {key}\ndescription: {description}\n---\n\n{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)
save_memoryツールをツールリストに追加し、エージェントが知識永続化できるようにしましょう。
パーミッションシステムの追加
Claude Codeには5つのパーミッションモードがありますが、DIYでは3層(low/medium/high)で十分です。
RISK_LEVELS = {
"read_file": "low",
"search_code": "low",
"edit_file": "medium",
"write_file": "medium",
"run_command": "high",
}
def check_permission(tool_name: str, params: dict, auto_approve_low: bool = True) -> bool:
"""Check if the user approves this tool call."""
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"
ApidogでエージェントのAPI呼び出しをテストする
コーディングエージェントの開発では、マルチターンのAPI呼び出しをデバッグする必要があります。生ログ解析だけでは困難ですが、Apidogを使うと下記のように効率的です。
APIリクエストのキャプチャとリプレイ
- Apidogで新規プロジェクト作成
- Anthropic Messages APIエンドポイントをインポート:
POST https://api.anthropic.com/v1/messages - システムプロンプト、ツールリスト、メッセージ配列をリクエストボディに設定
- 任意のターンでパラメータを変更し、リクエストを再送信して動作検証
これでフルエージェントループを回さず、個別ターンのテストができます。ツール呼び出しやパラメータの挙動もビジュアルに切り替え検証可能です。
マルチターンの会話をデバッグする
- 各ターン後に
messages配列を環境変数として保存 - 任意の時点から会話をリプレイ
- ツール結果を比較して分岐点を特定
ツールスキーマを検証する
ツール定義(JSONスキーマ)に誤りがあるとモデルがツールを使わない等の不具合が起きます。ApidogのJSONスキーマバリデーターで事前に検証しましょう。Apidogをダウンロードして、APIインタラクションのデバッグをすぐ始められます。
すべてをまとめる:完全なREPL
以下は、すべての要素を統合した動作REPLエージェント例です。
#!/usr/bin/env python3
"""A minimal Claude Code-style coding agent."""
import anthropic
import os
import sys
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
PROJECT_DIR = os.getcwd()
def main():
system_prompt = build_system_prompt(PROJECT_DIR)
memory = load_memory_index()
if memory:
system_prompt += f"\n\n# Memory\n{memory}"
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})
messages = maybe_compact(messages, system_prompt)
current_system = build_system_prompt(PROJECT_DIR)
memory = load_memory_index()
if memory:
current_system += f"\n\n# Memory\n{memory}"
result = agent_loop(current_system, TOOLS, messages)
print(f"\n{result}\n")
if __name__ == "__main__":
main()
このサンプルで、300行未満のPythonコードでコーディングエージェントを構築できます。ファイル操作、コード編集、コマンド実行、コード検索、コンテキスト管理、セッション間メモリ永続化まで含みます。
次に追加すべきこと
Claude Code流出から、以下の機能強化パターンが有用です。
並列作業のためのサブエージェント
サブエージェント(forked agent)パターンで独立タスクを実行し、結果だけを親エージェントに返します。タスクごとにagent_loop()を新規生成し、ツールセットも絞ることでメイン会話のノイズを低減できます。
ファイル読み込みの重複排除
読み込んだファイルとその変更時刻を追跡し、未変更なら再読み込みせず「前回から変更なし」とモデルに伝えることでトークン消費を抑えます。
出力の切り詰めとサンプリング
大量出力(grep結果など)は先頭・末尾サンプリング+省略行数明記で切り詰め、コンテキスト圧迫を防ぎます。
ファイル再注入による自動圧縮
要約後も最近アクセスしたファイル内容(最大5,000トークン/ファイル)を再注入して、知識保持性を上げます。
流出から学んだこと
- コアループはシンプル(30行で成立。複雑さはツール・コンテキスト管理)
- 専用ツールはbashより優れる(構造化・安全・効率)
- メモリはレイヤー化すべき(インデックス+オンデマンド+全文ログ)
- コンテキスト管理が本質的価値(自動圧縮・ガイドライン再注入・切り詰め実装)
- モデルではなくハーネスを作る(知覚・アクション・メモリレイヤー)
マルチターンのツール利用会話やリクエストスキーマ検証、応答検証など、カスタムエージェントのAPIデバッグにはApidogを無料で試してください。APIデバッグにリソースを割かず、エージェントロジック構築に集中できます。
FAQ(よくある質問)
Claude Codeの流出からのパターンを法的に使用できますか?
この流出は設計パターンの公開です。whileループ+ツールディスパッチのエージェントはAnthropic公式APIドキュメントにも記載されており、独自コードで再現するのは一般的です。コード全文のコピーは避けましょう。
DIYコーディングエージェントにはどのモデルを使用すべきですか?
Claude Sonnet 4.6は速度・能力のバランスが良好。より複雑な設計にはOpus 4.6(ただしコスト増)、シンプル作業にはHaiku 4.5(コスト90%減)も選択肢です。
独自のコーディングエージェントを実行するにはどれくらいの費用がかかりますか?
Sonnet 4.6の典型的な30~50ターンセッションでAPI料金は1~5ドル。コンテキスト圧縮戦略でコストを最適化できます。
Claude CodeがターミナルアプリにReactを使用する理由は何ですか?
Ink(ターミナル用React)でダイアログやツール呼び出しUIなど複雑なやりとりにReactの状態管理・コンポーネント化を活用。DIYではinput() / print()のREPLだけでも十分です。
コアループの次に構築すべき最も重要な機能は何ですか?
パーミッションシステムです。最低限「書き込み/実行前の確認」ゲートで偶発的な損害を抑制できます。
Claude Codeはツール呼び出しからのエラーをどのように処理しますか?
ツールエラーはtool_resultメッセージ内のテキストとして返却。モデルがエラー内容を見てリカバリ動作を推論します。
Claude以外のモデルでもこれを使用できますか?
はい。関数呼び出し(Function Calling)をサポートするモデル(GPT-4, Gemini, Llama等)で同パターンが利用可能です。API呼び出し仕様は調整が必要ですが、コアロジック・ツール・メモリ構造は共通です。
エージェントが危険なコマンドを実行するのを防ぐにはどうすればよいですか?
rm -rf /やmkfs等危険コマンドのブロックリスト+run_command時の明示承認を必須化しましょう。Claude Codeも操作リスク分類に応じてプロンプトやブロックを実装しています。あなたのツールにも同等の仕組みを組み込んでください。

Top comments (0)