Crush Architecture Deep Dive
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
| Package | Purpose |
|---|---|
charm.land/fantasy | LLM provider abstraction ("one API, many providers") |
charm.land/catwalk | Community-maintained model registry with auto-updating metadata |
charm.land/x/vcr | VCR test recording/replay |
charm.land/bubbletea/v2 | TUI framework (v2, Elm architecture) |
charm.land/bubbles/v2 | TUI components (v2) |
charm.land/lipgloss/v2 | Terminal styling (v2) |
charm.land/glamour/v2 | Markdown rendering |
charm.land/log/v2 | Structured logging |
charmbracelet/fang | CLI configuration |
External Dependencies
| Package | Purpose |
|---|---|
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-sqlite3 | Pure-Go SQLite (primary) |
modernc.org/sqlite | Alternative SQLite driver |
spf13/cobra | CLI framework |
mvdan.cc/sh/v3 | POSIX shell interpreter/emulation |
JohannesKaufmann/html-to-markdown | HTML conversion |
PuerkitoBio/goquery | HTML parsing |
alecthomas/chroma/v2 | Code highlighting |
sourcegraph/jsonrpc2 | LSP 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 asyncRun: Refreshes models, merges provider options (3-layer: catwalk defaults → provider config → model config), handles OAuth token refresh on 401, executes agentbuildAgent: Constructs SessionAgent with large/small models, system prompt, toolsbuildTools: Assembles tools (bash, edit, fetch, grep, LSP, MCP) based on agent configbuildProvider: 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
- User submits prompt to Coordinator
- Coordinator queues prompt in per-session FIFO queue (one active request per session)
- SessionAgent creates user message in SQLite, broadcasts event via pubsub
- SessionAgent calls
fantasy.Agent.Stream()with callbacks - Streaming callbacks persist text deltas, tool calls, tool results as they arrive
- Tools execute synchronously within the streaming loop
- Stream completes, message finalized in database
- 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)
| Category | Tools | Source Files |
|---|---|---|
| Shell | bash | bash.go |
| File Ops | view, edit, multi_edit, write | view.go, edit.go, multiedit.go, write.go |
| Search | ls, glob, grep | ls.go, glob.go, grep.go |
| LSP | diagnostics, references, lsp_restart | diagnostics.go, references.go, lsp_restart.go |
| MCP | list_mcp_resources, read_mcp_resource, MCP tools | list_mcp_resources.go, read_mcp_resource.go, mcp-tools.go |
| Network | fetch, agentic_fetch, download, web_fetch, web_search | corresponding .go files |
| Code Search | sourcegraph | sourcegraph.go |
| Agent | agent (sub-agent spawning) | agent_tool.go |
| Task Mgmt | todos, job_kill, job_output | todos.go, job_kill.go, job_output.go |
| Safety | safe (read-only tool set) | safe.go |
Tool Registration and Filtering
- Constructed with injected dependencies (permissions, LSP manager, history, etc.)
- Filtered by agent config's
AllowedToolsslice - MCP tools filtered via
AllowedMCPmap (server name -> permitted tool names) - Sorted alphabetically for consistency
- 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/messageSupportsImagesContextKey/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
- Tool calls
Request()with operation details - Early exits: skip=true, allowlist match, session auto-approved
- Cache check: session history for identical prior approvals
- User prompt: pub/sub → TUI dialog
- Blocking wait: channel-based response
- 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
uiOnboarding → uiInitialize → uiLanding → uiChat
Pub/Sub Integration
UI subscribes to domain events:
pubsub.Event[session.Session]- Session updatespubsub.Event[message.Message]- Chat messagespubsub.Event[app.LSPEvent]- Language server statuspubsub.Event[mcp.Event]- MCP client changespubsub.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.tplsystem prompt (vs.coder.md.tplfor 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
- MCP Servers - Primary extension point for custom tools
- Custom Commands - Markdown files in
~/.config/crush/commands/or.crush/commands/ - Agent Skills -
SKILL.mdfiles (agentskills.io standard), YAML frontmatter + markdown - Custom Providers - Any OpenAI/Anthropic-compatible API
- LSP Servers - Custom language server configurations
.crushignore- File exclusion patterns- Tool Disabling -
"tools": { "disabled": ["bash", "mcp-server:tool-name"] }
Proposed Plugin System (Issue #2038)
- Caddy-style Go module plugins, compiled at build-time via
xcrushtool - 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=0test-go test -race -failfast ./...lint- golangci-lint v2,fmt- gofumptdev- run with profiling (pprof on :6060)deps- update Fantasy + Catwalkrelease- 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
| Feature | Crush Status |
|---|---|
| Client/Server split | NO - monolithic |
| Agent teams/swarms | NO - single coordinator, single agent |
| Inter-agent messaging | NO |
| Shared task lists | NO (has todos.go but single-agent) |
| Plan approval workflow | NO |
| Plugin/hook system | NO - MCP only for external tools |
| Custom compiled tools | NO - 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