golang/go · error

updates to go.mod needed, but go.mod is part of the overlay

Error message

updates to go.mod needed, but go.mod is part of the overlay specified with -overlay

What it means

With `-overlay`, the go command reads go.mod from a substitute file. If the build determines go.mod needs to be rewritten (dirty=true) but go.mod itself is part of the overlay (fsys.Replaced returns true), it refuses to write because it cannot propagate changes back into an overlay-mapped file. The command aborts to avoid a silently stale go.mod.

Source

Thrown at src/cmd/go/internal/modload/init.go:2055

	if !dirty && cfg.CmdName != "mod tidy" {
		// The go.mod file has the same semantic content that it had before
		// (but not necessarily the same exact bytes).
		// Don't write go.mod, but write go.sum in case we added or trimmed sums.
		// 'go mod init' shouldn't write go.sum, since it will be incomplete.
		if cfg.CmdName != "mod init" {
			if err := ld.Fetcher().WriteGoSum(ctx, keepSums(ld, ctx, ld.pkgLoader, ld.requirements, addBuildListZipSums), mustHaveCompleteRequirements(ld)); err != nil {
				return err
			}
		}
		return nil
	}

	mainModule := ld.MainModules.mustGetSingleMainModule(ld)
	modFilePath := modFilePath(ld.MainModules.ModRoot(mainModule))
	if fsys.Replaced(modFilePath) {
		if dirty {
			return errors.New("updates to go.mod needed, but go.mod is part of the overlay specified with -overlay")
		}
		return nil
	}
	defer func() {
		// At this point we have determined to make the go.mod file on disk equal to new.
		ld.MainModules.SetIndex(mainModule, indexModFile(updatedGoMod, modFile, mainModule, false))

		// Update go.sum after releasing the side lock and refreshing the index.
		// 'go mod init' shouldn't write go.sum, since it will be incomplete.
		if cfg.CmdName != "mod init" {
			if err == nil {
				err = ld.Fetcher().WriteGoSum(ctx, keepSums(ld, ctx, ld.pkgLoader, ld.requirements, addBuildListZipSums), mustHaveCompleteRequirements(ld))
			}
		}
	}()

	// Make a best-effort attempt to acquire the side lock, only to exclude
	// previous versions of the 'go' command from making simultaneous edits.

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Remove the go.mod entry from the overlay JSON so the real on-disk file is used.
  2. Run the command without `-overlay` when it needs to update go.mod.
  3. Force read-only mode (-mod=readonly) so no go.mod write is attempted.

Example fix

// before overlay.json
// {"Replace": {"/mod/go.mod": "/tmp/generated-go.mod"}}
//   cmd: go get -overlay overlay.json example.com/x@latest

// after overlay.json (go.mod removed)
// {"Replace": {}}
//   cmd: go get example.com/x@latest
Defensive patterns

Strategy: validation

Validate before calling

// Ensure go.mod is NOT covered by the -overlay before running a mutating command.
func overlayExcludesGoMod(overlayPath string) error {
    b, err := os.ReadFile(overlayPath)
    if err != nil { return err }
    if bytes.Contains(b, []byte("go.mod")) {
        return fmt.Errorf("overlay includes go.mod; remove it before go get/mod tidy")
    }
    return nil
}

Type guard

null

Try / catch

null

Prevention

When it happens

Trigger: Any command that mutates go.mod (go get, go mod tidy/edit, build with -mod=mod) run together with `-overlay=<file>` whose JSON maps the module's go.mod path to another file.

Common situations: Editor/LSP/gopls or build tools that build a synthetic overlay and accidentally include go.mod; CI that overlays the whole module directory.

Related errors


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