can1357/oh-my-pi · error
The managed-skills root is a symlink; refusing to operate ou
Error message
The managed-skills root is a symlink; refusing to operate outside the managed directory.
What it means
Before any managed-skill write or delete, assertManagedRootSafe lstats the managed-skills root and refuses to operate if it is a symbolic link. This is a security guard so writes stay confined to the real managed directory and cannot be redirected elsewhere via a symlink.
Source
Thrown at packages/coding-agent/src/autolearn/managed-skills.ts:123
void guarded.finally(() => {
if (skillMutationChains.get(name) === guarded) skillMutationChains.delete(name);
});
return run;
}
/**
* Reject when the managed-skills root itself is a symlink. lstat on a child
* follows intermediate components, so a symlinked root would let an otherwise
* valid name write/delete outside the isolated directory (e.g. onto authored
* skills). Checked before composing any child path.
*/
async function assertManagedRootSafe(): Promise<void> {
const rootStat = await fs.lstat(getManagedSkillsDir()).catch(err => {
if (isEnoent(err)) return null;
throw err;
});
if (rootStat?.isSymbolicLink()) {
throw new Error("The managed-skills root is a symlink; refusing to operate outside the managed directory.");
}
}
const UPDATE_FILE_OPEN_FLAGS = fsConstants.O_WRONLY | fsConstants.O_NOFOLLOW;
function assertManagedSkillFileSafeForUpdate(name: string, fileStat: Stats): void {
if (!fileStat.isFile()) {
throw new Error(`Managed skill "${name}" SKILL.md is not a regular file; refusing to overwrite it.`);
}
if (fileStat.nlink > 1) {
throw new Error(
`Managed skill "${name}" SKILL.md has ${fileStat.nlink} hard links; refusing to overwrite a file that may be user-authored elsewhere.`,
);
}
}
async function openManagedSkillFileForUpdate(name: string, file: string) {
try {View on GitHub (pinned to 9690622007)
Solutions
- Remove the symlink and let omp recreate managed-skills as a real directory (rm the link, run the operation again)
- If you need skills elsewhere, put them in the user-authored skills dir (~/.omp/agent/skills), not a symlinked managed root
- Check `ls -la ~/.omp/agent/` to confirm the root is a real directory
Example fix
// before (shell) ln -s ~/sync/managed-skills ~/.omp/agent/managed-skills // after (shell) rm ~/.omp/agent/managed-skills && mkdir ~/.omp/agent/managed-skills
Defensive patterns
Strategy: validation
Validate before calling
import { lstat } from "node:fs/promises";
const st = await lstat("~/.omp/agent/managed-skills");
if (st.isSymbolicLink()) throw new Error("managed-skills root must not be a symlink"); Type guard
function isRealDirectoryStat(st: { isSymbolicLink(): boolean; isDirectory(): boolean }): boolean {
return st.isDirectory() && !st.isSymbolicLink();
} Try / catch
try {
await writeManagedSkill(input);
} catch (err) {
if (String((err as Error).message).includes("managed-skills root is a symlink")) {
// remove the symlink and recreate as a real dir, or abort
} else throw err;
} Prevention
- Keep ~/.omp/agent/managed-skills a real directory; never symlink or bind-mount it
- Configure sync tools (Dropbox, dotfile managers) to exclude ~/.omp/agent
- Check `ls -la ~/.omp/agent` after machine setup or migration
- Place user-authored skills in ~/.omp/agent/skills, which has no symlink policy
When it happens
Trigger: writeManagedSkill or deleteManagedSkill runs while ~/.omp/agent/managed-skills is a symlink (to another directory or an attacker-controlled location).
Common situations: A user replaced the managed-skills dir with a symlink to share skills across machines or sync tools (Dropbox, dotfiles repo) created it as a link; setup scripts reorganized ~/.omp with symlinks.
Related errors
- Managed skill "${name}" SKILL.md is a symlink; refusing to o
- Managed skill "${name}" resolves through a symlink; refusing
- Managed skill "${safe}" is a symlink; refusing to delete out
- 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/858753e0f7072a92.
Report an issue: GitHub.