can1357/oh-my-pi · error
Managed skill "${name}" resolves through a symlink; refusing
Error message
Managed skill "${name}" resolves through a symlink; refusing to write outside the managed directory. What it means
Inside writeManagedSkill, after checking the root, the code lstats the per-skill directory <managed-skills>/<name> and refuses if that directory is itself a symlink — the write could then land outside the managed root. This closes the traversal hole the root check cannot catch (a legit root containing a linked subdirectory).
Source
Thrown at packages/coding-agent/src/autolearn/managed-skills.ts:186
const bytes = Buffer.byteLength(content, "utf8");
if (bytes > MAX_MANAGED_SKILL_BYTES) {
throw new Error(
`Managed skill is ${bytes} bytes; the limit is ${MAX_MANAGED_SKILL_BYTES}. Trim the body or description.`,
);
}
return serializeSkillMutation(name, async () => {
await assertManagedRootSafe();
const dir = path.join(getManagedSkillsDir(), name);
const file = path.join(dir, "SKILL.md");
// Reject a symlinked skill directory: an intermediate symlink would let the
// write escape the isolated managed root. lstat does not follow the final
// 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 mustView on GitHub (pinned to 9690622007)
Solutions
- Remove the symlinked skill directory: `rm ~/.omp/agent/managed-skills/<name>` (removes the link only), then retry — writeManagedSkill will mkdir a real directory
- Never symlink directories into managed-skills; copy content instead or use the authored skills dir
- Audit ~/.omp/agent/managed-skills with `find -type l` to find remaining symlinks
Example fix
// before (shell) ln -s /shared/skills/foo ~/.omp/agent/managed-skills/foo // after (shell) rm ~/.omp/agent/managed-skills/foo # drop the link; retry writeManagedSkill to recreate a real dir
Defensive patterns
Strategy: validation
Validate before calling
import { lstat } from "node:fs/promises";
import * as path from "node:path";
const dir = path.join(getManagedSkillsDir(), name);
const st = await lstat(dir).catch(() => null);
if (st?.isSymbolicLink()) throw new Error("skill dir must not be a symlink"); Type guard
function isRealDirStat(st: { isSymbolicLink(): boolean; isDirectory(): boolean } | null): boolean {
return st !== null && st.isDirectory() && !st.isSymbolicLink();
} Try / catch
try {
await writeManagedSkill(input);
} catch (err) {
if (String((err as Error).message).includes("resolves through a symlink")) {
// rm the linked skill dir and retry so a real directory is created
} else throw err;
} Prevention
- Never symlink directories into ~/.omp/agent/managed-skills; copy instead
- Audit with `find ~/.omp/agent/managed-skills -type l` after external setup tools run
- Use the authored skills dir for shared/linked skill content
When it happens
Trigger: writeManagedSkill targets a skill name whose directory under the managed root is a symlink to another location.
Common situations: A user symlinked a managed skill folder to a shared/location-controlled directory; stow-style dotfile management linked whole skill directories; a malicious or buggy setup script pre-created linked dirs to redirect auto-generated content.
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 "${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/2c56b4a054da7d66.
Report an issue: GitHub.