How CodexBar's Settings Persistence System Works with Configuration Files

CodexBar persists user preferences through a dual-layer architecture that stores simple scalar values in UserDefaults while maintaining richer provider configurations in a JSON file within the app-group container.

The open-source macOS menu bar application steipete/CodexBar implements a robust persistence layer centered around the SettingsStore class. This system ensures that user preferences survive app restarts, handles legacy setting migrations automatically, and maintains thread-safe access to configuration data across the main app and its extensions.

The Dual-Storage Architecture

CodexBar divides persistent data into two distinct storage mechanisms based on complexity and usage patterns. This separation allows frequently accessed simple flags to remain in memory-mapped preferences while complex provider definitions reside in version-controlled JSON.

UserDefaults for Scalar Preferences

Simple configuration values—including refresh intervals, launch-at-login flags, and UI metric choices—are stored in UserDefaults. According to the source code in SettingsStore.swift, the initializer reads every key using userDefaults.object(forKey:) or string(forKey:) and populates a SettingsDefaultsState struct. The complete loading routine lives in SettingsStore.loadDefaultsState(userDefaults:) between lines 73 and 124.

The system supports both standard defaults and an optional shared app-group container accessed via AppGroupSupport.sharedDefaults(). This allows the main application and any extensions to share preference data seamlessly.

JSON Configuration for Provider Definitions

Richer configuration data—specifically per-provider definitions including endpoint URLs, token overrides, and cookie sources—is managed by CodexBarConfigStore. This class maintains a JSON file located at ~/Library/Group Containers/<team-id>/Library/Application Support/CodexBar/config.json within the app-group container.

During initialization, SettingsStore creates a CodexBarConfigStore instance and calls load() to retrieve a CodexBarConfig object. When provider settings change, the store schedules an asynchronous configPersistTask that writes updates back to disk via configStore.save(_:), ensuring disk I/O occurs off the main thread.

Initialization and Migration Flow

The startup sequence in SettingsStore.swift handles complex migration scenarios to ensure seamless updates between app versions.

App-Group Migration

If the app is running outside of test mode, SettingsStore fires scheduleAppGroupMigration asynchronously (lines 65-70). This routine copies legacy data from the standard UserDefaults database into the shared app-group defaults, ensuring users retain their settings when upgrading to versions that support app extensions.

Legacy Defaults Bridging

The method shouldBridgeSharedDefaults(for:) (lines 15-20) determines whether a given UserDefaults instance should fall back to shared defaults. This bridging allows the same UI code to read and write values even when unit tests replace the defaults with a sandboxed instance.

Configuration Loading and Key Migration

After migration, the system performs several housekeeping tasks:

  1. Legacy key migration – Older keys like openAIWebAccess are automatically migrated to newer equivalents such as openAIWebAccessEnabled immediately after defaults load (around line 78).
  2. Configuration loading – CodexBarConfigMigrator.loadOrMigrate(...) reads the JSON file or creates a fresh default config, returning a CodexBarConfig that becomes self.config (lines 100-105).
  3. State population – The SettingsDefaultsState struct is filled with values from UserDefaults, including derived flags like mergedMenuLastSelectedWasOverviewStorage.

Runtime Persistence Mechanisms

CodexBar employs different persistence strategies depending on which storage layer is being modified.

Synchronous UserDefaults Updates

When UI elements toggle simple preferences, updates are written immediately to UserDefaults. For example, setting defaultsState.launchAtLogin triggers a direct call to userDefaults.set(_:forKey:) during initialization (line 19). This synchronous approach ensures that preferences are instantly available to system services like launch-at-login helpers.

Asynchronous Config File Writes

Provider-specific changes require more care to prevent disk thrashing. The method setProviderEnabled(_:enabled:) mutates self.config and then calls persistConfig(), which schedules a Task to invoke configStore.save(self.config). This indirection guarantees that multiple rapid updates coalesce into a single disk write and that filesystem operations never block the UI thread.

You can optionally await the persistence task if immediate durability is required:

// Toggle a provider (queues async write)
settings.setProviderEnabled(provider: .zai,
                            metadata: ProviderDescriptorRegistry.metadata[.zai]!,
                            enabled: false)

// Wait for the write to complete
await settings.configPersistTask

Test Mode Special Handling

When running unit tests, SettingsStore detects isRunningTests and modifies its behavior to ensure test isolation:

  • Temporary defaults – Forces creation of a temporary UserDefaults suite instead of the real user database.
  • Auto-seeding – Populates missing default keys automatically so tests assume a fully seeded environment.
  • Keychain gating – Disables keychain access unless explicitly enabled via KeychainAccessGate.isDisabled.

This logic is embedded directly within the SettingsStore class, making the persistence layer a single source of truth for both production and test environments.

Summary

  • Dual-layer storage separates simple UserDefaults scalars from complex JSON provider configurations.
  • Automatic migration handles legacy key updates and app-group container transitions during startup.
  • Asynchronous persistence prevents UI blocking when writing provider configuration changes to disk.
  • Test isolation ensures unit tests run against temporary storage without affecting user data.
  • Centralized coordination through SettingsStore.swift provides a single API for all persistence operations.

Frequently Asked Questions

Where does CodexBar store its configuration files?

CodexBar stores simple preferences in UserDefaults.standard (or a shared app-group instance) and complex provider configurations in a JSON file located at ~/Library/Group Containers/<team-id>/Library/Application Support/CodexBar/config.json. This path resides within the app-group container, allowing data sharing between the main app and extensions.

How does CodexBar handle settings from older app versions?

The SettingsStore class automatically migrates legacy settings during initialization. It schedules an AppGroupMigration to move data from standard defaults to shared defaults, and it migrates renamed keys (such as openAIWebAccess to openAIWebAccessEnabled) immediately after loading. These migrations run asynchronously to prevent startup delays.

What happens when I toggle a provider in the CodexBar UI?

When you toggle a provider, the setProviderEnabled(_:enabled:) method immediately updates the in-memory CodexBarConfig object and schedules an asynchronous configPersistTask. This task writes the updated configuration to the JSON file via configStore.save(_:), ensuring the disk write happens off the main thread while the UI remains responsive.

Can I force CodexBar to save settings immediately?

For UserDefaults-backed settings, changes are written synchronously and are immediate. For provider configuration changes, you can await the configPersistTask property on SettingsStore after making changes. This optional await ensures the JSON file write completes before your code continues execution.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →