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

  1. Stop all running instances of the server (check Task Manager for lingering processes) and retry
  2. Delete the stale shadow directory %TEMP%\cliproxy-pluginhost manually once no process holds it
  3. Add an antivirus exclusion for the cliproxy-pluginhost temp directory or the plugin artifact
  4. 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

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


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