GitButler Code Generation Architecture
How GitButler's Claude AI code generation functionality works, from CLI subprocess execution to real-time UI updates
Based on my exploration of the codebase, here's a high-level overview of how GitButler's Claude AI/Code Generation functionality works:
Architecture Overview
Not Direct API Calls - CLI Subprocess Execution
GitButler does NOT make direct HTTP calls to the Anthropic/Claude API. Instead, it:
- Spawns the Claude Code CLI as a child process
- Communicates with it via stdin/stdout pipes
- Streams JSON events from the CLI's stdout
- The CLI itself handles the actual Claude API communication
High-Level Flow
User types message in GitButler UI
↓
Frontend (Svelte) calls Tauri command: claude_send_message
↓
Rust Backend (but-api layer) receives request
↓
but-claude::bridge.rs spawns Claude CLI subprocess
↓
Claude CLI streams newline-delimited JSON to stdout
↓
Backend spawns thread to read stdout line-by-line
↓
Each JSON line is parsed and:
- Saved to SQLite database (but-db)
- Broadcast via WebSocket to frontend
↓
Frontend listens to: project://{projectId}/claude/{stackId}/message_recieved
↓
UI updates in real-time with Claude's responsesThe Claude CLI is invoked with:
claude --model sonnet --session-id <uuid> \
--output-format stream-json \
--permission-prompt-tool mcp__but-security__approval_prompt \
-p "user message with context"Key Components
1. Process Management
Location: crates/but-claude/src/bridge.rs:145-539
- Spawns
claudeCLI astokio::process::Child - One active session per stack (virtual branch)
- Tracks running sessions in
HashMap<StackId, Arc<Claude>> - Handles graceful shutdown (SIGINT on Unix) and forced kill
2. Session Persistence
Location: crates/but-claude/src/db.rs
Sessions are stored in SQLite with stable UUIDs. Each session tracks:
- All message history (User, Claude, System, GitButler events)
- Approved/denied permissions
- Session IDs (Claude may create multiple session IDs when resuming)
Messages have different sources:
- User: User input
- Claude: Raw JSON from CLI stdout
- System: GitButler system messages (exit codes, errors)
- GitButler: Domain events (e.g., commit created)
3. Context Injection
Location: crates/but-claude/src/bridge.rs:541-633
A custom system prompt is injected into every Claude session:
- Tells Claude about GitButler's virtual branches
- Prohibits certain git commands (
commit,checkout,rebase, etc.) - Instructs to use
butCLI instead for modifications - Provides branch info: target branch, stack branches, assigned files with line ranges
Example system prompt snippet:
CRITICAL: You are working on a project managed by GitButler.
PROHIBITED Git Commands:
- git commit (use `but commit` instead)
- git checkout
- git rebase
The following branches are part of the current stack:
- my-feature-branch (current working branch)
Uncommitted files assigned to this stack:
- src/main.rs (lines: 10-25, 50-60)4. Permission System
GitButler provides a custom MCP server: but-security. When Claude tries to execute tools (Bash, file writes, etc.), it requests permission:
- Permission requests are stored in database
- Broadcast to UI for user approval
- Can be scoped: once, session, project, or globally
- User approval/denial is sent back to Claude CLI via the MCP server
5. Streaming Architecture
Location: crates/but-claude/src/bridge.rs:849-903
fn spawn_response_streaming(...) {
// Blocking thread reads from pipe
std::thread::spawn(move || {
let reader = BufReader::new(read_stdout);
for line in reader.lines() {
// Each line is a JSON event from Claude
tx.send(line);
}
});
// Async task processes events
while let Some(line) = rx.recv().await {
let parsed_event: serde_json::Value = serde_json::from_str(&line);
// Save to database
db::save_new_message(ctx, session_id, ClaudeOutput { data: parsed_event });
// Broadcast to frontend
broadcaster.send(FrontendEvent {
name: "project://xxx/claude/yyy/message_recieved",
payload: parsed_event
});
}
}Authentication
For code generation sessions:
- GitButler does NOT handle Claude authentication
- The Claude CLI binary itself handles auth (likely via
~/.claudeconfig) - GitButler just spawns it with the configured executable path:
crates/gitbutler-tauri/src/claude.rs:20-50
Note: GitButler also has a separate AI service (apps/desktop/src/lib/ai/service.ts) for simpler tasks like commit message generation and PR descriptions. That service CAN make direct API calls to Anthropic/OpenAI, and supports two modes:
- Bring Your Own Key - direct API calls with user's key
- Butler API - proxy through GitButler cloud
Summary
- No direct API calls from GitButler to Claude for code sessions
- Spawns Claude CLI subprocess with specific arguments
- Streams JSON output from CLI's stdout
- Persists sessions in SQLite
- Broadcasts events via WebSocket to UI
- Injects GitButler context via system prompts and appended info
- Manages permissions via custom MCP server
The architecture is essentially a wrapper around the Claude Code CLI that provides:
- UI integration
- Session/message persistence
- Permission management
- GitButler-specific context injection