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 calledView on GitHub (pinned to c9def3e214)
Solutions
- Point the global cache directory at a different path than the installation target (e.g. keep TF_PLUGIN_CACHE_DIR separate from .terraform/providers).
- Before calling SetGlobalCacheDir, verify with copydir.SameFile(target.baseDir, cache.baseDir) and skip or error instead of panicking.
- Resolve symlinks for both paths (filepath.EvalSymlinks) and compare to catch indirect collisions.
- 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
- Keep TF_PLUGIN_CACHE_DIR and the managed plugin target (.terraform/providers) on distinct paths.
- Resolve symlinks before comparing, since SameFile follows to the same inode.
- After Installer.Clone(targetDir), confirm the new target differs from the inherited cache dir.
- Wrap SetGlobalCacheDir in a helper that validates distinctness up front.
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
- default workspace not supported You can create a new workspa
- error deleting workspace %s: %w
- failed to write state: %w
- Failed to open script '%s': %v
- backend does not support key/value tags. Try using key-only
AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07).
Data as JSON: /api/errors/4a43b0b917c90feb.
Report an issue: GitHub.