Yeachan-Heo/oh-my-codex · error · Error

plugin_skill_mirror_out_of_sync\nkind=${mismatch.kind}\n${mi

Error message

plugin_skill_mirror_out_of_sync\nkind=${mismatch.kind}\n${mismatch.skillName ? `skill=${mismatch.skillName}` : undefined}\nmessage=${mismatch.message}\n${mismatch.expected ? `expected=${JSON.stringify(mismatch.expected)}` : undefined}\n${mismatch.actual ? `actual=${JSON.stringify(mismatch.actual)}` : undefined}

What it means

Thrown by assertSkillMirror when the on-disk mirror of plugin skills does not match the expected catalog state (missing/extra/changed skill files or metadata). The message embeds machine-readable key=value lines (kind, skill, expected, actual) describing the exact mismatch. It is an internal consistency assertion raised during syncPluginMirror, meaning the mirror directory diverged from the source of truth.

Source

Thrown at src/catalog/skill-mirror.ts:147

  return null;
}

export async function assertSkillMirror(
  expectedSkillsDir: string,
  actualSkillsDir: string,
  expectedSkillNames: readonly string[],
  options: DirectoryMirrorOptions = {},
): Promise<void> {
  const mismatch = await compareSkillMirror(
    expectedSkillsDir,
    actualSkillsDir,
    expectedSkillNames,
    options,
  );
  if (!mismatch) return;

  throw new Error(
    [
      'plugin_skill_mirror_out_of_sync',
      `kind=${mismatch.kind}`,
      mismatch.skillName ? `skill=${mismatch.skillName}` : undefined,
      `message=${mismatch.message}`,
      mismatch.expected ? `expected=${JSON.stringify(mismatch.expected)}` : undefined,
      mismatch.actual ? `actual=${JSON.stringify(mismatch.actual)}` : undefined,
    ].filter(Boolean).join('\n'),
  );
}

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Inspect the kind/skill/expected/actual fields in the message to identify which mirrored skill diverged
  2. Delete or restore the affected mirrored skill directory and re-run the plugin mirror sync
  3. Reinstall or re-register the plugin that owns the mismatched skill
  4. If the mirror is stale from an upgrade, regenerate the whole mirror rather than patching individual files

Example fix

# before
rm -rf .omx/skills/my-skill  # partial manual delete -> out of sync

# after
omx plugin sync-mirror  # re-run full sync to rebuild mirror
Defensive patterns

Strategy: validation

Validate before calling

// Before sync, verify each expected skill file exists and matches expected names
import { readdirSync, existsSync } from 'node:fs';
const expected = new Set(expectedSkillNames); // from plugin manifest
const actual = new Set(readdirSync(actualSkillsDir).filter(f => f.endsWith('.md')));
for (const name of expected) if (!actual.has(name)) throw new Error(`missing mirror skill: ${name}`);
for (const name of actual) if (!expected.has(name)) console.warn(`extra mirror skill: ${name}`);

Try / catch

try {
  await syncPluginMirror(plugin);
} catch (e) {
  if (e instanceof Error && e.message.includes('plugin_skill_mirror_out_of_sync')) {
    await rm(actualSkillsDir, { recursive: true, force: true });
    await syncPluginMirror(plugin); // rebuild from scratch
  } else throw e;
}

Prevention

When it happens

Trigger: Calling the skill-mirror sync flow when the mirrored skills directory has been manually edited, partially deleted, written by an older plugin version, or when a plugin was added/removed without re-running the mirror sync.

Common situations: Developers hand-edit mirrored skill files, stale mirrors left behind after upgrading a plugin, interrupted sync leaving partial files, or checkout of a repo where the mirror directory is gitignored.

Related errors


AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27). Data as JSON: /api/errors/7b25787df16c4e11. Report an issue: GitHub.