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

  1. Terminate duplicate/lingering server processes so only one owns the shadow directory
  2. Free space in %TEMP% and clear the cliproxy-pluginhost directory when the server is stopped
  3. Retry the load after a short delay (locks from scanners are usually transient)
  4. 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

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


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/87d00ed841a82f74. Report an issue: GitHub.