How Modly's Built‑in Sync Mechanism Works for Extensions in User Data Directories

Modly synchronizes built‑in extensions by copying them from the application bundle's resources/extensions folder to the user's data directory on startup, using version‑file comparison to detect updates and perform atomic replacements.

The lightningpixel/modly repository implements a robust extension synchronization system that ensures users always have the latest built‑in extensions while preserving their custom installations. This mechanism runs automatically during the Electron main process initialization, handling path resolution, version detection, and safe file operations.

Where Built‑in Extensions Are Stored

Modly bundles its default extensions inside the Electron application package. The source location depends on whether the app is running in development or production:

  • Production: process.resourcesPath/extensions — the standard Electron resources directory within the compiled app bundle
  • Development: Typically resolved relative to the project root for local testing

The target directory is always within the user's personal data space: app.getPath('userData')/extensions. On most platforms this resolves to:

  • macOS: ~/Library/Application Support/Modly/extensions
  • Windows: %APPDATA%/Modly/extensions
  • Linux: ~/.config/Modly/extensions

The Core Sync Algorithm in builtin-sync.ts

The synchronization logic lives in electron/main/builtin-sync.ts. This module exports a single async function syncBuiltinExtensions() that orchestrates the entire process.

Version detection strategy: The system uses a simple builtin-version.txt file as its source of truth. This file contains a version string or checksum generated at build time. The sync routine compares the version in the source directory against the version already present in the user's extensions folder.

// electron/main/builtin-sync.ts (architecture overview)
import { app } from 'electron';
import { promises as fs } from 'fs';
import path from 'path';
import { logger } from './logger';

export async function syncBuiltinExtensions(): Promise<void> {
  const userData = app.getPath('userData');
  const targetDir = path.join(userData, 'extensions');
  const sourceDir = path.join(process.resourcesPath, 'extensions');
  const versionFile = 'builtin-version.txt';

  // Read versions from both locations (empty string if missing)
  const [srcVersion, tgtVersion] = await Promise.all([
    fs.readFile(path.join(sourceDir, versionFile), 'utf8').catch(() => ''),
    fs.readFile(path.join(targetDir, versionFile), 'utf8').catch(() => ''),
  ]);

  // Trigger full replacement only when versions differ
  if (srcVersion !== tgtVersion) {
    logger.info(`Updating built-in extensions: ${tgtVersion} → ${srcVersion}`);
    await replaceExtensions(sourceDir, targetDir);
  }
}

This approach avoids unnecessary filesystem operations when the app restarts with no extension updates.

File Copying with Recursive Directory Traversal

When a version mismatch is detected, the sync mechanism performs an atomic‑style replacement:

  1. Remove the existing target directory entirely
  2. Recreate it with fresh contents from the source
  3. Preserve nested directory structures and file permissions

The copyDirectory helper implements depth‑first recursion using Node.js fs.promises APIs:

// Recursive copy implementation from builtin-sync.ts
async function copyDirectory(src: string, dst: string): Promise<void> {
  const entries = await fs.readdir(src, { withFileTypes: true });
  await fs.mkdir(dst, { recursive: true });

  for (const entry of entries) {
    const srcPath = path.join(src, entry.name);
    const dstPath = path.join(dst, entry.name);

    if (entry.isDirectory()) {
      await copyDirectory(srcPath, dstPath);  // Recurse into subdirectories
    } else {
      await fs.copyFile(srcPath, dstPath);     // Copy individual files
    }
  }
}

This implementation handles arbitrary nesting depth and maintains the original file hierarchy exactly as packaged.

Integration with Main Process Startup

The sync runs before any renderer windows are created, ensuring extensions are available when the UI initializes. In electron/main/index.ts, the initialization sequence is:

// electron/main/index.ts — startup sequence
import { app, BrowserWindow } from 'electron';
import { syncBuiltinExtensions } from './builtin-sync';
import { logger } from './logger';

app.whenReady().then(async () => {
  try {
    await syncBuiltinExtensions();  // Synchronize before UI loads
    logger.info('Extension sync completed');
  } catch (err) {
    logger.error('Extension sync failed, continuing with existing extensions:', err);
  }

  createMainWindow();  // Now safe to initialize UI
});

The try/catch wrapper ensures that sync failures — whether from permission errors, corrupted bundles, or disk space issues — never prevent the application from launching. Users see the last known good extension set rather than a broken startup.

Error Handling and Logging Strategy

All sync operations report through Modly's centralized logger at electron/main/logger.ts. The system distinguishes between:

  • Informational: Version updates, successful completions
  • Warnings: Missing version files (treated as "always update")
  • Errors: Permission denials, I/O failures, malformed paths

Critical safety property: the sync routine never modifies user‑installed extensions. Because it operates on a dedicated subdirectory for built‑in extensions, third‑party extensions placed directly in ~/Modly/extensions remain untouched even during full replacement cycles.

Path Security with Extension‑Path Guard

A companion module at electron/main/extension-path-guard.ts validates all paths used during sync operations. This prevents directory traversal attacks where a maliciously crafted extension name might escape the intended sandbox. The guard checks:

  • Path normalization (resolving .. segments)
  • Allowed base directory prefixes
  • Symlink targets (when supported)

The sync mechanism invokes these guards before any fs operations, adding defense‑in‑depth to the file copying logic.

Summary

  • Source location: Bundled resources/extensions within the Electron app package
  • Target location: app.getPath('userData')/extensions in the user's profile
  • Update detection: builtin-version.txt comparison between source and destination
  • Replacement strategy: Full directory removal and recursive copy when versions differ
  • Safety guarantees: Try/catch isolation, user extension preservation, path validation via extension-path-guard.ts
  • Entry point: electron/main/index.ts calls syncBuiltinExtensions() during app.whenReady()

Frequently Asked Questions

How does Modly handle extension updates without losing user data?

Modly stores built‑in extensions in a dedicated subdirectory separate from user‑installed extensions. The sync routine only replaces this subdirectory, leaving any manually added extensions intact. The version‑file mechanism ensures updates apply atomically: the old built‑in set is removed entirely and replaced with the new bundle.

What happens if the sync fails due to permission errors?

The error is caught in electron/main/index.ts and logged via logger.error(), but the application continues launching. Users retain their existing extension directory contents. No partial writes occur because the replacement uses fs.rm() followed by full copyDirectory() — interrupted operations simply leave the directory missing, which triggers a fresh sync on the next restart.

Can users disable the built‑in extension synchronization?

According to the source code in builtin-sync.ts, there is no configuration flag to disable sync. The mechanism always runs during startup. Users who wish to modify built‑in extensions must do so after the sync completes, though their changes will be overwritten on the next application update that changes the version file.

Where is the version file builtin-version.txt generated?

The version file is created at build time and embedded into resources/extensions/ within the application bundle. Its content is typically a commit hash, package version, or content checksum. The sync routine treats any difference between source and target versions as a signal to perform full replacement.

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 →