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
- Run `tailscale version` and `tailscaled --version` side by side and compare the long strings
- Upgrade or reinstall both binaries together from one release (apt/dnf upgrade, brew upgrade, MSI, official tarball)
- Restart tailscaled after upgrading so the running daemon matches its on-disk binary
- 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
- Deploy both binaries atomically from a single release artifact
- Restart tailscaled as part of every upgrade runbook step
- In clusters, drain and upgrade nodes one at a time instead of replacing halves of the pair
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
- malformed version %q
- only one of Version(%q) or Track(%q) can be set
- unsupported track %q
- cannot find Synology package for os=%s arch=%s, please repor
- invalid JSON from cmd/tailscale version --json: %w
AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18).
Data as JSON: /api/errors/fd8b716770cc4dd3.
Report an issue: GitHub.