{"record":{"id":"2ae6595e84e1e8ce","repo":"can1357/oh-my-pi","slug":"managed-skill-safe-is-a-symlink-refusing-to","errorCode":null,"errorMessage":"Managed skill \"${safe}\" is a symlink; refusing to delete outside the managed directory.","messagePattern":"Managed skill \"(.+?)\" is a symlink; refusing to delete outside the managed directory\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/autolearn/managed-skills.ts","lineNumber":244,"sourceCode":"\t\t\tawait handle.close();\n\t\t}\n\t\treturn { path: file };\n\t});\n}\n\n/** Delete a managed skill directory. Throws when it does not exist. */\nexport async function deleteManagedSkill(name: string): Promise<void> {\n\tconst safe = sanitizeSkillName(name);\n\tawait serializeSkillMutation(safe, async () => {\n\t\tawait assertManagedRootSafe();\n\t\tconst dir = path.join(getManagedSkillsDir(), safe);\n\t\t// Refuse to follow a symlinked skill directory (rm would delete the target).\n\t\tconst dirStat = await fs.lstat(dir).catch(err => {\n\t\t\tif (isEnoent(err)) return null;\n\t\t\tthrow err;\n\t\t});\n\t\tif (dirStat?.isSymbolicLink()) {\n\t\t\tthrow new Error(`Managed skill \"${safe}\" is a symlink; refusing to delete outside the managed directory.`);\n\t\t}\n\t\ttry {\n\t\t\tawait fs.rm(dir, { recursive: true });\n\t\t} catch (err) {\n\t\t\tif (isEnoent(err)) {\n\t\t\t\tthrow new Error(`Managed skill \"${safe}\" does not exist.`);\n\t\t\t}\n\t\t\tthrow err;\n\t\t}\n\t});\n}\n","sourceCodeStart":226,"sourceCodeEnd":256,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/autolearn/managed-skills.ts#L226-L256","documentation":"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.","triggerScenarios":"deleteManagedSkill(name) is called while ~/.omp/agent/managed-skills/<name> is a symlink to another directory (authored skills, a tmp dir, etc.).","commonSituations":"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.","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."],"exampleFix":"# before: managed-skills/my-skill -> /elsewhere/my-skill (symlink)\n# after\nrm ~/.omp/agent/managed-skills/my-skill   # removes the link only","handlingStrategy":"validation","validationCode":"import * as fs from \"node:fs/promises\";\nconst dir = `${getManagedSkillsDir()}/${sanitizeSkillName(name)}`;\nconst st = await fs.lstat(dir).catch(() => null);\nif (st?.isSymbolicLink()) throw new Error(`${name} is a symlink; refusing to delete`);","typeGuard":null,"tryCatchPattern":"try {\n  await deleteManagedSkill(name);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('symlink')) {\n    logger.warn(\"refusing to delete symlinked managed skill\", { name });\n  } else throw err;\n}","preventionTips":["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."],"tags":["security","symlink","filesystem"],"backgroundTag":"symlink-attack-refused","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}