router-for-me/CLIProxyAPI · error
remove old plugin file: %w
Error message
remove old plugin file: %w
What it means
Windows-only branch of writeFileAtomic (install.go:558-563). On Windows, os.Rename onto an existing file can fail because the target is open or locked; the installer then tries os.Remove(targetPath) first and renames again. This error wraps that removal failing for a reason other than 'file did not exist' — typically the existing library is loaded into the process (locking the DLL) or held open by another process, including antivirus.
Source
Thrown at internal/pluginstore/install.go:561
}()
if errChmod := temp.Chmod(mode); errChmod != nil {
return fmt.Errorf("chmod temp plugin file: %w", errChmod)
}
if _, errWrite := temp.Write(data); errWrite != nil {
return fmt.Errorf("write temp plugin file: %w", errWrite)
}
if errSync := temp.Sync(); errSync != nil {
return fmt.Errorf("sync temp plugin file: %w", errSync)
}
if errClose := temp.Close(); errClose != nil {
return fmt.Errorf("close temp plugin file: %w", errClose)
}
closed = true
if errRename := os.Rename(tempPath, targetPath); errRename != nil {
if runtime.GOOS == "windows" {
if errRemove := os.Remove(targetPath); errRemove != nil && !errors.Is(errRemove, os.ErrNotExist) {
return fmt.Errorf("remove old plugin file: %w", errRemove)
}
if errRenameRetry := os.Rename(tempPath, targetPath); errRenameRetry == nil {
removeTemp = false
return nil
} else {
return fmt.Errorf("install plugin file: %w", errRenameRetry)
}
}
return fmt.Errorf("install plugin file: %w", errRename)
}
removeTemp = false
return nil
}
func loadedPluginInstallBlocked(options InstallOptions) bool {
return options.PluginLoaded != nil && strings.EqualFold(options.GOOS, "windows") && options.PluginLoaded()
}
View on GitHub (pinned to 78f0c4079e)
Solutions
- Set InstallOptions.PluginLoaded to a callback reporting the real load state, and unload the plugin (or restart the server) so installs over a loaded DLL are blocked early with the clearer ErrLoadedPluginLocked instead
- Retry after stopping the process holding the DLL (or wait out the antivirus lock)
- Install a different version — the versioned filename means a new version writes a new path and does not contend with the old file
Example fix
// before: no callback, install races the loaded DLL
res, err := client.Install(ctx, plugin, pluginstore.InstallOptions{PluginsDir: dir, GOOS: "windows"})
// after: surface the lock deterministically
res, err := client.Install(ctx, plugin, pluginstore.InstallOptions{
PluginsDir: dir,
GOOS: "windows",
PluginLoaded: func() bool { return host.IsPluginLoaded(plugin.ID) },
}) Defensive patterns
Strategy: validation
Validate before calling
// Pass PluginLoaded so the store refuses with ErrLoadedPluginLocked EARLY
// (install.go:296-297) instead of failing mid-rename:
opts := pluginstore.InstallOptions{
PluginsDir: dir,
GOOS: "windows",
PluginLoaded: func() bool { return host.IsPluginLoaded(plugin.ID) },
}
if pluginstore.InstallBlockedOnWindows(plugin, opts) { // your own pre-check
host.ScheduleRestartThenInstall(plugin)
} Try / catch
if _, err := client.Install(ctx, plugin, opts); err != nil {
if errors.Is(err, pluginstore.ErrLoadedPluginLocked) {
// planned case: unload/restart, then re-run install
return
}
if runtime.GOOS == "windows" && strings.Contains(err.Error(), "remove old plugin file") {
// target locked by another process (AV?): wait and retry once
}
} Prevention
- Always set InstallOptions.PluginLoaded on Windows hosts
- Unload plugins or restart the server before upgrading a loaded one
- Install new versions rather than overwriting old ones — the versioned filename avoids lock contention entirely
When it happens
Trigger: InstallArchive on windows where the old <id>-v<ver>.dll at the target path is memory-mapped by the running server (the plugin is loaded), or locked by a scanner/editor, so both rename and remove fail.
Common situations: Upgrading a plugin on a running Windows host without unloading it first; antivirus or indexing services briefly locking freshly-written DLLs.
Related errors
- plugin_update_requires_restart
- install plugin file: %w
- zip contains multiple target dynamic libraries
- zip does not contain %s
- zip entry %s uses backslash path separators
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/3d47982c33727a0d.
Report an issue: GitHub.