# How the Configuration Manager in Desktop Commander Persists and Loads Settings

> Discover how Desktop Commander's config manager persists and loads settings using atomic writes, non-blocking saves, and automatic init in a JSON file.

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

---

**Desktop Commander stores its runtime configuration in a JSON file at `~/.claude-server-commander/config.json` and manages it through a singleton `ConfigManager` class that provides atomic writes, non-blocking save scheduling, and automatic initialization.**

The configuration system in [wonderwhy-er/DesktopCommanderMCP](https://github.com/wonderwhy-er/DesktopCommanderMCP) handles everything from shell preferences to telemetry opt-outs. This article breaks down how the configuration manager initializes, persists changes safely, and recovers settings across process restarts.

## Configuration File Location and Structure

The configuration manager determines where to store data through a single constant defined in [`src/config.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/config.ts):

```typescript
// src/config.ts#L8-L10
export const CONFIG_FILE = join(homedir(), '.claude-server-commander', 'config.json');

```

All settings live in this JSON file, including the configuration version, default shell, telemetry preferences, and a persistent client ID for analytics.

## Initialization: Loading or Creating the Config

The `ConfigManager.init()` method in [`src/config-manager.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/config-manager.ts) handles first-run scenarios and subsequent restarts. The initialization sequence runs automatically before any read or write operation.

**What happens during initialization:**

1. **Ensures directory exists** — Creates `~/.claude-server-commander/` recursively if missing
2. **Checks for existing file** — Uses `fs.access` to test for [`config.json`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/config.json)
3. **Loads existing config** — Parses the JSON into `this.config` if present
4. **Creates default config** — Calls `getDefaultConfig()` if the file is absent
5. **Stamps version** — Writes `VERSION` from [`src/version.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/version.ts) into the config object

```typescript
// Example: init runs automatically, but you can await it explicitly
import { configManager } from './config-manager.js';

await configManager.init(); // Optional — getConfig/setValue trigger it implicitly
const isFirstRun = configManager.isFirstRun(); // true if new file was created

```

The `_isFirstRun` flag lets the application detect fresh installations and show onboarding flows.

## Reading Configuration Values

All read operations are asynchronous to guarantee initialization completes first.

**Get the entire configuration:**

```typescript
const cfg = await configManager.getConfig();
console.log(cfg.defaultShell); // Current shell preference
console.log(cfg.telemetryEnabled); // boolean

```

**Get a single value:**

```typescript
const limit = await configManager.getValue('fileReadLineLimit');

```

Both methods return shallow copies to prevent accidental mutation outside the manager.

## Persisting Changes: Two Save Strategies

The configuration manager implements dual write paths optimized for different access patterns.

### Blocking Writes with `setValue`

Use `setValue` when you need immediate persistence and confirmation:

```typescript
await configManager.setValue('defaultShell', '/bin/zsh');
// Returns only after config.json is written to disk

```

Behind the scenes, `setValue` chains writes through `this.writeChain` — a promise queue that serializes concurrent operations. Even if one write fails, the chain continues (`catch(() => {})`) so subsequent saves aren't blocked. The actual disk operation happens in `writeConfigToDisk()` via `fs.writeFile` with pretty-printed JSON.

### Non-Blocking Writes with `setValueNonBlocking`

High-frequency updates use `setValueNonBlocking` to avoid I/O bottlenecks:

```typescript
// Called rapidly — only one disk write will occur
await configManager.setValueNonBlocking('filesProcessed', count);
await configManager.setValueNonBlocking('bytesRead', bytes);

```

The `scheduleSave()` method implements **write coalescing**: rapid calls set a `saveScheduled` flag, and a single pending write handles all accumulated changes. This prevents I/O storms from telemetry counters or batch operations.

## Bulk Updates and Factory Reset

**Merge multiple changes atomically:**

```typescript
await configManager.updateConfig({
  defaultShell: '/bin/fish',
  telemetryEnabled: true,
  fileReadLineLimit: 5000
});
// Single write operation for all three changes

```

**Restore defaults:**

```typescript
await configManager.resetConfig();
// Replaces config with getDefaultConfig() and persists immediately

```

## Persistent Client ID Generation

The manager lazily generates and stores a UUID for analytics tracking:

```typescript
const clientId = await configManager.getOrCreateClientId();
// Same value returned across all future calls — stored in config.json

```

This ensures consistent user identification without external dependencies.

## Telemetry Opt-Out Handling

When `telemetryEnabled` is set to `false`, the manager captures one final event before persisting the change. This respects user privacy while recording the opt-out decision itself — implemented with help from [`src/utils/capture.js`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/utils/capture.js).

```typescript
await configManager.setValue('telemetryEnabled', false);
// Emits final telemetry event, then saves config with telemetry disabled

```

## Key Implementation Files

| File | Responsibility |
|------|---------------|
| [`src/config-manager.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/config-manager.ts) | Core singleton with init, save, and mutation logic |
| [`src/config.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/config.ts) | `CONFIG_FILE` constant and path resolution |
| [`src/version.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/version.ts) | `VERSION` string stamped into saved configs |
| [`src/utils/capture.js`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/utils/capture.js) | Telemetry emission for opt-out events |

## Summary

- **Storage location**: `~/.claude-server-commander/config.json` via `CONFIG_FILE` constant
- **Singleton access**: Import `configManager` from [`src/config-manager.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/config-manager.ts)
- **Atomic writes**: Promise chain serialization prevents corruption during concurrent updates
- **Performance optimization**: Non-blocking saves with automatic coalescing for high-frequency changes
- **First-run detection**: `isFirstRun()` returns true when default config is created fresh
- **Persistent identity**: `getOrCreateClientId()` provides stable UUID across restarts

## Frequently Asked Questions

### What happens if the config file is corrupted?

The manager relies on standard JSON parsing. If `JSON.parse` throws during `init()`, the error propagates to the caller. No automatic recovery is implemented — corrupted files require manual deletion or restoration.

### Can multiple processes safely write to the same config file?

The promise chain in `saveConfig()` serializes writes within a single process, but there is no cross-process file locking. Concurrent access from separate Node.js processes could theoretically corrupt the file.

### How do I migrate settings when the configuration format changes?

The `VERSION` field is written into every saved config, but no automatic migration logic exists in the current implementation. Version-dependent branching would need to be added to `init()` or `getDefaultConfig()`.

### Why are read operations asynchronous when the file is already loaded?

The async wrapper guarantees that `init()` has completed, handling race conditions where `getConfig()` or `getValue()` is called immediately after module import before the background initialization finishes.