How the DesktopCommanderMCP Configuration Manager Saves and Loads Settings Between Server Restarts

The DesktopCommanderMCP configuration manager persists settings by atomically writing JSON to ~/.claude-server-commander/config.json, using a serialized write chain to prevent corruption while providing both blocking and non-blocking update APIs.

The configuration manager in wonderwhy-er/DesktopCommanderMCP ensures that user preferences, telemetry flags, and onboarding state survive process restarts through a robust file-based persistence layer. Implemented as a singleton in src/config-manager.ts, it coordinates all disk I/O through a centralized queue that guarantees data integrity even during rapid successive updates. Understanding its dual-path persistence strategy—synchronous for critical changes and deferred for high-frequency metrics—enables reliable integration of custom configuration options.

Configuration File Location and Structure

All persistent data resides in a single JSON file defined in src/config.ts. The path CONFIG_FILE resolves to ~/.claude-server-commander/config.json using the user’s home directory as the base. This location remains constant across server restarts, ensuring the configuration manager always reads from and writes to the same file.

Initialization and Loading on Server Startup

When the server boots, the configuration manager executes the init() method (lines 69-102 in src/config-manager.ts). This routine performs three critical tasks:

  • Directory creation: Creates ~/.claude-server-commander/ if it does not exist
  • File loading: Parses existing config.json or falls back to getDefaultConfig() if absent
  • First-run detection: Sets the internal _isFirstRun flag (lines 53-55) to true only when the file must be created, enabling onboarding UI flows

The loadConfig() helper reads the file synchronously during initialization, populating an in-memory configuration object that serves as the working state for the server’s lifetime.

Updating Configuration Values

The manager exposes two distinct persistence paths to accommodate different latency requirements.

Blocking Updates with setValue()

The setValue(key, value) method (lines 52-82) updates the in-memory configuration immediately, normalizes telemetry flags, and awaits saveConfig() before returning. This guarantees that the file system reflects the change before the caller continues execution, making it ideal for critical settings like shell paths or feature toggles.

await configManager.setValue('defaultShell', '/usr/bin/zsh');
// File is guaranteed written when the promise resolves

Non-Blocking Updates with setValueNonBlocking()

For high-frequency updates—such as usage statistics or session metrics—the setValueNonBlocking(key, value) method (lines 85-98) updates memory state instantly but defers disk I/O via scheduleSave(). This prevents thread-pool saturation and keeps response times low while still ensuring eventual consistency.

await configManager.setValueNonBlocking('telemetryEnabled', false);
// Returns immediately; write happens in background

Write Serialization and Conflict Prevention

To prevent concurrent writes from corrupting the JSON file, the manager maintains a writeChain promise queue. Every call to saveConfig() appends its write operation to this chain, ensuring each file operation completes before the next begins. Additionally, scheduleSave() (lines 72-81) coalesces rapid successive updates into a single disk write, resetting the saveScheduled sentinel only after the pending operation finishes.

Reading Configuration Values

Accessors guarantee that init() has completed before returning data:

  • getConfig() (lines 36-40): Returns a shallow copy of the entire configuration object
  • getValue(key) (lines 44-48): Retrieves a single property value
  • getOrCreateClientId() (lines 27-35): Lazily generates a UUID, persists it via setValue(), and returns it for analytics or A/B testing
const cfg = await configManager.getConfig();      // Full object
const shell = await configManager.getValue('defaultShell'); // Single value
const clientId = await configManager.getOrCreateClientId(); // Persistent UUID

Practical Code Examples

Resetting to factory defaults:

await configManager.resetConfig();   // Overwrites config.json with defaults from getDefaultConfig()

Checking first-run status:

if (configManager.isFirstRun()) {
  console.log('Showing onboarding flow...');
}

Summary

  • Persistent storage: Settings survive restarts via JSON at ~/.claude-server-commander/config.json
  • Initialization: init() loads existing config or creates defaults on server startup, tracking first-run state
  • Update patterns: setValue() for immediate durability; setValueNonBlocking() for background persistence
  • Data integrity: writeChain serializes file access to prevent corruption during concurrent updates
  • Safe reads: getConfig() and getValue() provide read-only views after initialization completes

Frequently Asked Questions

Where is the configuration file stored on disk?

The configuration manager writes to ~/.claude-server-commander/config.json, as defined in src/config.ts. This path is resolved relative to the user’s home directory, ensuring consistent access across server restarts regardless of the working directory from which the server is launched.

What is the difference between setValue and setValueNonBlocking?

setValue() performs a synchronous file write and awaits completion before returning, guaranteeing durability for critical settings. setValueNonBlocking() updates memory immediately but queues the disk write via scheduleSave(), making it suitable for high-frequency updates like telemetry counters that would otherwise saturate the libuv thread pool.

How does the configuration manager prevent file corruption during concurrent writes?

The manager implements a writeChain promise queue in src/config-manager.ts. Each save operation appends to this chain, ensuring that only one write executes at a time. This serialized access pattern prevents race conditions where overlapping writes could produce malformed JSON.

How can I detect if the server is running for the first time?

After initialization, check the isFirstRun() method (backed by the internal _isFirstRun flag set during init()). This flag is set to true only when the configuration file did not exist and had to be created, allowing the server to trigger onboarding flows or initial setup routines.

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 →