EveryInc/compound-engineering-plugin · warning

Skipping : existing user-managed symlink (not overwritten)

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.

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.

Appendix: 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)