router-for-me/CLIProxyAPI · error

create plugin directory: %w

Error message

create plugin directory: %w

What it means

Wrapped error from os.MkdirAll inside writeFileAtomic (install.go:520-523). Before writing the plugin library atomically, the installer creates the target directory tree (<PluginsDir>/<GOOS>/<GOARCH>). Failure means the filesystem refused the mkdir: permission denied, a path component that is a file, read-only mount, or a nonexistent parent on some network filesystems.

Source

Thrown at internal/pluginstore/install.go:522

	}
	return pluginFileInfo{ID: id, Path: filePath, Version: version}, true
}

func pluginExtension(goos string) string {
	switch strings.ToLower(strings.TrimSpace(goos)) {
	case "darwin", "mac", "macos", "osx":
		return ".dylib"
	case "windows":
		return ".dll"
	default:
		return ".so"
	}
}

func writeFileAtomic(targetPath string, data []byte, mode os.FileMode) error {
	targetDir := filepath.Dir(targetPath)
	if errMkdir := os.MkdirAll(targetDir, 0o755); errMkdir != nil {
		return fmt.Errorf("create plugin directory: %w", errMkdir)
	}

	temp, errTemp := os.CreateTemp(targetDir, "."+filepath.Base(targetPath)+".tmp-*")
	if errTemp != nil {
		return fmt.Errorf("create temp plugin file: %w", errTemp)
	}
	tempPath := temp.Name()
	removeTemp := true
	closed := false
	defer func() {
		if !closed {
			if errClose := temp.Close(); errClose != nil {
				log.WithError(errClose).Debug("failed to close temp plugin file")
			}
		}
		if removeTemp {
			if errRemove := os.Remove(tempPath); errRemove != nil && !errors.Is(errRemove, os.ErrNotExist) {
				log.WithError(errRemove).Debug("failed to remove temp plugin file")

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Check permissions on every component of PluginsDir and grant write access to the running user (chown/chmod)
  2. Confirm no regular file shadows a needed directory component; remove or relocate it
  3. Point PluginsDir at a writable location (e.g. under the config dir or a writable volume) and retry

Example fix

# before
ls -la /opt/cliproxy/plugins  # root:root drwxr-xr-x, server runs as 'app'
# after
chown -R app:app /opt/cliproxy/plugins
Defensive patterns

Strategy: try-catch

Validate before calling

func pluginsDirWritable(pluginsDir string) error {
    probe := filepath.Join(pluginsDir, ".write-probe")
    if err := os.MkdirAll(pluginsDir, 0o755); err != nil {
        return fmt.Errorf("plugins dir not creatable: %w", err)
    }
    return os.WriteFile(probe, nil, 0o644)
}

Try / catch

if _, err := store.InstallArchive(data, plugin, opts); err != nil {
    if errors.Is(err, os.ErrPermission) { // wrapped through MkdirAll
        log.Errorf("plugins dir %s not writable by uid=%d; fix ownership", opts.PluginsDir, os.Getuid())
    }
}

Prevention

When it happens

Trigger: InstallArchive with options.PluginsDir pointing somewhere the process cannot write (e.g. /usr/local/lib owned by root while running as non-root), or PluginsDir set to a path where an existing regular file occupies a directory component.

Common situations: Running the server under a restricted user or container with a read-only volume mounted at the plugins dir; a stale file left where a directory should be; SELinux/AppArmor denial on the target path.

Related errors


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