router-for-me/CLIProxyAPI · error
move shadow plugin: %w
Error message
move shadow plugin: %w
What it means
During Windows plugin shadow-copy, after (re)moving a stale target, os.Rename of the freshly written temp file to the target path failed and the existing target does not verify as a valid copy. Rename on Windows fails when the destination exists and is open in another process, or when source and target are locked.
Source
Thrown at internal/pluginhost/loader_windows.go:176
return "", errClose
}
digest := hex.EncodeToString(hasher.Sum(nil))
target := shadowPluginPath(dir, file.ID, digest, filepath.Ext(source))
if shadowPluginMatches(target, size, digest) {
return target, nil
}
if errRemove := os.Remove(target); errRemove != nil && !errors.Is(errRemove, os.ErrNotExist) {
if shadowPluginMatches(target, size, digest) {
return target, nil
}
removeShadowPlugin(target)
return "", fmt.Errorf("remove stale shadow plugin: %w", errRemove)
}
if errRename := os.Rename(tmpName, target); errRename != nil {
if shadowPluginMatches(target, size, digest) {
return target, nil
}
return "", fmt.Errorf("move shadow plugin: %w", errRename)
}
removeTemp = false
return target, nil
}
func shadowPluginDir() (string, error) {
dir := filepath.Join(os.TempDir(), "cliproxy-pluginhost", shadowPluginProcessDirName(os.Getpid()))
if errMkdir := os.MkdirAll(dir, 0o700); errMkdir != nil {
return "", errMkdir
}
return dir, nil
}
func shadowPluginProcessDirName(pid int) string {
return fmt.Sprintf("%s%d", shadowPluginProcessDirPrefix, pid)
}
func removeShadowPlugin(path string) {View on GitHub (pinned to 78f0c4079e)
Solutions
- Terminate duplicate/lingering server processes so only one owns the shadow directory
- Free space in %TEMP% and clear the cliproxy-pluginhost directory when the server is stopped
- Retry the load after a short delay (locks from scanners are usually transient)
- Exclude the plugin temp dir from real-time antivirus scanning
Defensive patterns
Strategy: retry
Try / catch
err := retryWithBackoff(3, func() error {
_, e := shadowCopyPlugin(file)
return e
}) Prevention
- Avoid rapid plugin reload churn while requests are in flight
- Keep %TEMP% volume from filling (shadow copies need space)
- Kill orphaned processes before deploys
When it happens
Trigger: The shadow DLL is currently mapped by this or another process (Windows does not allow overwriting a loaded DLL — which is why shadow-copy exists, but the same-name target can still be locked by a different PID reuse); antivirus holding the new temp file; disk-full on the temp volume.
Common situations: Rapid plugin reload churn while requests are in flight; two server processes racing in the same shadow dir; low disk space in %TEMP%.
Related errors
- remove stale shadow plugin: %w
- cliproxy_plugin_init returned %d: %v
- plugin ABI version %d is not supported
- plugin function table is incomplete
- plugin client is closed
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/87d00ed841a82f74.
Report an issue: GitHub.