can1357/oh-my-pi · error

Managed skill "${safe}" is a symlink; refusing to delete out

Error message

Managed skill "${safe}" is a symlink; refusing to delete outside the managed directory.

What it means

deleteManagedSkill lstats the skill directory and refuses if the directory itself is a symlink, because fs.rm recursive would follow it and delete files outside the managed root. This keeps deletion confined to ~/.omp/agent/managed-skills.

Source

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

			await handle.close();
		}
		return { path: file };
	});
}

/** Delete a managed skill directory. Throws when it does not exist. */
export async function deleteManagedSkill(name: string): Promise<void> {
	const safe = sanitizeSkillName(name);
	await serializeSkillMutation(safe, async () => {
		await assertManagedRootSafe();
		const dir = path.join(getManagedSkillsDir(), safe);
		// Refuse to follow a symlinked skill directory (rm would delete the target).
		const dirStat = await fs.lstat(dir).catch(err => {
			if (isEnoent(err)) return null;
			throw err;
		});
		if (dirStat?.isSymbolicLink()) {
			throw new Error(`Managed skill "${safe}" is a symlink; refusing to delete outside the managed directory.`);
		}
		try {
			await fs.rm(dir, { recursive: true });
		} catch (err) {
			if (isEnoent(err)) {
				throw new Error(`Managed skill "${safe}" does not exist.`);
			}
			throw err;
		}
	});
}

View on GitHub (pinned to 9690622007)

Solutions

  1. Remove the symlink itself (rm the link, not its target), then recreate a real directory if the skill should exist.
  2. Delete the actual target directory manually if that is the intent, and verify the managed root no longer contains links.
  3. Audit with `find ~/.omp/agent/managed-skills -maxdepth 1 -type l` to locate offending links.

Example fix

# before: managed-skills/my-skill -> /elsewhere/my-skill (symlink)
# after
rm ~/.omp/agent/managed-skills/my-skill   # removes the link only
Defensive patterns

Strategy: validation

Validate before calling

import * as fs from "node:fs/promises";
const dir = `${getManagedSkillsDir()}/${sanitizeSkillName(name)}`;
const st = await fs.lstat(dir).catch(() => null);
if (st?.isSymbolicLink()) throw new Error(`${name} is a symlink; refusing to delete`);

Try / catch

try {
  await deleteManagedSkill(name);
} catch (err) {
  if (err instanceof Error && err.message.includes('symlink')) {
    logger.warn("refusing to delete symlinked managed skill", { name });
  } else throw err;
}

Prevention

When it happens

Trigger: deleteManagedSkill(name) is called while ~/.omp/agent/managed-skills/<name> is a symlink to another directory (authored skills, a tmp dir, etc.).

Common situations: A user linked a managed skill dir to a real skill folder to keep them in sync; a migration or sync tool replaced the directory with a link; malicious symlink planting.

Related errors


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