can1357/oh-my-pi · error

Managed skill "${name}" already exists. Use action "update"

Error message

Managed skill "${name}" already exists. Use action "update" to change it.

What it means

writeManagedSkill with action "create" writes SKILL.md atomically using O_CREAT|O_EXCL ("wx"), which fails with EEXIST if the file already exists. The library converts that EEXIST into this error instead of silently overwriting an existing managed skill. It enforces the create/update contract: create only makes new skills, update mutates existing ones.

Source

Thrown at packages/coding-agent/src/autolearn/managed-skills.ts:198

		// component, so a symlinked `dir` is caught here.
		const dirStat = await fs.lstat(dir).catch(err => {
			if (isEnoent(err)) return null;
			throw err;
		});
		if (dirStat?.isSymbolicLink()) {
			throw new Error(
				`Managed skill "${name}" resolves through a symlink; refusing to write outside the managed directory.`,
			);
		}
		if (input.action === "create") {
			await fs.mkdir(dir, { recursive: true });
			// O_CREAT|O_EXCL ("wx"): atomic create that fails if the file already
			// exists (closing the check-then-write race) and refuses a symlinked SKILL.md.
			try {
				await fs.writeFile(file, content, { flag: "wx" });
			} catch (err) {
				if ((err as { code?: string }).code === "EEXIST") {
					throw new Error(`Managed skill "${name}" already exists. Use action "update" to change it.`);
				}
				throw err;
			}
			return { path: file };
		}
		// update: the file must already exist, be a plain managed file, and must
		// not share an inode with a user-authored file via hard link. Open the
		// checked file handle before truncating so a path swap after lstat cannot
		// redirect the write into a symlink or newly hard-linked target.
		const fileStat = await fs.lstat(file).catch(err => {
			if (isEnoent(err)) return null;
			throw err;
		});
		if (fileStat === null) {
			throw new Error(`Managed skill "${name}" does not exist. Use action "create" to add it.`);
		}
		if (fileStat.isSymbolicLink()) {
			throw new Error(`Managed skill "${name}" SKILL.md is a symlink; refusing to overwrite it.`);

View on GitHub (pinned to 9690622007)

Solutions

  1. Call writeManagedSkill with action "update" instead of "create" to overwrite the existing skill.
  2. Check existence first (lstat the managed SKILL.md) and pick create/update accordingly.
  3. Delete the skill with deleteManagedSkill(name) if you truly want a fresh create.

Example fix

// before
await writeManagedSkill({ action: "create", name: "my-skill", description: "d", body: "b" });
// after
const file = `${getManagedSkillsDir()}/my-skill/SKILL.md`;
const exists = await Bun.file(file).exists();
await writeManagedSkill({ action: exists ? "update" : "create", name: "my-skill", description: "d", body: "b" });
Defensive patterns

Strategy: validation

Validate before calling

import * as fs from "node:fs/promises";
const file = `${getManagedSkillsDir()}/${sanitizeSkillName(name)}/SKILL.md`;
const action = await fs.lstat(file).then(s => s?.isFile() ? "update" : "create").catch(() => "create");

Try / catch

try {
  await writeManagedSkill({ action: "create", name, description, body });
} catch (err) {
  if (err instanceof Error && err.message.includes('already exists')) {
    await writeManagedSkill({ action: "update", name, description, body });
  } else throw err;
}

Prevention

When it happens

Trigger: Calling writeManagedSkill({ action: "create", name, ... }) when ~/.omp/agent/managed-skills/<name>/SKILL.md already exists — e.g. the skill was created in a previous session or by a prior tool call.

Common situations: An agent auto-learn flow re-running a create step for a skill it already generated; re-running a batch script without checking existence first; a stale local cache thinking the skill is missing.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/fc8ea297925618f0. Report an issue: GitHub.