{"record":{"id":"09a805a56644369e","repo":"can1357/oh-my-pi","slug":"managed-skill-name-skill-md-is-a-symlink-ref","errorCode":null,"errorMessage":"Managed skill \"${name}\" SKILL.md is a symlink; refusing to overwrite it.","messagePattern":"Managed skill \"(.+?)\" SKILL\\.md is a symlink; refusing to overwrite it\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/autolearn/managed-skills.ts","lineNumber":145,"sourceCode":"const 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 {\n\t\treturn await fs.open(file, UPDATE_FILE_OPEN_FLAGS);\n\t} catch (err) {\n\t\tif ((err as { code?: string }).code === \"ELOOP\") {\n\t\t\tthrow new Error(`Managed skill \"${name}\" SKILL.md is a symlink; refusing to overwrite it.`);\n\t\t}\n\t\tthrow err;\n\t}\n}\n\n/** Create or update a managed `SKILL.md`. Returns the resolved file path. */\nexport async function writeManagedSkill(input: WriteManagedSkillInput): Promise<{ path: string }> {\n\tconst name = sanitizeSkillName(input.name);\n\tconst description = sanitizeManagedDescription(input.description);\n\tconst body = input.body.trim();\n\t// Reject empty content: an all-whitespace/control description sanitizes to \"\"\n\t// and the `requireDescription` discovery scan then silently drops the skill,\n\t// so the tool would report success for a skill that never appears.\n\tif (!description) {\n\t\tthrow new Error(`Managed skill \"${name}\" needs a non-empty description.`);\n\t}\n\tif (!body) {\n\t\tthrow new Error(`Managed skill \"${name}\" needs a non-empty body.`);","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/autolearn/managed-skills.ts#L127-L163","documentation":"openManagedSkillFileForUpdate opens SKILL.md with O_NOFOLLOW, which makes open() fail with ELOOP if the target is a symlink. The ELOOP is converted into this explicit error so an update can never write through a symlink to a path outside the managed directory.","triggerScenarios":"writeManagedSkill updates an existing skill whose <name>/SKILL.md is a symbolic link (fs.open returns errno ELOOP).","commonSituations":"A user or sync tool replaced a managed skill's SKILL.md with a symlink to their own file; dotfile managers stow-link individual files into ~/.omp.","solutions":["Replace the symlink with a real file: `rm <path> && cp <target-of-link> <path>`, then retry","Stop symlinking individual SKILL.md files into managed-skills; place authored skills in ~/.omp/agent/skills instead","Verify with `ls -la ~/.omp/agent/managed-skills/<name>/`"],"exampleFix":"// before (shell)\nln -s ~/my-skills/foo.md ~/.omp/agent/managed-skills/foo/SKILL.md\n// after (shell)\nrm ~/.omp/agent/managed-skills/foo/SKILL.md && cp ~/my-skills/foo.md ~/.omp/agent/managed-skills/foo/SKILL.md","handlingStrategy":"validation","validationCode":"import { lstat } from \"node:fs/promises\";\nconst st = await lstat(skillMdPath).catch(() => null);\nif (st?.isSymbolicLink()) throw new Error(\"SKILL.md must not be a symlink\");","typeGuard":"function isNotSymlink(st: { isSymbolicLink(): boolean } | null): boolean {\n  return st === null || !st.isSymbolicLink();\n}","tryCatchPattern":"try {\n  await writeManagedSkill({ name, action: \"update\", ... });\n} catch (err) {\n  if (String((err as Error).message).includes(\"is a symlink\")) {\n    // replace the symlink with a real file (rm + cp), then retry\n  } else throw err;\n}","preventionTips":["Never symlink individual SKILL.md files into managed-skills","Use the user-authored skills dir (~/.omp/agent/skills) for linked content","Audit with `find ~/.omp/agent/managed-skills -type l` periodically"],"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"}