golang/go · error

replacement directory %s: %w

Error message

replacement directory %s: %w

What it means

Sibling of 1063: a local replace directory failed fsys.Stat for a reason OTHER than os.ErrNotExist (e.g. permission denied, I/O error). The original error is wrapped with %w so it can be unwrapped by callers. Carried through module.VersionError so the offending module version is identified.

Source

Thrown at src/cmd/go/internal/modload/import.go:811

			dir = r.Path
			if !filepath.IsAbs(dir) {
				dir = filepath.Join(replaceRelativeTo(ld), dir)
			}
			// Ensure that the replacement directory actually exists:
			// dirInModule does not report errors for missing modules,
			// so if we don't report the error now, later failures will be
			// very mysterious.
			if _, err := fsys.Stat(dir); err != nil {
				// TODO(bcmills): We should also read dir/go.mod here and check its Go version,
				// and return a gover.TooNewError if appropriate.

				if os.IsNotExist(err) {
					// Semantically the module version itself “exists” — we just don't
					// have its source code. Remove the equivalence to os.ErrNotExist,
					// and make the message more concise while we're at it.
					err = fmt.Errorf("replacement directory %s does not exist", r.Path)
				} else {
					err = fmt.Errorf("replacement directory %s: %w", r.Path, err)
				}
				return dir, true, module.VersionError(mod, err)
			}
			return dir, true, nil
		}
		mod = r
	}

	if mustHaveSums(ld) && !modfetch.HaveSum(ld.Fetcher(), mod) {
		return "", false, module.VersionError(mod, &sumMissingError{})
	}

	dir, err = ld.Fetcher().Download(ctx, mod)
	return dir, false, err
}

// mustHaveSums reports whether we require that all checksums
// needed to load or build packages are already present in the go.sum file.

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Check permissions on every component of the path: chmod/chown so the Go process can stat it.
  2. Repair or remove dangling symlinks in the replace path.
  3. Verify the volume/mount holding the target is actually mounted in CI containers.
  4. Point the replace at a readable location, or switch to a versioned replace.

Example fix

// before
replace example.com/foo => /mnt/foo   // /mnt/foo: permission denied
// error: replacement directory /mnt/foo: permission denied

// after
$ sudo chown -R $USER /mnt/foo    // or fix the mount / mode
$ go build ./...
Defensive patterns

Strategy: validation

Validate before calling

// Validate local replace targets are stat-able (not just present).
data, _ := os.ReadFile("go.mod")
f, _ := modfile.Parse("go.mod", data, nil)
for _, r := range f.Replace {
    if r.New.Version == "" {
        if _, err := os.Stat(r.New.Path); err != nil && !os.IsNotExist(err) {
            return fmt.Errorf("replace target %s unreadable: %w", r.New.Path, err)
        }
    }
}

Try / catch

out, err := exec.Command("go", "build", "./...").CombinedOutput()
if err != nil && bytes.Contains(out, []byte("replacement directory")) {
    // Could be 1063 (missing) or 1064 (perm/io). Surface wrapped cause.
    var pathErr *os.PathError
    if errors.As(errors.Unwrap(err), &pathErr) {
        log.Printf("fix permissions on %s: %v", pathErr.Path, pathErr.Err)
    }
    return err
}
return err

Prevention

When it happens

Trigger: go.mod has a local replace directive and fsys.Stat(dir) returns a non-NotExist error: EACCES (permission denied), EIO, a broken symlink, or a path crossing an unreadable mount.

Common situations: Replace target on a mounted volume that is not mounted; file mode / ownership blocks the Go process; dangling symlink in the replace path; container where the bind-mount is missing.

Related errors


AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12). Data as JSON: /api/errors/086efda56542383f. Report an issue: GitHub.