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
- 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.
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
- 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
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
- Skipping ${storeDir}: resolves outside the managed root via
- Skipping ${targetPath}: existing user-managed symlink (not o
- Skipping ${targetDir}: existing user-managed symlink (not ov
- ${context.collectionPath} exists and is not a symlink; refus
- ${context.collectionPath} points outside a Compound Engineer
AI-assisted analysis of EveryInc/compound-engineering-plugin@c9c10f8c75 (2026-08-31).
Data as JSON: /api/errors/5c8cb47c7d0d84a4.
Report an issue: GitHub.