|
| 1 | +# swift-claude-code |
| 2 | + |
| 3 | +Building a simplified Claude Code-like CLI agent from scratch in Swift. |
| 4 | + |
| 5 | +> **Current progress:** Stage 02 of 12 — tool dispatch with `read_file`, `write_file`, `edit_file` |
| 6 | +
|
| 7 | +## Why This Exists |
| 8 | + |
| 9 | +If you've used Claude Code, you know exactly what I'm talking about. Especially the first time you use it — it's just _different_ than any other coding agent out there. There's definitely some magic behind Claude Code, and I went down a huge rabbit hole trying to figure out what makes it special. |
| 10 | + |
| 11 | +My hypothesis: **Claude Code is so good because of how simple it is.** |
| 12 | + |
| 13 | +Not the UI — the architecture. Down to the fact that it doesn't really have many tools. And the tools it does have are really simple: a search tool, a file editing tool. That's about it. But those tools are _really, really good_. |
| 14 | + |
| 15 | +The other big thing is that Claude Code relies on the model way more than other tools. Most people build a lot of scaffolding around the model, but Claude Code really lets the model do all of the heavy lifting. |
| 16 | + |
| 17 | +So I wanted to understand this deeply — not by reading about it, but by building it. This project rebuilds the core agent loop from scratch in Swift, one layer at a time, to see exactly how few moving parts you actually need. |
| 18 | + |
| 19 | +## Architecture |
| 20 | + |
| 21 | +Two-target Swift Package Manager project: |
| 22 | + |
| 23 | +``` |
| 24 | +swift-claude-code/ |
| 25 | +├── Package.swift |
| 26 | +├── Sources/ |
| 27 | +│ ├── Core/ ← library (all logic) |
| 28 | +│ │ ├── API/ |
| 29 | +│ │ ├── Agent.swift agent loop + tool dispatch |
| 30 | +│ │ └── ShellExecutor.swift |
| 31 | +│ └── cli/ ← executable (@main entry point) |
| 32 | +└── Tests/CoreTests/ |
| 33 | +``` |
| 34 | + |
| 35 | +**Core** is the library — API client, shell executor, agent loop, tools, everything testable. **cli** is just the entry point. The executable is called `claude`. |
| 36 | + |
| 37 | +Raw HTTP to `POST https://api.anthropic.com/v1/messages` using [AsyncHTTPClient](https://github.com/swift-server/async-http-client). Works on both macOS and Linux. |
| 38 | + |
| 39 | +## The Agent Loop |
| 40 | + |
| 41 | +The whole thing boils down to one loop: |
| 42 | + |
| 43 | +```swift |
| 44 | +func run(query: String) async throws -> String { |
| 45 | + messages.append(.user(query)) |
| 46 | + |
| 47 | + while true { |
| 48 | + let request = APIRequest( |
| 49 | + model: model, system: systemPrompt, messages: messages, tools: Self.toolDefinitions |
| 50 | + ) |
| 51 | + let response = try await apiClient.createMessage(request) |
| 52 | + messages.append(Message(role: .assistant, content: response.content)) |
| 53 | + |
| 54 | + guard response.stopReason == .toolUse else { |
| 55 | + return response.content.textContent |
| 56 | + } |
| 57 | + |
| 58 | + var results: [ContentBlock] = [] |
| 59 | + for block in response.content { |
| 60 | + if case .toolUse(let id, let name, let input) = block { |
| 61 | + let output = await executeTool(name: name, input: input) |
| 62 | + results.append(.toolResult(toolUseId: id, content: output, isError: false)) |
| 63 | + } |
| 64 | + } |
| 65 | + messages.append(Message(role: .user, content: results)) |
| 66 | + } |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +That's it. The loop is the invariant. Tools are the variable. Every stage adds entries to the tool handler dictionary and injection points before the API call, but the loop body itself never changes. |
| 71 | + |
| 72 | +## Roadmap |
| 73 | + |
| 74 | +Each stage adds one mechanism on top of the previous one. Progress is tracked via git tags. |
| 75 | + |
| 76 | +| Stage | What It Adds | Tag | |
| 77 | +| ------ | ---------------------------------------------------------------------- | ------------------ | |
| 78 | +| **00** | Bootstrap: SPM project | `00-bootstrap` | |
| 79 | +| **01** | Agent loop + bash tool | `01-agent-loop` | |
| 80 | +| **02** | Tool dispatch: `read_file`, `write_file`, `edit_file` with path safety | `02-tool-dispatch` | |
| 81 | +| 03 | TodoWrite: Codable todo tracking with nag reminder injection | — | |
| 82 | +| 04 | Subagents: recursive `agentLoop()` with fresh messages | — | |
| 83 | +| 05 | Skill loading: read `.md` files from disk, inject as tool results | — | |
| 84 | +| 06 | Context compaction: 3-layer strategy (micro, auto, manual) | — | |
| 85 | +| 07 | Task system: file-based CRUD with dependency DAG | — | |
| 86 | +| 08 | Background tasks: `Task {}` + actor-based notification queue | — | |
| 87 | +| 09 | Agent teams: JSONL mailbox files + actor coordination | — | |
| 88 | +| 10 | Team protocols: request-response with correlation IDs | — | |
| 89 | +| 11 | Autonomous agents: idle-poll-claim cycle | — | |
| 90 | +| 12 | Worktree isolation: `git worktree` via Process | — | |
| 91 | + |
| 92 | +## Tech Stack |
| 93 | + |
| 94 | +- **Swift 6.2** with strict concurrency |
| 95 | +- **AsyncHTTPClient** (SwiftNIO-based) for cross-platform HTTP + streaming SSE |
| 96 | +- **Foundation `Process`** for shell command execution |
| 97 | +- macOS 10.15+ / Linux |
| 98 | + |
| 99 | +## Getting Started |
| 100 | + |
| 101 | +```bash |
| 102 | +git clone https://github.com/ivan-magda/swift-claude-code.git |
| 103 | +cd swift-claude-code |
| 104 | + |
| 105 | +# Set up your API key and model |
| 106 | +cp .env.example .env |
| 107 | +# Edit .env with your ANTHROPIC_API_KEY and MODEL_ID |
| 108 | + |
| 109 | +swift build |
| 110 | +swift run claude |
| 111 | +``` |
| 112 | + |
| 113 | +## References |
| 114 | + |
| 115 | +- [Anthropic Messages API](https://docs.anthropic.com/en/api/messages) — the single endpoint the entire agent talks to |
| 116 | +- [Anthropic Tool Use](https://docs.anthropic.com/en/docs/build-with-claude/tool-use/overview) — how tool definitions, `tool_use`, and `tool_result` work |
| 117 | + |
| 118 | +## License |
| 119 | + |
| 120 | +MIT |
0 commit comments