tailscale/tailscale · error

cmd/tailscale version %q does not match tailscaled version %

Error message

cmd/tailscale version %q does not match tailscaled version %q

What it means

Before delegating a self-update to the CLI, tailscaled compares the CLI's reported long version string against its own version.Long() and refuses when they differ. `tailscale update` must update the exact package/install the daemon belongs to, so a mismatched pair is a hard stop.

Source

Thrown at feature/clientupdate/clientupdate.go:453

		}
	}()

	cmdTS, err := findCmdTailscale()
	if err != nil {
		return fmt.Errorf("failed to find cmd/tailscale binary: %w", err)
	}
	var ver struct {
		Long string `json:"long"`
	}
	out, err := exec.Command(cmdTS, "version", "--json").Output()
	if err != nil {
		return fmt.Errorf("failed to find cmd/tailscale binary: %w", err)
	}
	if err := json.Unmarshal(out, &ver); err != nil {
		return fmt.Errorf("invalid JSON from cmd/tailscale version --json: %w", err)
	}
	if ver.Long != version.Long() {
		return fmt.Errorf("cmd/tailscale version %q does not match tailscaled version %q", ver.Long, version.Long())
	}

	cmd := tailscaleUpdateCmd(cmdTS)
	buf := new(bytes.Buffer)
	cmd.Stdout = buf
	cmd.Stderr = buf
	e.logf("%s: running %q", logPrefix, strings.Join(cmd.Args, " "))
	if err := cmd.Start(); err != nil {
		return fmt.Errorf("failed to start cmd/tailscale update: %w", err)
	}

	go func() {
		if err := cmd.Wait(); err != nil {
			e.logf("%s: update command failed: %v, output: %s", logPrefix, err, buf)
		} else {
			e.logf("%s: update attempt complete", logPrefix)
		}
		e.setC2NUpdateStarted(false)

View on GitHub (pinned to 6e0912f979)

Solutions

  1. Run `tailscale version` and `tailscaled --version` side by side and compare the long strings
  2. Upgrade or reinstall both binaries together from one release (apt/dnf upgrade, brew upgrade, MSI, official tarball)
  3. Restart tailscaled after upgrading so the running daemon matches its on-disk binary
  4. For source builds, rebuild and install cmd/tailscale and cmd/tailscaled from the same commit and flags
Defensive patterns

Strategy: validation

Validate before calling

out, _ := exec.Command(cmdTS, "version", "--json").Output()
var v struct{ Long string `json:"long"` }
if json.Unmarshal(out, &v) == nil && v.Long != version.Long() {
	return fmt.Errorf("refusing update: CLI %s != daemon %s; upgrade both binaries together", v.Long, version.Long())
}

Try / catch

if err := updater.Update(...); err != nil {
	if strings.Contains(err.Error(), "does not match tailscaled version") {
		// surface an 'upgrade both binaries / restart daemon' instruction to the operator
	}
}

Prevention

When it happens

Trigger: ver.Long != version.Long() after parsing the CLI's JSON: tailscaled is 1.66.0 while /usr/bin/tailscale is 1.72.0; one binary was replaced but not the other; dev builds whose long strings differ (different commit, `-dev`/dirty suffixes, different build tags).

Common situations: Half-completed upgrades where the package manager replaced tailscaled but not tailscale (or the daemon was not restarted); mixing tarball installs in /usr/local with distro packages in /usr; building one binary from source and keeping the other from a release.

Related errors


AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18). Data as JSON: /api/errors/fd8b716770cc4dd3. Report an issue: GitHub.