{"record":{"id":"858753e0f7072a92","repo":"can1357/oh-my-pi","slug":"the-managed-skills-root-is-a-symlink-refusing-to","errorCode":null,"errorMessage":"The managed-skills root is a symlink; refusing to operate outside the managed directory.","messagePattern":"The managed-skills root is a symlink; refusing to operate outside the managed directory\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/autolearn/managed-skills.ts","lineNumber":123,"sourceCode":"\tvoid guarded.finally(() => {\n\t\tif (skillMutationChains.get(name) === guarded) skillMutationChains.delete(name);\n\t});\n\treturn run;\n}\n\n/**\n * Reject when the managed-skills root itself is a symlink. lstat on a child\n * follows intermediate components, so a symlinked root would let an otherwise\n * valid name write/delete outside the isolated directory (e.g. onto authored\n * skills). Checked before composing any child path.\n */\nasync function assertManagedRootSafe(): Promise<void> {\n\tconst rootStat = await fs.lstat(getManagedSkillsDir()).catch(err => {\n\t\tif (isEnoent(err)) return null;\n\t\tthrow err;\n\t});\n\tif (rootStat?.isSymbolicLink()) {\n\t\tthrow new Error(\"The managed-skills root is a symlink; refusing to operate outside the managed directory.\");\n\t}\n}\n\nconst UPDATE_FILE_OPEN_FLAGS = fsConstants.O_WRONLY | fsConstants.O_NOFOLLOW;\n\nfunction assertManagedSkillFileSafeForUpdate(name: string, fileStat: Stats): void {\n\tif (!fileStat.isFile()) {\n\t\tthrow new Error(`Managed skill \"${name}\" SKILL.md is not a regular file; refusing to overwrite it.`);\n\t}\n\tif (fileStat.nlink > 1) {\n\t\tthrow new Error(\n\t\t\t`Managed skill \"${name}\" SKILL.md has ${fileStat.nlink} hard links; refusing to overwrite a file that may be user-authored elsewhere.`,\n\t\t);\n\t}\n}\n\nasync function openManagedSkillFileForUpdate(name: string, file: string) {\n\ttry {","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/autolearn/managed-skills.ts#L105-L141","documentation":"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.","triggerScenarios":"writeManagedSkill or deleteManagedSkill runs while ~/.omp/agent/managed-skills is a symlink (to another directory or an attacker-controlled location).","commonSituations":"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.","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"],"exampleFix":"// before (shell)\nln -s ~/sync/managed-skills ~/.omp/agent/managed-skills\n// after (shell)\nrm ~/.omp/agent/managed-skills && mkdir ~/.omp/agent/managed-skills","handlingStrategy":"validation","validationCode":"import { lstat } from \"node:fs/promises\";\nconst st = await lstat(\"~/.omp/agent/managed-skills\");\nif (st.isSymbolicLink()) throw new Error(\"managed-skills root must not be a symlink\");","typeGuard":"function isRealDirectoryStat(st: { isSymbolicLink(): boolean; isDirectory(): boolean }): boolean {\n  return st.isDirectory() && !st.isSymbolicLink();\n}","tryCatchPattern":"try {\n  await writeManagedSkill(input);\n} catch (err) {\n  if (String((err as Error).message).includes(\"managed-skills root is a symlink\")) {\n    // remove the symlink and recreate as a real dir, or abort\n  } else throw err;\n}","preventionTips":["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"],"tags":["security","symlink","filesystem"],"backgroundTag":"symlink-security-guard","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}