← Back to blog

Crush Architecture Deep Dive

·Crush
crushgoarchitecturetui

TL;DR: Crush is the canonical Go continuation of OpenCode (SST's version is a fork). This deep-dive maps 31 internal packages, the Fantasy multi-provider abstraction, coordinator pattern, and Bubble Tea v2 TUI. Essential reading if you're extending Crush, understanding its agent orchestration model, or evaluating it as a foundation for multi-agent systems.

Charmbracelet/Crush Architecture Deep-Dive

Status: Complete


Key Finding: Crush IS the Original

From maintainer meowgorithm: "Crush started out as OpenCode and has continued here at Charm with the original author. sst/opencode is a fork of that earlier work."

Lineage: Crush (original) → archived as opencode-ai/opencode → forked by SST → forked by anomalyco → rewritten in TypeScript. Crush is the canonical Go continuation.


1. Repository Structure

charmbracelet/crush/
├── .github/                    # CI/CD workflows
├── .golangci.yml               # Linter config (golangci-lint v2)
├── .goreleaser.yml             # Multi-platform release automation
├── AGENTS.md                   # Dev guide (build/test/lint/style)
├── CLA.md                      # Contributor License Agreement
├── LICENSE.md                  # FSL-1.1-MIT
├── Taskfile.yaml               # Task runner (build, test, lint, dev, release)
├── crush.json                  # Default/example config
├── go.mod / go.sum             # Go 1.25.5
├── main.go                     # Entry point
├── schema.json                 # Config JSON Schema
├── sqlc.yaml                   # SQL code generation config
├── internal/                   # All application code (31 packages)
└── scripts/                    # Build/utility scripts

Internal Packages (31 total)

internal/
├── agent/           # Core agent loop, coordinator, tools, prompts, sub-agents
│   ├── hyper/       # Hyper provider integration
│   ├── prompt/      # Prompt builder
│   ├── templates/   # System prompt templates (coder.md.tpl, task.md.tpl, etc.)
│   └── tools/       # 31 Go files - all built-in tools
│       └── mcp/     # MCP tool wrappers (init, tools, prompts, resources)
├── ansiext/         # ANSI extensions
├── app/             # App orchestrator (app.go, provider.go, lsp_events.go)
├── cmd/             # CLI commands (cobra)
│   └── stats/       # Usage statistics subcommand
├── commands/        # Custom slash commands system
├── config/          # Hierarchical config (19 files including catwalk, copilot, hyper)
├── csync/           # Concurrent-safe data structures
├── db/              # SQLite database layer (sqlc-generated, migrations)
│   ├── migrations/
│   └── sql/
├── diff/            # Diff generation
├── env/             # Environment handling
├── event/           # Telemetry/analytics events
├── filepathext/     # Filepath utilities
├── filetracker/     # File tracking per session
├── format/          # Output formatting
├── fsext/           # Filesystem extensions
├── history/         # File edit history
├── home/            # Home directory resolution
├── log/             # Logging
├── lsp/             # Language Server Protocol client
│   └── util/
├── message/         # Message model and service (attachment, content types)
├── oauth/           # OAuth integration
│   ├── copilot/     # GitHub Copilot OAuth
│   └── hyper/       # Hyper OAuth
├── permission/      # Permission model and service
├── projects/        # Project management
├── pubsub/          # Generic pub/sub broker
├── session/         # Session model and service
├── shell/           # POSIX shell emulation (mvdan.cc/sh)
├── skills/          # Agent Skills standard (agentskills.io)
├── stringext/       # String utilities
├── ui/              # TUI layer
│   ├── anim/        # Animations
│   ├── attachments/ # File attachment UI
│   ├── chat/        # Chat display
│   ├── common/      # Shared UI context
│   ├── completions/ # Autocomplete
│   ├── dialog/      # Dialog/overlay system
│   ├── diffview/    # Diff viewer
│   ├── image/       # Image rendering
│   ├── list/        # List components
│   ├── logo/        # Branding
│   ├── model/       # Main UI model (17 files)
│   ├── styles/      # Theme/styling
│   └── util/        # UI utilities
├── update/          # Auto-update checking
└── version/         # Version info

2. Key Dependencies (from go.mod)

Core Charm Ecosystem

PackagePurpose
charm.land/fantasyLLM provider abstraction ("one API, many providers")
charm.land/catwalkCommunity-maintained model registry with auto-updating metadata
charm.land/x/vcrVCR test recording/replay
charm.land/bubbletea/v2TUI framework (v2, Elm architecture)
charm.land/bubbles/v2TUI components (v2)
charm.land/lipgloss/v2Terminal styling (v2)
charm.land/glamour/v2Markdown rendering
charm.land/log/v2Structured logging
charmbracelet/fangCLI configuration

External Dependencies

PackagePurpose
openai-go/v2 (v2.7.1)OpenAI client
modelcontextprotocol/go-sdk (v1.2.0)MCP Go SDK
go-git/v5 (v5.16.5)Git operations
ncruces/go-sqlite3Pure-Go SQLite (primary)
modernc.org/sqliteAlternative SQLite driver
spf13/cobraCLI framework
mvdan.cc/sh/v3POSIX shell interpreter/emulation
JohannesKaufmann/html-to-markdownHTML conversion
PuerkitoBio/goqueryHTML parsing
alecthomas/chroma/v2Code highlighting
sourcegraph/jsonrpc2LSP client communication

Go version: 1.25.5


3. Coordinator Pattern (agent/coordinator.go)

Top-level orchestrator managing the agent lifecycle.

type Coordinator interface {
    Run(ctx context.Context, sessionID, prompt string,
        attachments ...message.Attachment) (*fantasy.AgentResult, error)
    Cancel(sessionID string)
    CancelAll()
    IsSessionBusy(sessionID string) bool
    IsBusy() bool
    QueuedPrompts(sessionID string) int
    QueuedPromptsList(sessionID string) []string
    ClearQueue(sessionID string)
    Summarize(context.Context, string) error
    Model() Model
    UpdateModels(ctx context.Context) error
}

type coordinator struct {
    cfg           *config.Config
    sessions      session.Service
    messages      message.Service
    permissions   permission.Service
    history       history.Service
    filetracker   filetracker.Service
    lspManager    *lsp.Manager
    currentAgent  SessionAgent
    agents        map[string]SessionAgent
    readyWg       errgroup.Group
}

Key Methods

  • NewCoordinator: Initializes with config, builds coder agent, sets up system prompt + tools async
  • Run: Refreshes models, merges provider options (3-layer: catwalk defaults → provider config → model config), handles OAuth token refresh on 401, executes agent
  • buildAgent: Constructs SessionAgent with large/small models, system prompt, tools
  • buildTools: Assembles tools (bash, edit, fetch, grep, LSP, MCP) based on agent config
  • buildProvider: Factory for appropriate provider

Provider Builder Methods

buildOpenaiProvider        buildAzureProvider
buildAnthropicProvider     buildBedrockProvider
buildOpenrouterProvider    buildGoogleProvider
buildVercelProvider        buildGoogleVertexProvider
buildOpenaiCompatProvider  buildHyperProvider

4. Fantasy Library (charm.land/fantasy)

Go equivalent of Vercel's AI SDK. "Multi-provider, multi-model, one API."

// Provider interface
type Provider interface {
    Name() string
    LanguageModel(ctx context.Context, modelID string) (LanguageModel, error)
}

// Language model interface
type LanguageModel interface {
    Generate(ctx context.Context, call Call) (*Response, error)
    Stream(ctx context.Context, call Call) (StreamResponse, error)
    GenerateObject(ctx context.Context, call ObjectCall) (*ObjectResponse, error)
    StreamObject(ctx context.Context, call ObjectCall) (ObjectStreamResponse, error)
    Provider() string
    Model() string
}

// Usage
provider, err := openrouter.New(openrouter.WithAPIKey(key))
model, err := provider.LanguageModel(ctx, "model-identifier")
agent := fantasy.NewAgent(model, fantasy.WithSystemPrompt("..."), fantasy.WithTools(tool1, tool2))
result, err := agent.Generate(ctx, fantasy.AgentCall{...})

Supported Providers

OpenAI, Anthropic, Google, Azure, Bedrock, VertexAI, OpenRouter, OpenAI-compatible

Current Limitations

  • No image/audio/PDF model support yet
  • No provider-native tools (web search)
  • Preview status - API may change

5. Agent Loop (agent/agent.go)

SessionAgent Interface

type SessionAgent interface {
    Run(ctx context.Context, sessionID, prompt string,
        attachments ...message.Attachment) (*fantasy.AgentResult, error)
    Summarize(ctx context.Context, sessionID string) error
    SetModels(large, small Model)
    SetTools(tools []fantasy.AgentTool)
    SetSystemPrompt(prompt string)
    Cancel(sessionID string)
    CancelAll()
    IsSessionBusy(sessionID string) bool
    IsBusy() bool
    QueuedPrompts(sessionID string) int
    QueuedPromptsList(sessionID string) []string
    ClearQueue(sessionID string)
}

Agent Loop Flow

  1. User submits prompt to Coordinator
  2. Coordinator queues prompt in per-session FIFO queue (one active request per session)
  3. SessionAgent creates user message in SQLite, broadcasts event via pubsub
  4. SessionAgent calls fantasy.Agent.Stream() with callbacks
  5. Streaming callbacks persist text deltas, tool calls, tool results as they arrive
  6. Tools execute synchronously within the streaming loop
  7. Stream completes, message finalized in database
  8. Next queued prompt processes automatically

Two Model Types

  • Large model: Primary generation and tool use
  • Small model: Summarization, title generation, lightweight tasks

Automatic Summarization

When approaching context window limits:

  • Threshold: reserve 20k tokens (for windows >200k) or 20% (for smaller windows)
  • Process: halt generation, create summary via small model, reset token counts, persist summary
  • Pre-summary history is discarded on session reload

Loop Detection (agent/loop_detection.go)

  • Examines last 10 tool call steps
  • Creates SHA-256 fingerprints of (tool name + input + output) pairs
  • If any signature appears >5 times, signals the agent to break the cycle

6. Tool System (agent/tools/)

Tool Interface (from Fantasy)

type AgentTool interface {
    Info() ToolInfo
    Run(ctx context.Context, params ToolCall) (ToolResponse, error)
    ProviderOptions() ProviderOptions
    SetProviderOptions(opts ProviderOptions)
}

// Generic constructor - params struct auto-generates JSON schema from tags
func NewAgentTool[TInput any](
    name string,
    description string,
    fn func(ctx context.Context, input TInput, call ToolCall) (ToolResponse, error),
) AgentTool

// For parallel-safe tools (like agent_tool)
func NewParallelAgentTool[TInput any](...) AgentTool

Tool Creation Pattern (Example)

type EditParams struct {
    FilePath   string `json:"file_path" description:"The absolute path to the file to modify"`
    OldString  string `json:"old_string" description:"The text to replace"`
    NewString  string `json:"new_string" description:"The text to replace it with"`
    ReplaceAll bool   `json:"replace_all,omitempty" description:"Replace all occurrences"`
}

func NewEditTool(permissions permission.Service, ...) fantasy.AgentTool {
    return fantasy.NewAgentTool(
        "edit",
        editDescription,
        func(ctx context.Context, params EditParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
            // Implementation
        },
    )
}

Built-In Tools (31+ files, 20+ tools)

CategoryToolsSource Files
Shellbashbash.go
File Opsview, edit, multi_edit, writeview.go, edit.go, multiedit.go, write.go
Searchls, glob, grepls.go, glob.go, grep.go
LSPdiagnostics, references, lsp_restartdiagnostics.go, references.go, lsp_restart.go
MCPlist_mcp_resources, read_mcp_resource, MCP toolslist_mcp_resources.go, read_mcp_resource.go, mcp-tools.go
Networkfetch, agentic_fetch, download, web_fetch, web_searchcorresponding .go files
Code Searchsourcegraphsourcegraph.go
Agentagent (sub-agent spawning)agent_tool.go
Task Mgmttodos, job_kill, job_outputtodos.go, job_kill.go, job_output.go
Safetysafe (read-only tool set)safe.go

Tool Registration and Filtering

  1. Constructed with injected dependencies (permissions, LSP manager, history, etc.)
  2. Filtered by agent config's AllowedTools slice
  3. MCP tools filtered via AllowedMCP map (server name -> permitted tool names)
  4. Sorted alphabetically for consistency
  5. Coder agents get full tool access; Task agents get read-only tools only

Permission Integration

type PermissionRequest struct {
    ID, SessionID, ToolCallID, ToolName string
    Description, Action string
    Params map[string]any
    Path string
}

Tools call permissions.Request() which blocks until user approval/denial. The --yolo flag auto-approves everything. Persistent grants cache approval for identical future requests within a session.

Context Propagation

Tools receive session metadata via Go context:

  • SessionIDContextKey / MessageIDContextKey - current session/message
  • SupportsImagesContextKey / ModelNameContextKey - model capabilities

7. Permission System (internal/permission/)

type Service interface {
    GrantPersistent(permission)    // Approve + cache for session
    Grant(permission)              // One-time approval
    Deny(permission)               // Reject
    Request(ctx, opts)             // Block until user responds
    AutoApproveSession(id)         // Pre-approve entire session
    SetSkipRequests(bool)          // Disable permission checks
    SkipRequests() bool            // Query skip state
    SubscribeNotifications(ctx)    // Receive approval outcomes
}

Permission Flow

  1. Tool calls Request() with operation details
  2. Early exits: skip=true, allowlist match, session auto-approved
  3. Cache check: session history for identical prior approvals
  4. User prompt: pub/sub → TUI dialog
  5. Blocking wait: channel-based response
  6. Persistent storage: GrantPersistent() for future requests

8. TUI Architecture (internal/ui/)

Framework

Built on Bubble Tea v2 following Elm architecture (Model-Update-View).

Main UI Model (ui/model/ui.go)

type UI struct {
    com         *common.Common        // Shared app context
    session     *session.Session      // Current session
    width, height int                 // Terminal dimensions
    layout      uiLayout
    focus       uiFocusState          // None, Editor, Main
    state       uiState              // Onboarding, Initialize, Landing, Chat
    textarea    textarea.Model        // Input editor
    chat        *Chat                 // Chat display component
    dialog      *dialog.Overlay       // Dialog/overlay system
    status      *Status               // Status bar
    attachments *attachments.Attachments
    completions *completions.Completions
}

Component Hierarchy

UI (root model)
├── Header/Logo
├── Sidebar (session list)
├── Chat display
│   └── Message items (lazy-rendered list)
├── Pills (action buttons)
├── Textarea (input)
├── Status bar
├── Dialog overlay stack
└── Completions popup

UI State Machine

uiOnboardinguiInitializeuiLandinguiChat

Pub/Sub Integration

UI subscribes to domain events:

  • pubsub.Event[session.Session] - Session updates
  • pubsub.Event[message.Message] - Chat messages
  • pubsub.Event[app.LSPEvent] - Language server status
  • pubsub.Event[mcp.Event] - MCP client changes
  • pubsub.Event[permission.PermissionRequest] - Permission dialogs

UI Files (17 in model/)

ui.go, chat.go, clipboard.go, filter.go, header.go, history.go, keys.go, landing.go, lsp.go, mcp.go, onboarding.go, pills.go, session.go, sidebar.go, status.go, plus clipboard variants


9. Sub-Agent Architecture

Two-Level Agent Hierarchy

Coder Agent (primary)
├── Full tool access (all 20+ tools)
├── Can invoke agent tool
└── Spawns:
    └── Task Agent (sub-agent)
        ├── Read-only tools only
        ├── Isolated session
        └── Cannot spawn further sub-agents

Implementation (agent/agent_tool.go)

type AgentParams struct {
    Prompt string `json:"prompt" description:"The task for the agent to perform"`
}
  • Uses fantasy.NewParallelAgentTool() for concurrent sub-agent calls
  • Each invocation gets a unique session (deterministic ID from parent message + tool call ID)
  • Task agents use task.md.tpl system prompt (vs. coder.md.tpl for main agent)
  • Cost aggregation: sub-agent costs roll up to parent session
  • Maximum depth: 2 (Coder → Task; Task agents cannot spawn sub-agents)

What Is NOT Present

  • Team coordination across multiple top-level agents
  • Shared task queues or work distribution
  • Agent-to-agent messaging
  • Multi-user collaboration
  • Persistent multi-agent orchestration

10. MCP Integration (agent/tools/mcp/)

Transport Types

createTransport() supports:
├── stdio  - Executes local process, pipes communication
├── http   - Standard HTTP client with custom headers
└── sse    - Server-Sent Events over HTTP

Uses github.com/modelcontextprotocol/go-sdk (v1.2.0).

Configuration

{
  "mcp": {
    "server-name": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem"],
      "env": { "KEY": "value" }
    }
  }
}

MCP Tool Wrapping

MCP tools are wrapped as fantasy.AgentTool instances. GetMCPTools() aggregates all tools from all connected servers. Each tool's Run() method: extracts session context → requests permission → calls mcp.RunTool() → handles text/image/media responses.

MCP Prompts

MCP servers expose prompts loaded as custom commands in the slash command system via LoadMCPPrompts().

OAuth Support

OAuth 2.0 for remote MCP servers (RFC standards, PKCE, Dynamic Client Registration).


11. Client/Server Split

Crush does NOT have a client/server split. Single-process application.

  • TUI mode (default): full Bubble Tea TUI
  • Non-interactive mode: crush run [prompt...] - headless, output to stdout
  • Open Issue #990 requesting "Agent Client Protocol" for IDE integration (not implemented)

12. Data Layer (internal/db/)

db/
├── connect.go / connect_modernc.go / connect_ncruces.go  # SQLite drivers
├── db.go              # Database service
├── embed.go           # Embedded migrations
├── models.go          # sqlc generated models
├── querier.go         # sqlc generated interface
├── files.sql.go / messages.sql.go / sessions.sql.go / stats.sql.go
├── migrations/        # Schema migration files
└── sql/               # Raw SQL query definitions

Two SQLite drivers: ncruces/go-sqlite3 (primary, pure Go) and modernc.org/sqlite (alternative).


13. Extensibility

Current Mechanisms

  1. MCP Servers - Primary extension point for custom tools
  2. Custom Commands - Markdown files in ~/.config/crush/commands/ or .crush/commands/
  3. Agent Skills - SKILL.md files (agentskills.io standard), YAML frontmatter + markdown
  4. Custom Providers - Any OpenAI/Anthropic-compatible API
  5. LSP Servers - Custom language server configurations
  6. .crushignore - File exclusion patterns
  7. Tool Disabling - "tools": { "disabled": ["bash", "mcp-server:tool-name"] }

Proposed Plugin System (Issue #2038)

  • Caddy-style Go module plugins, compiled at build-time via xcrush tool
  • Direct access to Go type system, negligible runtime overhead
  • Future plans for gRPC and WASM adapters for non-Go languages
  • Currently "very new and very unstable"

14. Build & Distribution

Taskfile.yaml

  • build - compile with ldflags + CGO_ENABLED=0
  • test - go test -race -failfast ./...
  • lint - golangci-lint v2, fmt - gofumpt
  • dev - run with profiling (pprof on :6060)
  • deps - update Fantasy + Catwalk
  • release - signed git tags, test:record - VCR cassettes

GoReleaser

  • Platforms: Linux, Darwin, Windows, FreeBSD, OpenBSD, NetBSD, Android
  • Architectures: amd64, arm64, 386, arm (armv7)
  • Package managers: Homebrew, Scoop, AUR, NPM, NFPM (APK/DEB/RPM/ArchLinux/Termux), WinGet, Nix (NUR), Fury.io
  • Artifacts: shell completions, man pages, SBOM, cosign signatures

15. License

FSL-1.1-MIT - Functional Source License. Proprietary for 2 years, then converts to MIT. This means you can read/study the code but cannot commercially compete with it until the license converts. Important consideration for forking.


16. What's Missing: Multi-Agent / Team Coordination

FeatureCrush Status
Client/Server splitNO - monolithic
Agent teams/swarmsNO - single coordinator, single agent
Inter-agent messagingNO
Shared task listsNO (has todos.go but single-agent)
Plan approval workflowNO
Plugin/hook systemNO - MCP only for external tools
Custom compiled toolsNO - tools are compiled in

Existing Foundation That Could Support It:

  • Coordinator pattern — manages agent lifecycle, could extend for multiple agents
  • Permission service with pub/sub — extensible for concurrent permission handling
  • Event system — generic typed broker for cross-component communication
  • Session management — already scoped per session, could support per-agent sessions
  • Fantasy library — LLM abstraction with concurrent-safe agent builder
  • Bubble Tea v2 — composable model architecture for team dashboards
  • Shell emulation (mvdan.cc/sh) — persists state across commands per Shell instance

Sources