multica-ai/multica · error

replace binary: %w

Error message

replace binary: %w

What it means

replaceBinary performs the final swap: on Unix a rename over the running inode; on Windows it first moves the running executable aside. Failure is wrapped as 'replace binary: %w' and the temp file removed. This is the last step, so everything before it (download, verify, extract) already succeeded.

Source

Thrown at server/internal/cli/update.go:475

	}
	tmpFile.Close()

	// Preserve original file permissions.
	info, err := os.Stat(exePath)
	if err != nil {
		os.Remove(tmpPath)
		return "", fmt.Errorf("stat original binary: %w", err)
	}
	if err := os.Chmod(tmpPath, info.Mode()); err != nil {
		os.Remove(tmpPath)
		return "", fmt.Errorf("chmod temp file: %w", err)
	}

	// Replace the original binary. On Windows this moves the running executable
	// aside first; on Unix a plain rename over the running inode is fine.
	if err := replaceBinary(tmpPath, exePath); err != nil {
		os.Remove(tmpPath)
		return "", fmt.Errorf("replace binary: %w", err)
	}

	return fmt.Sprintf("Downloaded %s and replaced %s", assetName, exePath), nil
}

// extractBinaryFromTarGz reads a .tar.gz stream and returns the contents of the
// named file entry.
func extractBinaryFromTarGz(r io.Reader, name string) ([]byte, error) {
	gz, err := gzip.NewReader(r)
	if err != nil {
		return nil, fmt.Errorf("gzip reader: %w", err)
	}
	defer gz.Close()

	tr := tar.NewReader(gz)
	for {
		hdr, err := tr.Next()
		if err == io.EOF {

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Retry after a moment — AV locks on the temp file are usually brief, and the poller retries the whole update on its next tick.
  2. Ensure write permission on the binary's directory (rename needs directory write access, not file write access).
  3. Remove the immutable flag if set (lsattr/chattr -i).
  4. On Windows, exclude the install directory from real-time AV scanning or retry the update after the scan completes.

Example fix

null
Defensive patterns

Strategy: retry

Try / catch

out, err := cli.UpdateViaDownload(ver)
if err != nil && strings.HasPrefix(err.Error(), "replace binary") {
    if errors.Is(err, fs.ErrPermission) {
        // need write access to the install DIRECTORY (not the file)
    } else if runtime.GOOS == "windows" {
        // likely AV/another instance holding the exe: retry after delay
        time.Sleep(5 * time.Second)
        out, err = cli.UpdateViaDownload(ver)
    }
}

Prevention

When it happens

Trigger: Windows: the running executable is locked (antivirus scanning it, another instance starting, terminal cwd inside the directory with legacy locking); Unix: EACCES on the directory (no write permission needed for the file, but for the dir), EPERM on immutable-flagged files, or EXDEV if the temp file somehow landed on another filesystem; Text-file-busy (ETXTBSY) on Linux when renaming onto a file being written/executed in edge cases.

Common situations: AV/EDR scanning the freshly written temp file at the exact moment of rename; users running multiple CLI instances concurrently; binaries installed with chattr +i by hardening scripts; Windows SmartScreen holding a handle.

Related errors


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/9b2a7fd95a3dd539. Report an issue: GitHub.