# How Config Persistence and Write Serialization Work in DesktopCommanderMCP

> Understand DesktopCommanderMCP config persistence and write serialization. Learn how atomic writes prevent file corruption during simultaneous setting saves.

- Repository: [Eduard Ruzga/DesktopCommanderMCP](https://github.com/wonderwhy-er/DesktopCommanderMCP)
- Tags: internals
- Published: 2026-07-22

---

**DesktopCommanderMCP guarantees atomic configuration writes by funneling all disk operations through a single promise chain, preventing file corruption when concurrent tool calls attempt to persist settings simultaneously.**

The `ConfigManager` class in DesktopCommanderMCP handles runtime settings by persisting them to a JSON file on disk. Understanding its config persistence and write serialization strategy is essential for developers integrating with the server or building similar Node.js tools that demand high-throughput, crash-safe configuration updates.

## Where Configuration Is Stored

DesktopCommanderMCP stores its settings in a JSON file located in the user's home directory under `.claude-server-commander`. According to the source code in [`src/config.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/config.ts) (lines 6-10), the absolute path is constructed at startup and exported as `CONFIG_FILE`.

On Linux and macOS, this resolves to `~/.claude-server-commander/config.json`; on Windows, it maps to the equivalent user-profile directory. This location remains constant for the process lifetime, ensuring predictable file access across all platform targets.

## The ConfigManager Singleton

All persistence logic is centralized in [`src/config-manager.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/config-manager.ts) within the `ConfigManager` class. This singleton is the sole authority that reads from and writes to the config file, maintaining an in-memory copy to serve tool calls without blocking on disk I/O.

### Initialization and Defaults

On first start, `ConfigManager.init()` (lines 69-101) performs three critical tasks: it creates the `.claude-server-commander` directory if missing, loads an existing [`config.json`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/config.json) if present, or falls back to `getDefaultConfig()` to generate a fresh configuration. The private flag `_isFirstRun` tracks whether the server just instantiated a new config file, allowing downstream logic to detect first-time setup scenarios.

## Write Serialization Mechanism

To prevent concurrent writes from interleaving and corrupting the JSON structure, DesktopCommanderMCP implements a strict write serialization protocol using a promise chain that acts as a FIFO queue.

### The Promise Chain Queue

At line 55 in [`src/config-manager.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/config-manager.ts), the class initializes `writeChain` as `Promise.resolve()`. This chain serves as the backbone for all write operations. Every subsequent persistence request appends to this chain, guaranteeing that only one `fs.writeFile` operation executes at any moment, regardless of how many asynchronous tool calls trigger updates simultaneously.

### Atomic Disk Writes

The `writeConfigToDisk()` method (lines 97-100) performs the actual file I/O. It serializes the entire in-memory config object and writes it atomically to `CONFIG_FILE`. Because this method is only ever invoked through the promise chain, physical disk contention is eliminated, and writes occur in strict order.

### Explicit and Scheduled Persistence

Two distinct paths trigger writes, optimized for different latency requirements:

- **`saveConfig()`** (lines 105-110): Creates a new promise that awaits the current `writeChain`, then invokes `writeConfigToDisk()`. The chain is updated to this new promise immediately, ensuring subsequent writes queue behind it. Even if a write throws an error, the chain continues to the next link, preventing deadlock.

- **`scheduleSave()`** (lines 122-130): Used for high-frequency updates. This method sets a `saveScheduled` flag to prevent duplicate scheduling, then appends a write to `writeChain`. When the previous write finishes, the most recent config snapshot is persisted. This coalesces rapid changes into single disk operations, reducing I/O overhead.

## API Methods for Configuration Updates

The `ConfigManager` exposes distinct persistence behaviors through its public API:

**`setValue(key, value)`** is fully asynchronous and blocking. It updates the in-memory config and awaits `saveConfig()`, ensuring the file is flushed to disk before returning. Use this when immediate persistence is required.

**`setValueNonBlocking(key, value)`** returns instantly after updating the in-memory config, then delegates the disk write to `scheduleSave()`. The caller proceeds without waiting for I/O, making this ideal for latency-sensitive tool calls that must remain responsive.

**`updateConfig(updates)`** merges multiple key-value changes from a single object and persists them with one atomic write via `saveConfig()`.

**`resetConfig()`** restores factory defaults and writes them synchronously (within an async context).

**`getValue(key)`** and **`getConfig()`** ensure initialization via `init()` before returning deep copies of the configuration, preventing external mutation of the singleton's internal state.

## Practical Code Examples

```typescript
import { configManager } from './config-manager.js';

// Blocking update: waits until config.json is flushed
await configManager.setValue('defaultShell', '/bin/zsh');

// Non-blocking update: returns immediately, writes in background
await configManager.setValueNonBlocking('fileWriteLineLimit', 50);

// Bulk update: persists multiple changes in one atomic write
await configManager.updateConfig({
  telemetryEnabled: false,
  allowedDirectories: ['/home/user/projects', '/opt/data']
});

```

## Why Serialization Matters

Without write serialization, rapid tool calls could trigger overlapping `fs.writeFile` operations. On slow or networked filesystems, this interleaving could fragment or corrupt the JSON structure. By enforcing a single active writer through the promise chain, DesktopCommanderMCP maintains file integrity even under heavy MCP server load.

Additionally, blocking the libuv threadpool with synchronous disk I/O would degrade server responsiveness. The `setValueNonBlocking()` method solves this by decoupling the API response from the physical write, improving throughput while the serialization layer guarantees eventual consistency.

## Summary

- DesktopCommanderMCP persists configuration to `~/.claude-server-commander/config.json` as defined in [`src/config.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/config.ts).
- The `ConfigManager` singleton in [`src/config-manager.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/config-manager.ts) controls all access via a `writeChain` promise queue initialized at line 55.
- **Write serialization** ensures only one disk operation runs at a time, preventing corruption during concurrent updates.
- Use **`setValue()`** for blocking, guaranteed persistence; use **`setValueNonBlocking()`** for high-frequency updates that queue via `scheduleSave()`.
- The `init()` method (lines 69-101) handles first-run detection and default configuration generation.

## Frequently Asked Questions

### How does DesktopCommanderMCP prevent config file corruption during concurrent writes?

DesktopCommanderMCP prevents corruption by serializing all writes through a single promise chain (`writeChain`). Every write operation, whether triggered by `saveConfig()` or `scheduleSave()`, appends to this chain in [`src/config-manager.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/config-manager.ts). This guarantees FIFO ordering and ensures that only one `fs.writeFile` call executes at a time, even if dozens of tool calls invoke `setValueNonBlocking()` simultaneously.

### What is the difference between blocking and non-blocking config updates?

**`setValue()`** is blocking: it awaits `saveConfig()`, which waits for the current `writeChain` to settle and flushes the file to disk before returning. **`setValueNonBlocking()`** is non-blocking: it updates the in-memory config instantly and returns control to the caller immediately, while `scheduleSave()` appends the physical write to the background queue. Use blocking for critical settings and non-blocking for high-frequency, latency-sensitive updates.

### How is the configuration initialized on first startup?

During `ConfigManager.init()` (lines 69-101), the code checks for the existence of the config directory and file. If missing, it creates the directory and writes a default configuration generated by `getDefaultConfig()`. The private `_isFirstRun` flag tracks this state, allowing the server to detect when it is running with fresh settings.

### What happens if a write operation fails?

The promise chain in `saveConfig()` (lines 105-110) is designed to be resilient. Even if `writeConfigToDisk()` throws an error, the `writeChain` is updated to the new promise immediately. This ensures that subsequent writes continue to process in order rather than hanging indefinitely, preventing the configuration system from deadlocking after a transient disk error.