Yeachan-Heo/oh-my-codex · error · Error
plugin_skill_mirror_out_of_sync\nkind=
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.
Solutions
- Inspect the kind/skill/expected/actual fields in the message to identify which mirrored skill diverged
- Delete or restore the affected mirrored skill directory and re-run the plugin mirror sync
- Reinstall or re-register the plugin that owns the mismatched skill
- 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
- Never hand-edit mirrored skill files; edit the source plugin and re-sync
- Run mirror sync immediately after adding/removing plugins
- Keep the mirror directory out of manual git merges or regenerate it in CI
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
- autoresearch_candidate_missing
- canonical_scale_up_rollback_worker_verification_failed
- checkpoint lock is missing or foreign
- cleanupErrors.join(' | ')
- Failed to acquire AGENTS.md lock within timeout
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/7b25787df16c4e11.
Report an issue: GitHub.
Appendix: 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)