opentofu/opentofu · error

The specified plugin cache dir %s cannot be opened: %w

Error message

The specified plugin cache dir %s cannot be opened: %w

What it means

If plugin_cache_dir is set (file or TF_PLUGIN_CACHE_DIR, with os.ExpandEnv applied), validation stats the directory to prove it is usable; any os.Stat failure is wrapped as 'The specified plugin cache dir <dir> cannot be opened'. OpenTofu does not create the directory for you, so a missing path is the most common cause.

Source

Thrown at internal/command/cliconfig/cliconfig.go:407

	if len(c.OCIRepositoryCredentials) != 0 {
		seenOCICredentialsAddrs := make(map[string]struct{})
		for _, creds := range c.OCIRepositoryCredentials {
			if _, ok := seenOCICredentialsAddrs[creds.RepositoryPrefix]; ok {
				diags = diags.Append(
					//nolint:stylecheck // Despite typical Go idiom, our existing precedent here is to return full sentences suitable for inclusion in diagnostics.
					fmt.Errorf("Duplicate oci_credentials block for %q", creds.RepositoryPrefix),
				)
				continue
			}
			seenOCICredentialsAddrs[creds.RepositoryPrefix] = struct{}{}
		}
	}

	if c.PluginCacheDir != "" {
		_, err := os.Stat(c.PluginCacheDir)
		if err != nil {
			diags = diags.Append(
				fmt.Errorf("The specified plugin cache dir %s cannot be opened: %w", c.PluginCacheDir, err),
			)
		}
	}

	return diags
}

// Merge merges two configurations and returns a third entirely
// new configuration with the two merged.
func (c *Config) Merge(c2 *Config) *Config {
	var result Config

	result.PluginCacheDir = c.PluginCacheDir
	if result.PluginCacheDir == "" {
		result.PluginCacheDir = c2.PluginCacheDir
	}

	if c.PluginCacheMayBreakDependencyLockFile || c2.PluginCacheMayBreakDependencyLockFile {

View on GitHub (pinned to 3561785c48)

Solutions

  1. Create the directory and make it writable: mkdir -p <dir> (then confirm with ls -ld)
  2. Replace ~ with an absolute path or a ${HOME} reference, since only $VAR/${VAR} expansion happens
  3. If set via TF_PLUGIN_CACHE_DIR, echo it in your shell/CI to verify the resolved value

Example fix

// before
plugin_cache_dir = "~/.tofu.d/plugin-cache"

// after
plugin_cache_dir = "${HOME}/.tofu.d/plugin-cache"
// and once in a shell: mkdir -p ~/.tofu.d/plugin-cache
Defensive patterns

Strategy: validation

Validate before calling

func pluginCacheDirUsable(dir string) error {
	if dir == "" {
		return nil
	}
	info, err := os.Stat(os.ExpandEnv(dir)) // mirrors loader behavior
	if err != nil {
		return err
	}
	if !info.IsDir() {
		return fmt.Errorf("%s is not a directory", dir)
	}
	return nil
}

Prevention

When it happens

Trigger: plugin_cache_dir points at a path that does not exist, is unreadable by the current user, or is a dangling symlink; the value contains '~' which ExpandEnv does not expand; an env var referenced via ${VAR} is empty, producing a wrong path.

Common situations: Copying plugin_cache_dir = "~/.terraform.d/plugin-cache" from docs (tilde is not expanded); setting TF_PLUGIN_CACHE_DIR in CI where the cache volume is mounted elsewhere or with wrong ownership; read-only cache dir owned by root.

Related errors


AI-assisted analysis of opentofu/opentofu@3561785c48 (2026-08-15). Data as JSON: /api/errors/e05876cafda61476. Report an issue: GitHub.