# GitButler Project Configuration Storage: Architecture and Implementation Guide

> Explore GitButler project configuration storage. Learn how GitButler manages JSON and TOML files for global and per-project settings, plus live watching and automatic migrations.

- Repository: [GitButler/gitbutler](https://github.com/gitbutlerapp/gitbutler)
- Tags: architecture
- Published: 2026-02-16

---

**GitButler stores runtime configuration in platform-specific directories using JSON files for global settings and TOML files for per-project virtual branch metadata, with live file watching and automatic migration from legacy Tauri stores.**

GitButler, the open-source Git client built in Rust, implements a sophisticated **project configuration storage** system that balances user customization with reliable defaults. This architecture separates global application preferences from per-project virtual branch state, ensuring that settings persist correctly across platforms while remaining responsive to external changes.

## Configuration Storage Architecture

GitButler organizes configuration into two distinct layers: global application settings stored in the user's configuration directory, and project-specific metadata embedded within each repository's `.git` folder.

### Global Settings Directory

The application determines the configuration root using `but_path::app_config_dir()` defined in [`crates/but-path/src/lib.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-path/src/lib.rs). This helper returns platform-appropriate paths:

- **Linux**: `~/.config/gitbutler`
- **macOS**: `~/Library/Application Support/gitbutler`
- **Windows**: `%APPDATA%/gitbutler`

Within this directory, [`settings.json`](https://github.com/gitbutlerapp/gitbutler/blob/main/settings.json) stores user-editable preferences including telemetry options, feature flags, and UI state.

### Per-Project Metadata Location

Each Git repository managed by GitButler contains a `.git/gitbutler/` directory. The file [`virtual_branches.toml`](https://github.com/gitbutlerapp/gitbutler/blob/main/virtual_branches.toml) within this directory stores the virtual branch topology, stack identifiers, and workspace state specific to that project. This approach ensures that GitButler's metadata travels with the repository and remains isolated between projects.

## Configuration Files and Formats

GitButler utilizes three primary configuration artifacts, each serving a distinct purpose in the settings hierarchy.

### settings.json

Located at `app_config_dir()/settings.json`, this JSON file persists user customizations. It is created automatically when users modify defaults and is watched for external changes. The file follows the schema defined in `defaults.jsonc` but only contains keys that differ from built-in defaults.

### virtual_branches.toml

This TOML file, located at `<project>/.git/gitbutler/virtual_branches.toml`, manages per-project state. The `VirtualBranchesTomlMetadata` type in [`crates/but-meta/src/legacy.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-meta/src/legacy.rs) handles serialization and deserialization, providing methods like `from_path()` to load metadata and `stack()` to retrieve specific branch information.

### defaults.jsonc

The built-in defaults reside in `crates/but-settings/assets/defaults.jsonc`. This JSON-with-comments file defines the complete configuration schema and sensible defaults for all settings, including telemetry preferences and feature flags. The file is compiled into the binary using `include_str!()` in [`crates/but-settings/src/persistence.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-settings/src/persistence.rs), ensuring defaults are always available even if the configuration file is missing.

## Loading and Merging Strategy

GitButler implements a default-first configuration strategy that ensures the application remains functional even with missing or corrupted user settings.

### Default-First Approach

The loading process, implemented in [`crates/but-settings/src/persistence.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-settings/src/persistence.rs), follows this sequence:

1. **Load defaults**: Parse the embedded `defaults.jsonc` into a base `serde_json::Value`
2. **Merge customizations**: Read [`settings.json`](https://github.com/gitbutlerapp/gitbutler/blob/main/settings.json) from disk and recursively merge it into the defaults using `merge_json_value`, where user values overwrite defaults but omitted keys preserve their default values
3. **Apply runtime overrides**: Optionally merge additional runtime configuration provided by the caller

This approach ensures that new settings introduced in updates automatically adopt their default values without requiring user intervention.

### Migration from Legacy Tauri Store

For users upgrading from older Tauri-based builds, the `maybe_migrate_legacy_settings` function in [`crates/but-settings/src/persistence.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-settings/src/persistence.rs) automatically migrates existing settings. The function:

1. Detects the legacy Tauri configuration location
2. Copies valid settings into the new [`settings.json`](https://github.com/gitbutlerapp/gitbutler/blob/main/settings.json) format
3. Sets `telemetry.migratedFromLegacy` to `true` to prevent repeated migration attempts

This ensures seamless upgrades without manual configuration transfer.

## Live Configuration Updates

GitButler provides a thread-safe, live view of configuration that automatically synchronizes with disk changes and enforces safe mutation patterns.

### AppSettingsWithDiskSync Implementation

The `AppSettingsWithDiskSync` type in [`crates/but-settings/src/watch.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-settings/src/watch.rs) provides the primary interface for runtime configuration access. It maintains an in-memory snapshot wrapped in `Arc<RwLock<AppSettings>>`, allowing concurrent reads while ensuring exclusive access for writes.

Key methods include:

- `new_with_customization(config_dir, extra)` – Initializes the sync instance, loading and merging configuration from disk
- `get()` – Acquires a read lock and returns a clone of the current settings
- `get_mut_enforce_save()` – Returns a mutable guard that **must** call `.save()` before being dropped (enforced via `Drop` implementation)

### File Watching Mechanism

The `watch_in_background` method spawns an asynchronous task that monitors [`settings.json`](https://github.com/gitbutlerapp/gitbutler/blob/main/settings.json) for external modifications using the `notify` crate. When the file changes:

1. The watcher detects the filesystem event
2. The configuration is reloaded from disk
3. The in-memory snapshot is updated via the `Arc<RwLock>`
4. A user-provided callback receives the updated `AppSettings` instance

This mechanism handles platform-specific quirks, such as re-watching files on Linux when editors use atomic save patterns (write-to-temp-then-rename).

## Working with Configuration in Code

The following examples demonstrate practical usage patterns for interacting with GitButler's configuration storage system.

### Load Settings and Read a Feature Flag

```rust
use but_settings::AppSettingsWithDiskSync;
use but_path::app_config_dir;

fn check_workspace_v3() -> anyhow::Result<()> {
    // Determine platform-specific config directory
    let config_dir = app_config_dir()?;
    
    // Initialize live settings view
    let settings = AppSettingsWithDiskSync::new_with_customization(config_dir, None)?;
    
    // Acquire read-only snapshot
    let snapshot = settings.get()?;
    
    // Access feature flag
    if snapshot.feature_flags.ws3 {
        println!("V3 workspace APIs are enabled");
    }
    
    Ok(())
}

```

*Relevant source:* `AppSettingsWithDiskSync::new_with_customization` in [`crates/but-settings/src/watch.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-settings/src/watch.rs).

### Mutate a Setting and Persist to Disk

```rust
use but_settings::AppSettingsWithDiskSync;
use but_path::app_config_dir;

fn enable_undo_feature() -> anyhow::Result<()> {
    let config_dir = app_config_dir()?;
    let sync = AppSettingsWithDiskSync::new_with_customization(config_dir, None)?;
    
    // Obtain mutable access with enforced save semantics
    let mut mutable = sync.get_mut_enforce_save()?;
    mutable.feature_flags.undo = true;
    
    // Persistence is mandatory before the guard drops
    mutable.save()?;
    
    Ok(())
}

```

*Relevant source:* `AppSettingsEnforceSaveToDisk` enforcement logic in [`crates/but-settings/src/watch.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-settings/src/watch.rs).

### Watch for External Configuration Changes

```rust
use but_settings::{AppSettingsWithDiskSync, AppSettings};
use but_path::app_config_dir;
use std::sync::Arc;
use tokio::sync::Mutex;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let config_dir = app_config_dir()?;
    let mut sync = AppSettingsWithDiskSync::new_with_customization(config_dir, None)?;
    
    // Shared state for reactive updates
    let latest = Arc::new(Mutex::new(sync.get()?.clone()));

    // Spawn background watcher
    sync.watch_in_background(move |updated: AppSettings| {
        let latest = Arc::clone(&latest);
        async move {
            let mut guard = latest.lock().await;
            *guard = updated;
            println!("Configuration reloaded from disk");
            Ok(())
        }
    })?;

    // Application continues running...
    Ok(())
}

```

*Relevant source:* `watch_in_background` implementation using the `notify` crate in [`crates/but-settings/src/watch.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-settings/src/watch.rs).

### Load Per-Project Virtual Branch Metadata

```rust
use but_meta::VirtualBranchesTomlMetadata;
use std::path::PathBuf;

fn inspect_project_metadata(project_root: PathBuf) -> anyhow::Result<()> {
    let meta_path = project_root.join(".git/gitbutler/virtual_branches.toml");
    
    // Load TOML metadata for virtual branches
    let meta = VirtualBranchesTomlMetadata::from_path(meta_path)?;
    
    // Iterate through stacks
    for stack in meta.stacks()? {
        println!("Stack {} - In workspace: {}", 
                 stack.id, 
                 stack.in_workspace);
    }
    
    Ok(())
}

```

*Relevant source:* `VirtualBranchesTomlMetadata::from_path` in [`crates/but-meta/src/legacy.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-meta/src/legacy.rs).

## Key Source Files and Implementation Details

The **GitButler project configuration storage** system spans several crates with distinct responsibilities:

| Path | Responsibility | Direct Link |
|------|---------------|-------------|
| [`crates/but-path/src/lib.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-path/src/lib.rs) | Cross-platform directory resolution (`app_config_dir`, `app_data_dir`) | <https://github.com/gitbutlerapp/gitbutler/blob/master/crates/but-path/src/lib.rs> |
| [`crates/but-settings/src/persistence.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-settings/src/persistence.rs) | Default loading, JSON merging, legacy migration | <https://github.com/gitbutlerapp/gitbutler/blob/master/crates/but-settings/src/persistence.rs> |
| [`crates/but-settings/src/watch.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-settings/src/watch.rs) | Live settings sync, file watching, enforced save semantics | <https://github.com/gitbutlerapp/gitbutler/blob/master/crates/but-settings/src/watch.rs> |
| `crates/but-settings/assets/defaults.jsonc` | Built-in default configuration schema | <https://github.com/gitbutlerapp/gitbutler/blob/master/crates/but-settings/assets/defaults.jsonc> |
| [`crates/but-meta/src/legacy.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-meta/src/legacy.rs) | Per-project virtual branch metadata handling | <https://github.com/gitbutlerapp/gitbutler/blob/master/crates/but-meta/src/legacy.rs> |

These components collectively provide a robust, default-first configuration system that supports live reloading, thread-safe access, and seamless upgrades from legacy installations.

## Summary

- **GitButler project configuration storage** uses platform-specific directories (e.g., `~/.config/gitbutler` on Linux) for global settings and `.git/gitbutler/` for per-project metadata.
- **Default-first loading** merges built-in `defaults.jsonc` with user customizations in [`settings.json`](https://github.com/gitbutlerapp/gitbutler/blob/main/settings.json), ensuring new features automatically adopt sensible defaults.
- **Live synchronization** via `AppSettingsWithDiskSync` provides thread-safe access with automatic file watching and background reloading when users edit configuration externally.
- **Enforced persistence** requires explicit `.save()` calls when mutating settings, preventing accidental data loss through the `get_mut_enforce_save()` API.
- **Legacy migration** automatically imports settings from older Tauri-based installations, setting `telemetry.migratedFromLegacy` to prevent duplicate migrations.
- **Virtual branch metadata** resides in [`virtual_branches.toml`](https://github.com/gitbutlerapp/gitbutler/blob/main/virtual_branches.toml) within each repository, managed by `VirtualBranchesTomlMetadata` in the `but-meta` crate.

## Frequently Asked Questions

### Where does GitButler store its configuration files?

GitButler stores global configuration in platform-specific application directories: `~/.config/gitbutler` on Linux, `~/Library/Application Support/gitbutler` on macOS, and `%APPDATA%/gitbutler` on Windows. The primary file is [`settings.json`](https://github.com/gitbutlerapp/gitbutler/blob/main/settings.json), which contains user preferences and feature flags. Per-project metadata, including virtual branch state, is stored in [`.git/gitbutler/virtual_branches.toml`](https://github.com/gitbutlerapp/gitbutler/blob/main/.git/gitbutler/virtual_branches.toml) within each managed repository.

### How does GitButler handle configuration changes while the app is running?

The application uses the `AppSettingsWithDiskSync` type from [`crates/but-settings/src/watch.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-settings/src/watch.rs) to maintain a live, thread-safe view of configuration. This implementation spawns a background file watcher using the `notify` crate that monitors [`settings.json`](https://github.com/gitbutlerapp/gitbutler/blob/main/settings.json) for external modifications. When changes are detected, the system automatically reloads the file, merges it with built-in defaults, and updates the in-memory snapshot, ensuring the application always reflects the current configuration without requiring a restart.

### What happens to my settings when upgrading from an older version of GitButler?

GitButler automatically migrates settings from legacy Tauri-based installations through the `maybe_migrate_legacy_settings` function in [`crates/but-settings/src/persistence.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-settings/src/persistence.rs). During initialization, the system checks for existing Tauri configuration files, copies valid settings into the new [`settings.json`](https://github.com/gitbutlerapp/gitbutler/blob/main/settings.json) format, and sets the `telemetry.migratedFromLegacy` flag to `true` to prevent repeated migrations. This ensures seamless upgrades without manual configuration transfer or loss of user preferences.

### How do I modify GitButler settings programmatically?

To mutate settings safely, use the `get_mut_enforce_save()` method on `AppSettingsWithDiskSync`, which returns a guard that enforces persistence through Rust's `Drop` trait. After modifying the desired fields (such as `feature_flags.undo` or `telemetry.appMetricsEnabled`), you must explicitly call `.save()` on the guard before it goes out of scope, otherwise the application will panic to prevent accidental data loss. For read-only access, use the `get()` method, which returns a clone of the current settings snapshot without locking requirements.