hashicorp/terraform · critical

global cache directory %s must not match the installation ta

Error message

global cache directory %s must not match the installation target directory %s

What it means

Installer.SetGlobalCacheDir panics when the global cache directory resolves to the same filesystem path as the installer's target directory (internal/providercache/installer.go:118). This is a deliberate early guard: if the read-through cache and the install target overlap, downstream install/copy operations would recurse and potentially destroy the cache contents. The check uses copydir.SameFile and only panics when both paths are stat-identical and no error occurred.

Source

Thrown at internal/providercache/installer.go:118

// ProviderSource returns the getproviders.Source that the installer would
// use for installing any new providers.
func (i *Installer) ProviderSource() getproviders.Source {
	return i.source
}

// SetGlobalCacheDir activates a second tier of caching for the receiving
// installer, with the given directory used as a read-through cache for
// installation operations that need to retrieve new packages.
//
// The global cache directory for an installer must never be the same as its
// target directory, and must not be used as one of its provider sources.
// If these overlap then undefined behavior will result.
func (i *Installer) SetGlobalCacheDir(cacheDir *Dir) {
	// A little safety check to catch straightforward mistakes where the
	// directories overlap. Better to panic early than to do
	// possibly-distructive actions on the cache directory downstream.
	if same, err := copydir.SameFile(i.targetDir.baseDir, cacheDir.baseDir); err == nil && same {
		panic(fmt.Sprintf("global cache directory %s must not match the installation target directory %s", cacheDir.baseDir, i.targetDir.baseDir))
	}
	i.globalCacheDir = cacheDir
}

// SetGlobalCacheDirMayBreakDependencyLockFile activates or deactivates our
// temporary exception to the rule that the global cache directory can be used
// only when entries are confirmed by existing entries in the dependency lock
// file.
//
// If this is set then if we install a provider for the first time from the
// cache then the dependency lock file will include only the checksum from
// the package in the global cache, which means the lock file won't be portable
// to Terraform running on another operating system or CPU architecture.
func (i *Installer) SetGlobalCacheDirMayBreakDependencyLockFile(mayBreak bool) {
	i.globalCacheDirMayBreakDependencyLockFile = mayBreak
}

// HasGlobalCacheDir returns true if someone has previously called

View on GitHub (pinned to c9def3e214)

Solutions

  1. Point the global cache directory at a different path than the installation target (e.g. keep TF_PLUGIN_CACHE_DIR separate from .terraform/providers).
  2. Before calling SetGlobalCacheDir, verify with copydir.SameFile(target.baseDir, cache.baseDir) and skip or error instead of panicking.
  3. Resolve symlinks for both paths (filepath.EvalSymlinks) and compare to catch indirect collisions.
  4. If cloning an Installer via Clone(targetDir), ensure the new targetDir is distinct from the inherited globalCacheDir before any reconfiguration.

Example fix

// before
installer.SetGlobalCacheDir(targetDir) // same dir as install target -> panic

// after
if same, _ := copydir.SameFile(targetDir.baseDir, cacheDir.baseDir); same {
    return fmt.Errorf("cache dir %s must differ from target %s", cacheDir.baseDir, targetDir.baseDir)
}
installer.SetGlobalCacheDir(cacheDir) // distinct dir
Defensive patterns

Strategy: validation

Validate before calling

import (
    "fmt"
    "github.com/hashicorp/terraform/internal/copydir"
)
func safeSetGlobalCacheDir(i *providercache.Installer, cacheDir *providercache.Dir) error {
    td := i.TargetDir() // assumes a getter exposes targetDir.baseDir
    if same, err := copydir.SameFile(td, cacheDir.baseDir); err == nil && same {
        return fmt.Errorf("global cache dir %s must differ from target %s", cacheDir.baseDir, td)
    }
    i.SetGlobalCacheDir(cacheDir)
    return nil
}

Type guard

func dirsAreDistinct(a, b string) (bool, error) {
    ra, err := filepath.EvalSymlinks(a)
    if err != nil { return false, err }
    rb, err := filepath.EvalSymlinks(b)
    if err != nil { return false, err }
    same, err := copydir.SameFile(ra, rb)
    return !same, err
}

Try / catch

defer func() {
    if r := recover(); r != nil {
        err = fmt.Errorf("SetGlobalCacheDir rejected overlapping dirs: %v", r)
    }
}()
i.SetGlobalCacheDir(cacheDir)

Prevention

When it happens

Trigger: Calling installer.SetGlobalCacheDir(cacheDir) where cacheDir.baseDir and installer.targetDir.baseDir point to the same directory (e.g. both constructed from the same path string or symlink-resolved to the same inode). Common when a CLI flag/env var feeds both the plugin install target and the shared cache location.

Common situations: Setting TF_PLUGIN_CACHE_DIR to the same value as the managed plugin directory (e.g. .terraform/providers); CLI wrappers or wrappers like tfenv/tenv that reuse one directory for both roles; symlinked paths that resolve to the same real directory; cloning an installer and forgetting to override the target before reassigning the cache.

Related errors


AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07). Data as JSON: /api/errors/4a43b0b917c90feb. Report an issue: GitHub.