My App

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:

  1. Spawns the Claude Code CLI as a child process
  2. Communicates with it via stdin/stdout pipes
  3. Streams JSON events from the CLI's stdout
  4. 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 responses

The 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 claude CLI as tokio::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 but CLI 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 ~/.claude config)
  • 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:

  1. UI integration
  2. Session/message persistence
  3. Permission management
  4. GitButler-specific context injection

On this page