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
- Remove the symlink itself (rm the link, not its target), then recreate a real directory if the skill should exist.
- Delete the actual target directory manually if that is the intent, and verify the managed root no longer contains links.
- 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
- Check lstat().isSymbolicLink() on the skill directory before deleting.
- Only delete directories your own tooling created inside the managed root.
- Run `find ~/.omp/agent/managed-skills -type l` as part of setup hygiene.
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
- The managed-skills root is a symlink; refusing to operate ou
- Managed skill "${name}" SKILL.md is a symlink; refusing to o
- Managed skill "${name}" resolves through a symlink; refusing
- Security output directory must not be a symbolic link
- Security output directory does not have a canonical identity
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/2ae6595e84e1e8ce.
Report an issue: GitHub.