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

cleanupCurrentManagedFile in src/targets/opencode.ts is the preserve-check for OpenCode managed files during cleanup. It returns true (skip) when the target path is a symlink — treated as user-managed and never overwritten. This is the OpenCode analogue of the shared isPreservedSymlink guard and protects user links from being deleted or replaced by install/cleanup runs.

Source

Thrown at src/targets/opencode.ts:37

  writeManagedInstallManifest,
} from "./managed-artifacts"

// Returns true when the existing path was preserved (skip cleanup AND the
// subsequent write -- writing through a preserved symlink would clobber the
// user's fork, which is worse than not overwriting at all). All four
// OpenCode artifact kinds (agents, commands, plugins, skills) are tracked in
// the install manifest's `groups` map, so the same ownership rule applies
// uniformly here rather than a symlink-only guard.
async function cleanupCurrentManagedFile(
  targetPath: string,
  manifest: ManagedInstallManifest | null,
  group: string,
  entryName: string,
): Promise<boolean> {
  const stat = await lstatOrNull(targetPath)
  if (!stat) return false
  if (stat.isSymbolicLink()) {
    console.warn(`Skipping ${targetPath}: existing user-managed symlink (not overwritten)`)
    return true
  }
  if (!manifest?.groups[group]?.includes(entryName)) {
    console.warn(`Skipping ${targetPath}: existing unmanaged file (not overwritten)`)
    return true
  }
  return false
}

async function mergeOpenCodeConfig(
  configPath: string,
  incoming: OpenCodeConfig,
): Promise<OpenCodeConfig> {
  if (!(await pathExists(configPath))) return incoming

  let existing: OpenCodeConfig
  try {
    existing = await readJson<OpenCodeConfig>(configPath)

View on GitHub (pinned to c9c10f8c75)

Solutions

  1. If the symlink is intentional, nothing to do — the file is preserved as-is.
  2. Remove the symlink (rm the link only) and re-run the install to get the generated file in place.
  3. If the link target holds customizations you want, merge them into your own copy or into the plugin source, then delete the link and reinstall.
  4. Verify with `ls -la .opencode/agents/` which entries are links before re-running.

Example fix

// before
ls -la .opencode/agents/review.md  # -> ~/shared/agents/review.md
// after
rm .opencode/agents/review.md
bun run convert --to opencode
Defensive patterns

Strategy: validation

Validate before calling

import { lstat } from 'node:fs/promises'
async function pathIsSymlink(p: string): Promise<boolean> {
  try { return (await lstat(p)).isSymbolicLink() } catch { return false }
}
// before install: if (await pathIsSymlink('.opencode/agents/<name>.md')) resolve manually

Type guard

function isSymlinkStat(stat: { isSymbolicLink(): boolean } | undefined): boolean {
  return stat?.isSymbolicLink() === true
}

Prevention

When it happens

Trigger: Running an OpenCode bundle write/cleanup when the managed file path (e.g. .opencode/agents/<name>.md) exists and lstat reports it is a symbolic link; cleanupCurrentManagedFile is reached via the `preserved` path of the cleanup routine.

Common situations: Dotfiles-managed .opencode directories (stow/chezmoi symlinking each file), users linking an agent file to a shared or customized copy, or prior tooling that replaced managed files with links.

Related errors


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