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

catalog_manifest_invalid:skills[${index}].canonical

Error message

catalog_manifest_invalid:skills[${index}].canonical

What it means

Entries whose status is alias or merged must declare a canonical skill name they redirect to; this error fires when such an entry has no canonical (or a whitespace-only one). Without canonical, alias/merged entries cannot be resolved to their target.

Source

Thrown at src/catalog/schema.ts:78

    assertNonEmptyString(entry.status, `skills[${index}].status`);

    if (!SKILL_CATEGORIES.has(entry.category as CatalogSkillCategory)) {
      throw new Error(`catalog_manifest_invalid:skills[${index}].category`);
    }
    if (!ENTRY_STATUSES.has(entry.status as CatalogEntryStatus)) {
      throw new Error(`catalog_manifest_invalid:skills[${index}].status`);
    }

    const name = entry.name.trim();
    if (seenSkills.has(name)) throw new Error(`catalog_manifest_invalid:duplicate_skill:${name}`);
    seenSkills.add(name);

    const canonical = typeof entry.canonical === 'string' && entry.canonical.trim() !== ''
      ? entry.canonical.trim()
      : undefined;

    if ((entry.status === 'alias' || entry.status === 'merged') && !canonical) {
      throw new Error(`catalog_manifest_invalid:skills[${index}].canonical`);
    }

    return {
      name,
      category: entry.category as CatalogSkillCategory,
      status: entry.status as CatalogEntryStatus,
      canonical,
      core: entry.core === true,
      internalRequired: entry.internalRequired === true,
    };
  });

  const seenAgents = new Set<string>();
  const agents: CatalogAgentEntry[] = input.agents.map((entry, index) => {
    if (!isObject(entry)) throw new Error(`catalog_manifest_invalid:agents[${index}]`);
    assertNonEmptyString(entry.name, `agents[${index}].name`);
    assertNonEmptyString(entry.category, `agents[${index}].category`);
    assertNonEmptyString(entry.status, `agents[${index}].status`);

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Add "canonical": "<primary skill name>" to the entry
  2. Ensure the canonical name matches an existing non-alias skill name in the manifest
  3. If the entry should stand alone, change status back to active

Example fix

// before
{ "name": "ap", "category": "execution", "status": "alias" }

// after
{ "name": "ap", "category": "execution", "status": "alias", "canonical": "autopilot" }
Defensive patterns

Strategy: validation

Validate before calling

for (const s of raw.skills) {
  if ((s.status === 'alias' || s.status === 'merged') && !(typeof s.canonical === 'string' && s.canonical.trim())) {
    /* add canonical or change status */
  }
}

Type guard

const hasCanonicalIfNeeded = (s: Record<string, unknown>) => !(s.status === 'alias' || s.status === 'merged') || (typeof s.canonical === 'string' && s.canonical.trim() !== '');

Prevention

When it happens

Trigger: A skills[i] entry with "status": "alias" or "merged" but missing the canonical field, or canonical: "" / " ".

Common situations: Renaming a skill to alias status and forgetting to add canonical, or generators that drop canonical when it equals the entry name.

Understand the failure class

Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.

Related errors


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