DEV Community

Cover image for Build a Claude Code Clone From Zero to One Using Golang
Tidiane Stano
Tidiane Stano

Posted on

Build a Claude Code Clone From Zero to One Using Golang

Introduction

Many developers have enjoyed building tools from scratch: writing web crawlers, implementing instant messaging systems, and building simple databases. This tutorial series follows the same hands-on philosophy, guiding readers to implement an AI Agent CLI tool similar to Claude Code entirely in Go.

The series adopts a unique pedagogical design. Each lesson delivers runnable code stored within an independent folder, and code volumes are intentionally kept compact. The design choice addresses a common pitfall in learning: developers often paste large blocks of code directly into LLMs to understand functionality, which reduces hands-on comprehension. Human cognitive capacity for reading and absorbing source code in a single session is limited. Code length itself forms a learning threshold, and concise code with focused logic delivers higher learning value.

Additional design rules support effective comparison and incremental learning. Code between chapters avoids cross-folder imports, and repeated code segments are preserved intentionally. Learners can compare two different versions side-by-side within a single IDE. Later chapters build incrementally on prior implementations, so developers can observe how logic evolves with simple diff tools. Most importantly, the project relies exclusively on Go standard libraries with zero third-party dependencies. The complete program requires merely four built-in packages, eliminating the burden of researching external library implementations and transitive dependency risks.

The full teaching roadmap spans ten planned episodes, each packaged as a standalone executable module.

Episode Folder Core Topic Status
1 01-http Non-stream LLM API calls for three mainstream API dialects Published
2 02-sse SSE streaming output implementation Published
3 03-cmd Command-line interactive loop, request wrapping Completed
4 04-console Full-screen terminal UI, key parsing and vim key bindings Completed
5 05-tool-call Tool calling implementation, enabling the agent to perform actions beyond chat Planned
6 06-agent-loop Core Agent main loop Planned
7 07-file-tools File read and write toolset Planned
8 08-bash Shell command execution and security sandbox Planned
9 09-permission Permission control system Planned
10 10-context Context and token quota management Planned

The complete source repository is hosted on GitHub. Developers are strongly recommended to clone the repository locally and follow each lesson sequentially.

Episode 1: Implement Basic LLM HTTP Requests

The first episode covers fundamental LLM API communication. Mainstream LLM API interfaces can be grouped into three primary dialects.

Dialect Main adopters Endpoint Auth Header Request Structure
Chat Completions OpenAI compatible APIs, Qwen, Kimi, GLM, Ollama /v1/chat/completions Authorization: Bearer messages[], plain string content
Responses New OpenAI interface /v1/responses Authorization: Bearer input array, string or message objects
Messages Anthropic Claude /v1/messages x-api-key + anthropic-version header messages[], system as top-level field, mandatory max_tokens

The Gemini API uses a separate specification and is excluded from this comparison. All three dialects share the same fundamental workflow, differing only in three dimensions: API endpoint path, authentication header format, and JSON request schema.

The project structure for 01-http separates logic into distinct source files:

  • main.go: Load provider configuration and match target API dialect
  • http.go: Shared HTTP transmission layer
  • openai_compat.go: Implementation for Chat Completions dialect
  • openai_responses.go: Implementation for Responses dialect
  • anthropic.go: Implementation for Anthropic Messages dialect

The simplified OpenAI-compatible implementation is shown below. The struct definitions mirror the JSON schema required by Chat Completions endpoints.

type ChatCompletionReq struct {
    Model    string              `json:"model"`
    Messages []ChatCompletionMsg `json:"messages"`
    Stream   bool                `json:"stream"`
}

type ChatCompletionMsg struct {
    Role    string `json:"role"`
    Content string `json:"content"`
}

type ChatCompletionResp struct {
    Choices []struct {
        Message struct {
            Role    string `json:"role"`
            Content string `json:"content"`
        } `json:"message"`
        FinishReason string `json:"finish_reason"`
    } `json:"choices"`
    Usage struct {
        PromptTokens     int `json:"prompt_tokens"`
        CompletionTokens int `json:"completion_tokens"`
        TotalTokens      int `json:"total_tokens"`
    } `json:"usage"`
}

func ChatCompletion(apiKey, baseURL, model, prompt string) (string, error) {
    body := ChatCompletionReq{
        Model: model,
        Messages: []ChatCompletionMsg{
            {Role: "system", Content: "You are a concise assistant, keep answers brief."},
            {Role: "user", Content: prompt},
        },
        Stream: false,
    }
    jsonData, err := json.Marshal(body)
    if err != nil {
        return "", err
    }
    req, err := http.NewRequest(http.MethodPost, baseURL+"/v1/chat/completions", bytes.NewReader(jsonData))
    if err != nil {
        return "", err
    }
    req.Header.Set("Authorization", "Bearer "+apiKey)
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{Timeout: 60 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    var respBody ChatCompletionResp
    err = json.NewDecoder(resp.Body).Decode(&respBody)
    if err != nil {
        return "", fmt.Errorf("parse response failed: %w", err)
    }
    if len(respBody.Choices) == 0 {
        return "", fmt.Errorf("response contains no choices")
    }
    return respBody.Choices[0].Message.Content, nil
}
Enter fullscreen mode Exit fullscreen mode

The core postJSON helper function shared by all three dialects contains roughly ten lines of code. It builds the HTTP request, attaches headers, sends the payload and returns the raw response body.

When running non-stream requests with stream: false, the LLM completes full text generation before returning the entire JSON payload. Developers must configure a reasonable HTTP client timeout value; overly short timeouts terminate requests prematurely.

After running the program and printing the returned JSON payload, developers gain clear visibility of the complete request-response lifecycle of LLM API calls. This completes the first episode implementation.

Episode 2: SSE Streaming Implementation

The second lesson builds upon the basic HTTP client and implements Server-Sent Events (SSE) streaming, the standard protocol for incremental text output in chat applications.

SSE maintains a persistent HTTP connection. The server continuously pushes plain-text data lines over this connection, similar to incremental file downloading. Three core parsing rules govern SSE streams:

  1. Each line follows field: value syntax.
  2. Empty lines mark the end of an individual event block. Lines starting with : represent comments and must be skipped.
  3. The event and data fields carry event metadata; application logic only needs to process the data field.

Client-side logic keeps the connection alive and reads the response stream line-by-line, parsing each chunk immediately as it arrives.

Differences Between Streaming and Non-streaming Mode

Item Non-stream Streaming
Request flag stream: false stream: true
Response format Single complete JSON object Multiple incremental JSON chunks
Text rendering Render after full response received Render incrementally from delta fragments
Timeout handling Controlled via http.Client.Timeout Cannot enforce static client-side timeout

The keyword delta refers to incremental fragments. Each chunk only carries newly generated text instead of the full message.

The core streaming implementation for OpenAI-compatible dialects is listed below.

func StreamSSE(apiKey, baseURL, model, prompt string) error {
    body := ChatCompletionReq{
        Model: model,
        Messages: []ChatCompletionMsg{
            {Role: "system", Content: "You are a concise assistant."},
            {Role: "user", Content: prompt},
        },
        Stream: true,
    }
    jsonData, err := json.Marshal(body)
    if err != nil {
        return err
    }
    req, err := http.NewRequest(http.MethodPost, baseURL+"/v1/chat/completions", bytes.NewReader(jsonData))
    if err != nil {
        return err
    }
    req.Header.Set("Authorization", "Bearer "+apiKey)
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    scanner := bufio.NewScanner(resp.Body)
    for scanner.Scan() {
        line := scanner.Text()
        if line == "" || strings.HasPrefix(line, ":") {
            continue
        }
        if !strings.HasPrefix(line, "data: ") {
            continue
        }
        dataPart := strings.TrimPrefix(line, "data: ")
        if dataPart == "[DONE]" {
            break
        }
        var chunk struct {
            Choices []struct {
                Delta struct {
                    Content string `json:"content"`
                } `json:"delta"`
            } `json:"choices"`
        }
        err = json.Unmarshal([]byte(dataPart), &chunk)
        if err != nil {
            return err
        }
        if len(chunk.Choices) > 0 {
            fmt.Print(chunk.Choices[0].Delta.Content)
        }
    }
    return scanner.Err()
}
Enter fullscreen mode Exit fullscreen mode

The SSE parser reads raw response text line by line. It skips comment lines and empty separators, extracts the data segment and parses embedded JSON objects. A critical detail: streaming requests cannot rely on http.Client.Timeout. The streaming lifecycle is controlled by the server, not client-side timers.

Each API dialect uses different termination markers for streaming sessions:
| Dialect | Delta text field | Termination signal |
|---|---|---|
| Chat Completions | choices[0].delta.content | Plain text [DONE] |
| Responses | output_text within response blocks | response.completed flag |
| Messages (Claude) | delta.text | message_stop event |

The [DONE] marker is plain text and not valid JSON. Code must explicitly intercept this literal string and stop parsing before attempting JSON deserialization.

When developers run the streaming program, text outputs progressively print to the terminal character by character. If incremental text fails to display, it typically indicates the stream flag is missing or intermediate gateway services interrupted SSE transmission.

Subsequent Episodes and Project Roadmap

After implementing the transport layer, the series moves into interactive CLI construction. Episode three creates a command interaction loop that continuously receives user input and wraps requests for the LLM. Episode four builds a full-screen terminal UI with keyboard capture and Vim-style shortcuts.

The core Agent capabilities start from episode five. Tool calling enables the LLM to trigger external functions, moving the program from simple chat to actionable agent workflows. The sixth episode implements the central Agent loop: the core state machine that plans tasks, invokes tools, observes outputs and iterates until objectives finish.

Following episodes implement practical agent tools: file read/write utilities, a constrained bash sandbox for command execution, permission systems for security isolation, and context/token quota management to avoid exceeding model context windows.

Building multi-model agent systems often requires unified routing for different LLM providers. 4sapi serves as an API gateway to consolidate model endpoints, simplifying switching between API dialects during local agent development.

This series intentionally avoids large monolithic codebases. Each lesson remains small and independently runnable. This incremental approach helps developers trace how individual components combine to build a complete agent, rather than copy-pasting finished products. Every component builds sequentially: HTTP transport, streaming parsing, terminal interface, tool calling, agent loop, security controls and context management.

Conclusion

This hands-on Golang tutorial series demystifies the internals of AI Agent CLI applications comparable to Claude Code. Starting from raw HTTP requests and SSE streaming, developers build each layer step by step using only Go standard libraries. The project isolates each feature in independent folders, making code comparison and iterative learning straightforward.

Understanding these low-level API transport and parsing mechanisms is foundational for building custom agents. Developers learn how different LLM API dialects structure payloads, how incremental streaming works, and how to wrap these primitives into interactive command-line agents. The completed foundation can be extended with custom tools, permission guardrails and context management for production-grade AI agent applications.

International access: https://4sapi.com
Domestic access: https://4sapi.cn

Top comments (0)