Every great CLI tool starts the same way — with an empty directory and a handful of decisions that will shape everything built on top of it. For our Swift agent, those decisions matter more than usual. We’re going to build a Claude Code-style coding assistant from scratch over the next eight guides, adding one mechanism per stage to a core that never changes. Getting the foundation right means we won’t need to restructure anything later.
The thesis driving this project is simple: Claude Code’s effectiveness comes from architectural restraint — a small set of excellent tools, thin orchestration, and heavy reliance on the model itself. We’re going to prove that by building our own version in Swift, one layer at a time.
In this guide, let’s set up the project structure, make sure everything compiles and runs, and lay the groundwork for the agent we’ll start building in the next stage.
Starting with Swift Package Manager
Let’s create our project and initialize it as a Swift package:
mkdir swift-claude-code
cd swift-claude-code
git init
swift package init --type executable --name swift-claude-code
This gives us a working starting point — SPM generates a Sources/ directory, a Package.swift, and a basic executable target. We could start writing code here and it would compile just fine.
However, the default layout puts everything into a single executable target, which means our agent logic and our command-line entry point live in the same place. That’s a problem for two reasons: we can’t write unit tests against an executable target (Swift Testing needs a library to import), and we can’t reuse any of our agent logic outside the CLI. Let’s fix that by splitting into two targets.
The two-target layout
The architecture we want is straightforward — a Core library that holds all the real logic, and a thin cli executable that just wires things together and starts the REPL:
swift-claude-code/
├── Package.swift
├── Sources/
│ ├── Core/ ← library (all agent logic)
│ └── cli/ ← executable (thin entry point)
└── Tests/
└── CoreTests/ ← tests import Core
Let’s replace SPM’s generated code with our two-target structure:
rm -rf Sources/*.swift
mkdir -p Sources/Core
mkdir -p Sources/cli
Now we need something for each target to compile. Let’s start with the Core library — for now, just a placeholder that proves the target exists:
// Sources/Core/Agent.swift
public enum Agent {
public static let version = "0.1.0"
}
We’re using a caseless enum as a pure namespace here — it’ll evolve into a full class in the next guide once we need mutable state.
The cli target is our executable entry point. Here’s where Swift’s @main attribute comes in:
// Sources/cli/SwiftClaudeCode.swift
import Core
@main
enum SwiftClaudeCode {
static func main() async throws {
print("swift-claude-code v\(Agent.version)")
}
}
Notice async throws on main() — we don’t need async yet, but every API call we’ll make starting in the next guide will be asynchronous, so we’re declaring the entry point as async from day one.
One thing to keep in mind: @main and main.swift can’t coexist in the same target. If you see a main.swift in the target, delete it — @main replaces it and will let us adopt AsyncParsableCommand from swift-argument-parser later without any restructuring.
The package manifest
With our source files in place, let’s replace SPM’s generated Package.swift with a manifest that reflects our two-target architecture:
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "swift-claude-code",
platforms: [.macOS(.v10_15)],
products: [
.executable(name: "claude", targets: ["cli"]),
.library(name: "Core", targets: ["Core"]),
],
dependencies: [
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.32.0"),
],
targets: [
.executableTarget(
name: "cli",
dependencies: ["Core"],
path: "Sources/cli"
),
.target(
name: "Core",
dependencies: [
.product(
name: "AsyncHTTPClient",
package: "async-http-client"
),
],
path: "Sources/Core"
),
.testTarget(
name: "CoreTests",
dependencies: ["Core"],
path: "Tests/CoreTests"
),
]
)
There’s a deliberate dependency choice here worth discussing. We’re pulling in AsyncHTTPClient from the swift-server project rather than using Foundation’s built-in URLSession. The reason is cross-platform reliability — URLSession’s async APIs weren’t available on Linux until very recently and remain inconsistent between Apple’s Foundation and the open-source swift-corelibs-foundation. AsyncHTTPClient is built on SwiftNIO, works identically on macOS and Linux, and handles async responses cleanly with Swift’s concurrency model.
Also note swift-tools-version: 6.2. This gives us Swift’s strict concurrency checking enabled by default — the compiler will catch data races at compile time rather than leaving them as runtime surprises. That strictness will pay for itself when we add background tasks and actors later in the series.
Adding tests from the start
Let’s set up our test target before we forget:
mkdir -p Tests/CoreTests
And our first test file to go inside it:
// Tests/CoreTests/AgentTests.swift
import Testing
@testable import Core
@Test func versionExists() {
#expect(Agent.version == "0.1.0")
}
We’re using Swift Testing (the @Test macro and #expect assertions) rather than XCTest. It’s the modern testing framework, it works on both macOS and Linux, and it supports async test functions — which we’ll need extensively once we start testing the agent loop.
One test might seem trivial, but it proves something important: Core is importable as a library, the test target can reach it, and our whole build graph is wired up correctly.
Environment configuration
Our agent will need an Anthropic API key to function. Let’s set up the convention now with an .env.example that documents what’s needed, and a .gitignore to keep the real .env, .build/, and other artifacts out of version control:
# .env.example
ANTHROPIC_API_KEY=your-api-key-here
MODEL_ID=claude-sonnet-4-6
We’ll read the API key from the process environment using ProcessInfo.processInfo.environment["ANTHROPIC_API_KEY"] when we build the API client in the next guide.
Taking it for a spin
Let’s verify everything works. The first build will take a minute or two as SPM resolves AsyncHTTPClient and its SwiftNIO dependencies:
swift build
swift run claude
# swift-claude-code v0.1.0
swift test
# Test Suite 'All tests' passed
# 1 test passed
If all three commands succeed, our foundation is solid. We have a two-target package where all logic lives in a testable library, an entry point ready for async work, and a dependency on the HTTP client we’ll need for API calls. That’s a lot of infrastructure for a few files, but none of it will need to change as we add capabilities over the next eight guides.
What we’ve built and where we’re going
We now have a Swift package with a clean separation between library and executable, strict concurrency enabled, and a test harness ready to go. It doesn’t do anything interesting yet — but that’s the point. Every stage in this series adds exactly one mechanism, and this stage’s mechanism is the project structure itself.
In the next guide, we’ll bring this project to life by making our first API call to Claude and building the agent loop — the while true kernel that drives everything else. Thanks for reading!