skip to content

A shipped Go binary's version output shows no commit hash. How do you establish what it was built from?

level: seniorimportance: should knowfreq 30%

answer

  1. ask the file, not the pipeline
  2. one go subcommand dumps the record
  3. -m against the shipped artifact
  4. absent is not the same as stripped
  5. stripping symbols leaves the record

basics

~20 s

Run go version -m against the artifact itself. It prints the embedded record straight from the file: toolchain, main module, linked modules and build settings. An absent vcs.revision means the build was never stamped, not that the data was stripped later.

solid answer

~50 s

Interrogate the artifact, not the pipeline. `go version -m ./tool` dumps the embedded record from the file on disk — toolchain version, main module, every linked module, and the build settings including `vcs.*`. Three outcomes matter. If the record is there but has no `vcs.*` keys, nothing was stripped: the build simply was not stamped, because it ran outside a working tree, without the version-control tool available, or with `-buildvcs=false`. If `vcs.revision` is present with `vcs.modified=true`, you have the base commit plus an unknown edit, so the source cannot be reconstructed. If there is no record at all, the binary did not come from the `go` command in module mode. Worth knowing: linking with `-s -w` strips symbols and debug data but leaves the build record intact, so "we stripped it" is rarely the explanation.

code

text · 11 lines
text
$ go version -m ./release/tool
./release/tool: go1.27.0
	path	example.com/tool/cmd/tool
	mod	example.com/tool	(devel)
	build	-buildmode=exe
	build	-trimpath=true
	build	GOOS=linux
	build	vcs=git
	build	vcs.revision=9f1c0a2b4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f90
	build	vcs.time=2026-08-14T09:12:03Z
	build	vcs.modified=true

go deeper

for a junior

Know that a built Go binary can be asked what it is, using go version -m on the file, and that the answer may legitimately contain no commit at all.

for a middle

Explain the difference between a record that was never stamped and one that is absent entirely, and name what does and does not remove it.

for a senior

Walk the diagnosis in order: inspect the shipped artifact, read the settings for build-path clues, treat a dirty stamp as an unreconstructable source, and reconcile the running process against what was published.

for a principal

Decide what evidence a release must leave behind so this investigation is never needed, and what the team gives up to guarantee it.

## Ask the artifact The release engineer's question months later is narrow: *what source produced this exact file?* The build logs may be expired, the tag may have moved, the pipeline may have been rewritten twice. The binary, however, still carries whatever the `go` command wrote into it, and it carries it wherever the file was copied. ``` go version -m ./release/tool ``` That prints the same record `debug.ReadBuildInfo` would return inside the process: the toolchain version, the main package path, the main module, each linked dependency module with its selected version and checksum, and every build setting. Do it against the artifact you actually shipped — the one in the release store — not against a fresh rebuild, because a rebuild answers a different question. ## Reading the three outcomes **A record with no `vcs.*` keys.** This is the common case and it is not a stripping problem. The `go` command stamps version-control data only when the main package and its module sit in a supported working tree and the tool is usable, and `-buildvcs` defaults to `auto`, which omits the data silently rather than failing. So the build ran from an unpacked source archive, or in an environment with no repository metadata or no version-control tool, or someone added `-buildvcs=false` months ago to get past a build error. Look at the other settings in the same dump: `-trimpath`, `-tags`, `GOOS`, `GOARCH`, `CGO_ENABLED` and the `-ldflags` value often identify which build path produced the file even when the revision is missing. **`vcs.revision` present with `vcs.modified=true`.** Now the hash is a lead, not an answer. It says the tree was based on that commit and then edited. Checking out that commit gives you something close to, but demonstrably not, the source that was compiled. For a production incident this is the worst of the three outcomes because it looks trustworthy at a glance. **No record at all** — `ReadBuildInfo` reporting false and `go version -m` printing nothing for the file. That means the executable was not produced by the `go` command in module mode, or the record was destroyed by a post-processing step. Note what does *not* cause this: linking with `-ldflags "-s -w"` removes the symbol table and DWARF debugging data and leaves the build record readable, so blaming stripping is usually wrong. ## Cross-checking the running process If the deployed process reports a different revision from the artifact you are inspecting, then it is not that artifact. The comparison is worth making explicitly: have the process print its own record and diff it against `go version -m` on the file in the release store. A mismatch reframes the incident from "which commit is broken" to "why did we deploy something we did not publish", which is a different investigation entirely. ## Also check the dependencies The record lists the modules that were actually linked in, with resolved versions. When two artifacts built from the same commit behave differently, that list is where the difference shows up — a `replace` target that only exists in one build environment, or a version that resolved differently. ## Closing the hole Diagnosis is one thing; not repeating it is the point. Three changes usually suffice. Make the `version` subcommand print the whole record, marking a dirty build visibly rather than showing a clean-looking hash. Have the release job run `go version -m` on the artifact it is about to publish and store that output alongside it, so the record survives even if the artifact is later lost. And make the job refuse to publish when `vcs.revision` is empty or `vcs.modified` is `true`, which converts a future archaeology dig into a build failure someone fixes in ten minutes.

  • Does linking with -s -w explain a missing build record?
    Almost never. Those link flags remove the symbol table and DWARF debugging information, which costs you a usable debugger and readable panic detail, but the embedded build record is stored separately and `go version -m` still reads it. A genuinely absent record points at a binary the go command did not build in module mode.
  • The deployed process reports a different revision from the artifact in the release store. What does that tell you?
    That the running process is not that artifact. Something in the delivery path shipped a different build — an older copy, a rebuild that was never published, or a promotion that skipped the store. Stop debugging the commit and start reconciling what was published with what was deployed.
  • What would you change so the next incident does not need this archaeology?
    Make the version output print the full record and flag a dirty tree; have the release job run `go version -m` on the exact artifact it publishes and archive that output beside it; and fail the release when `vcs.revision` is empty or `vcs.modified` is true, so the gap surfaces at build time instead of during an incident.

saying these in an interview costs you the question

  • Assumes -s -w stripped the build record
  • Rebuilds from the tag and assumes it matches the artifact
  • Trusts a hash recorded beside vcs.modified=true
  • Inspects a fresh build instead of the shipped file
  • Concludes the toolchain lost the revision rather than never stamping it