{"record":{"id":"5c8cb47c7d0d84a4","repo":"EveryInc/compound-engineering-plugin","slug":"skipping-targetpath-existing-user-managed-syml-5c8cb4","errorCode":null,"errorMessage":"Skipping ${targetPath}: existing user-managed symlink (not overwritten)","messagePattern":"Skipping (.+?): existing user-managed symlink \\(not overwritten\\)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"src/targets/managed-artifacts.ts","lineNumber":265,"sourceCode":"}\n\nfunction resolveArtifactPath(rootDir: string, relativePath: string): string {\n  return path.join(rootDir, ...relativePath.split(\"/\"))\n}\n\nexport async function lstatOrNull(targetPath: string): Promise<Stats | null> {\n  try {\n    return await fs.lstat(targetPath)\n  } catch (err) {\n    if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null\n    throw err\n  }\n}\n\nexport async function isPreservedSymlink(targetPath: string): Promise<boolean> {\n  const stat = await lstatOrNull(targetPath)\n  if (!stat?.isSymbolicLink()) return false\n  console.warn(`Skipping ${targetPath}: existing user-managed symlink (not overwritten)`)\n  return true\n}\n\n/**\n * Realpath of the nearest existing ancestor of `targetPath` (the path itself\n * when it exists). A not-yet-created descendant cannot introduce a new symlink\n * hop, so resolving the nearest existing ancestor is enough to decide whether\n * `targetPath` escapes a root -- and it lets the containment check run before a\n * fresh store directory has been created.\n */\nasync function realpathNearestExisting(targetPath: string): Promise<string> {\n  let current = path.resolve(targetPath)\n  for (;;) {\n    try {\n      return await fs.realpath(current)\n    } catch (err) {\n      if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err\n      const parent = path.dirname(current)","sourceCodeStart":247,"sourceCodeEnd":283,"githubUrl":"https://github.com/EveryInc/compound-engineering-plugin/blob/c9c10f8c75412c7232cb2bd663e5fd1cea98d84e/src/targets/managed-artifacts.ts#L247-L283","documentation":"This is a warning (not an exception) emitted by isPreservedSymlink in src/targets/managed-artifacts.ts. Before the converter overwrites or removes a managed artifact path, it checks whether the path is a symbolic link; if so, it assumes the user created it deliberately and leaves it untouched. This protects user data such as links pointing into dotfiles repos or custom locations.","triggerScenarios":"Calling writeCodexBundle, cleanupRemovedSkills, cleanupRemovedAgents, cleanupPreviousManagedCodexSkillStore, moveLegacyArtifactToBackup, or cleanupRemovedManagedDirectories when the target path (e.g. ~/.codex/skills/<name>) already exists as a symlink, detected via lstatOrNull(targetPath).isSymbolicLink().","commonSituations":"Users who symlink their agent config directories into a dotfiles repo (stow, GNU stow, chezmoi, bare-git-dotfiles setups) run a convert/install and see previously-managed paths now being links; also happens after a prior manual replacement of an installed file with a link to a customized copy.","solutions":["Decide whether the symlink is intentional: if yes, no action needed — the converter safely skips it and your link stays intact.","If you want the plugin-managed file installed instead, remove the symlink (rm the link, not its target) and re-run the convert/install command.","If the link points at a customized version you want to keep, move your customization into the repo or reconcile content manually, then replace the link with a real file.","Re-run the install command; the warning should disappear once the path is a regular file or absent."],"exampleFix":"// before: path is a symlink\nls -la ~/.codex/skills/ce-plan  # -> ~/dotfiles/codex/skills/ce-plan\n// after: remove the link so the installer can write\nrm ~/.codex/skills/ce-plan\nbun run convert --to codex","handlingStrategy":"validation","validationCode":"import { lstat } from 'node:fs/promises'\nasync function isUserSymlink(p: string): Promise<boolean> {\n  try { return (await lstat(p)).isSymbolicLink() } catch { return false }\n}\n// before install: if (await isUserSymlink(targetPath)) reconcile or remove the link first","typeGuard":"function isStatSymlink(stat: { isSymbolicLink(): boolean } | null): stat is { isSymbolicLink(): boolean } {\n  return stat !== null && stat.isSymbolicLink()\n}","tryCatchPattern":null,"preventionTips":["Keep plugin-managed paths free of hand-created symlinks; symlink at a higher level (e.g. the whole config dir) only if you accept skips","Check `ls -la` on managed dirs before running install/convert","Use the plugin's own customization/config mechanisms instead of replacing installed files with links"],"tags":["symlink","filesystem-safety","install","codex"],"backgroundTag":"user-managed-symlink-skipped","analyzedSha":"c9c10f8c75412c7232cb2bd663e5fd1cea98d84e","analyzedAt":"2026-08-31T15:18:07.959Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}