router-for-me/CLIProxyAPI · error
stat target plugin: %w
Error message
stat target plugin: %w
What it means
Wrapped error from InstallArchive when os.Stat on the computed target path fails with anything other than os.ErrNotExist — i.e. the existence check itself is broken, typically a permission problem on a parent directory or a path component that is not a directory. Only a clean 'does not exist' is treated as 'will create'; ambiguous stat failures abort the install rather than risk overwriting blindly.
Source
Thrown at internal/pluginstore/install.go:272
reader, errZip := zip.NewReader(bytes.NewReader(archiveData), int64(len(archiveData)))
if errZip != nil {
return InstallResult{}, fmt.Errorf("open zip: %w", errZip)
}
libraryData, mode, errLibrary := readTargetLibrary(reader, id, version, options.GOOS)
if errLibrary != nil {
return InstallResult{}, errLibrary
}
targetPath, errTarget := installTargetPath(options, id, version)
if errTarget != nil {
return InstallResult{}, errTarget
}
overwritten := false
if _, errStat := os.Stat(targetPath); errStat == nil {
overwritten = true
} else if !errors.Is(errStat, os.ErrNotExist) {
return InstallResult{}, fmt.Errorf("stat target plugin: %w", errStat)
}
if overwritten {
existingData, errReadExisting := os.ReadFile(targetPath)
if errReadExisting != nil {
return InstallResult{}, fmt.Errorf("read target plugin: %w", errReadExisting)
}
if bytes.Equal(existingData, libraryData) {
return InstallResult{
ID: id,
Version: strings.TrimSpace(plugin.Version),
Path: targetPath,
Overwritten: true,
Skipped: true,
}, nil
}
}
// Re-check immediately before replacing an existing file: the same version
// may have been loaded while the archive was being downloaded and verified.View on GitHub (pinned to 78f0c4079e)
Solutions
- Check the underlying stat error (errors.Is(errStat, fs.ErrPermission) etc.) to identify the exact directory in the path that denies access
- Ensure options.PluginsDir and its GOOS/GOARCH subdirectories exist and are writable/searchable by the process user
- Remove or relocate any regular file occupying a needed directory component
Example fix
# before: plugins dir owned by root, process runs as app-user ls -ld /opt/app/plugins # drwx------ root root # after mkdir -p /opt/app/plugins/linux/amd64 && chown -R app-user:app-user /opt/app/plugins
Defensive patterns
Strategy: try-catch
Validate before calling
func targetDirWritable(pluginsDir, goos, goarch string) error {
dir := filepath.Join(pluginsDir, goos, goarch)
info, err := os.Stat(dir)
if err == nil && !info.IsDir() {
return fmt.Errorf("%s exists but is not a directory", dir)
}
if err != nil {
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("cannot create %s: %w", dir, err)
}
return nil
}
probe := filepath.Join(dir, ".write-probe")
if err := os.WriteFile(probe, nil, 0o644); err != nil {
return fmt.Errorf("%s not writable: %w", dir, err)
}
_ = os.Remove(probe)
return nil
} Type guard
func isPermissionErr(err error) bool {
return errors.Is(err, fs.ErrPermission) || strings.Contains(err.Error(), "stat target plugin:") && errors.Is(errors.Unwrap(err), fs.ErrPermission)
} Try / catch
if _, err := pluginstore.InstallArchive(data, plugin, options); err != nil {
if strings.Contains(err.Error(), "stat target plugin:") {
if errors.Is(err, fs.ErrPermission) {
// fix ownership/permissions of PluginsDir tree, then retry
} else if errors.Is(err, syscall.ENOTDIR) {
// a file occupies a directory component; remove or relocate it
}
}
} Prevention
- Create and chown the PluginsDir/<goos>/<goarch> tree at deploy time
- Run all installs as one dedicated user owning the plugins directory
- Write-probe the target directory before starting installs
When it happens
Trigger: options.PluginsDir points into a directory without execute/search permission for the process user, or a non-directory file sits where a directory component is expected (e.g. 'plugins' is a regular file), producing EACCES/ENOTDIR/ELOOP from Stat.
Common situations: Running the installer as a different user than the one that owns the plugins directory; containers with read-only or mismounted volumes; a previous install created a file where a directory path now expects one; overly strict umask or SELinux/AppArmor denials.
Related errors
- read target plugin: %w
- failed to read auth file: %w
- failed to write auth file: %w
- download artifact: %w
- prepare plugin write: %w
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/36535b4211ffd63a.
Report an issue: GitHub.