router-for-me/CLIProxyAPI · error
remove stale shadow plugin: %w
Error message
remove stale shadow plugin: %w
What it means
Before loading a Windows plugin, the host shadow-copies it into %TEMP%\cliproxy-pluginhost\<pid>. If an existing shadow file with the expected digest path is stale (digest mismatch) and os.Remove fails (and the file on disk does not verify as a valid copy), this error surfaces the underlying remove failure. On Windows this almost always means the file is locked by a running process.
Source
Thrown at internal/pluginhost/loader_windows.go:170
size, errCopy := io.Copy(io.MultiWriter(tmp, hasher), in)
if errCopy != nil {
_ = tmp.Close()
return "", errCopy
}
if errClose := tmp.Close(); errClose != nil {
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
}View on GitHub (pinned to 78f0c4079e)
Solutions
- Stop all running instances of the server (check Task Manager for lingering processes) and retry
- Delete the stale shadow directory %TEMP%\cliproxy-pluginhost manually once no process holds it
- Add an antivirus exclusion for the cliproxy-pluginhost temp directory or the plugin artifact
- Remove read-only attributes from the temp directory contents
Defensive patterns
Strategy: retry
Try / catch
var target string
err := retry(3, time.Second, func() error {
var e error
target, e = shadowCopyPlugin(file)
return e
}) // file locks are often transient Prevention
- Ensure only one server instance runs per shadow dir; stop old processes before starting new ones
- Exclude %TEMP%\cliproxy-pluginhost from antivirus real-time scanning
- Clean the shadow dir in deployment scripts while the server is stopped
When it happens
Trigger: A previous instance of the server (or an antivirus scanner, indexer, or debugger) still holds the shadow DLL open; two server processes sharing the same PID directory after a PID reuse; the shadow file has read-only attributes.
Common situations: Old server process not fully exited when a new one starts; antivirus quarantining or locking freshly written DLLs; crash leaving a zombie process holding the handle.
Related errors
- move 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/cc545c42c95f97c6.
Report an issue: GitHub.