EveryInc/compound-engineering-plugin · warning

Skipping ${targetPath}: existing user-managed symlink (not o

Error message

Skipping ${targetPath}: existing user-managed symlink (not overwritten)

What it means

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.

Source

Thrown at src/targets/managed-artifacts.ts:265

}

function resolveArtifactPath(rootDir: string, relativePath: string): string {
  return path.join(rootDir, ...relativePath.split("/"))
}

export async function lstatOrNull(targetPath: string): Promise<Stats | null> {
  try {
    return await fs.lstat(targetPath)
  } catch (err) {
    if ((err as NodeJS.ErrnoException).code === "ENOENT") return null
    throw err
  }
}

export async function isPreservedSymlink(targetPath: string): Promise<boolean> {
  const stat = await lstatOrNull(targetPath)
  if (!stat?.isSymbolicLink()) return false
  console.warn(`Skipping ${targetPath}: existing user-managed symlink (not overwritten)`)
  return true
}

/**
 * Realpath of the nearest existing ancestor of `targetPath` (the path itself
 * when it exists). A not-yet-created descendant cannot introduce a new symlink
 * hop, so resolving the nearest existing ancestor is enough to decide whether
 * `targetPath` escapes a root -- and it lets the containment check run before a
 * fresh store directory has been created.
 */
async function realpathNearestExisting(targetPath: string): Promise<string> {
  let current = path.resolve(targetPath)
  for (;;) {
    try {
      return await fs.realpath(current)
    } catch (err) {
      if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err
      const parent = path.dirname(current)

View on GitHub (pinned to c9c10f8c75)

Solutions

  1. Decide whether the symlink is intentional: if yes, no action needed — the converter safely skips it and your link stays intact.
  2. 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.
  3. 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.
  4. Re-run the install command; the warning should disappear once the path is a regular file or absent.

Example fix

// before: path is a symlink
ls -la ~/.codex/skills/ce-plan  # -> ~/dotfiles/codex/skills/ce-plan
// after: remove the link so the installer can write
rm ~/.codex/skills/ce-plan
bun run convert --to codex
Defensive patterns

Strategy: validation

Validate before calling

import { lstat } from 'node:fs/promises'
async function isUserSymlink(p: string): Promise<boolean> {
  try { return (await lstat(p)).isSymbolicLink() } catch { return false }
}
// before install: if (await isUserSymlink(targetPath)) reconcile or remove the link first

Type guard

function isStatSymlink(stat: { isSymbolicLink(): boolean } | null): stat is { isSymbolicLink(): boolean } {
  return stat !== null && stat.isSymbolicLink()
}

Prevention

When it happens

Trigger: 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().

Common situations: 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.

Related errors


AI-assisted analysis of EveryInc/compound-engineering-plugin@c9c10f8c75 (2026-08-31). Data as JSON: /api/errors/5c8cb47c7d0d84a4. Report an issue: GitHub.