skip to content

Which vcs build settings does the go command stamp into a binary, and when are they missing?

level: middleimportance: should knowfreq 34%

answer

  1. four keys share one prefix
  2. the go command asks the repository
  3. one of them is a dirty-tree flag
  4. the timestamp belongs to the commit
  5. no repository metadata, no stamp

basics

~20 s

The go command stamps four build settings: vcs (the system, such as git), vcs.revision (the commit), vcs.time (that commit's timestamp) and vcs.modified (true when tracked files were edited). They are absent when the build is not from a supported working tree or -buildvcs=false was passed.

solid answer

~40 s

When it builds from a repository, the `go` command records four entries in `BuildInfo.Settings`: `vcs` names the system, `vcs.revision` is the commit hash, `vcs.time` is that **commit's** timestamp, and `vcs.modified` is `true` when tracked files differed from the commit at build time. Stamping happens only when the main package and its main module sit inside a supported version-control working tree and the tool is usable; `-buildvcs` defaults to `auto`, so the go command silently omits the settings when it cannot get them, and `-buildvcs=false` turns stamping off outright. So a build from an unpacked source tarball, or from a checkout with no repository metadata, carries no revision at all. Note what is never recorded: the wall-clock build time. Go leaves the clock out so that two builds of the same source stay byte-identical.

code

go · 11 lines
go
func vcsInfo(info *debug.BuildInfo) (revision string, dirty bool) {
	for _, s := range info.Settings {
		switch s.Key {
		case "vcs.revision":
			revision = s.Value
		case "vcs.modified":
			dirty = s.Value == "true"
		}
	}
	return revision, dirty
}

go deeper

for a junior

Recall that version-control data lands in the build settings under keys beginning with vcs, and that the revision is a commit hash rather than a release number.

for a middle

Explain the four keys, that vcs.time is the commit's timestamp, and the conditions under which stamping happens at all, including what -buildvcs controls.

for a senior

Show that you check for the settings in the release path rather than assuming them, and that you treat a dirty-tree stamp as a source you can no longer reconstruct.

for a principal

Decide what the organisation requires of a published artifact's provenance, and who absorbs the cost of build environments that cannot stamp it.

## The four keys Version-control data does not get its own struct field — it arrives as ordinary entries in `BuildInfo.Settings`, which is a `[]debug.BuildSetting` of `Key`/`Value` string pairs: - **`vcs`** — the system that was detected, e.g. `git`. - **`vcs.revision`** — the full commit identifier the build came from. - **`vcs.time`** — the timestamp **of that commit**, in RFC 3339 form. - **`vcs.modified`** — `"true"` or `"false"`: whether tracked files in the working tree differed from that commit when the build ran. Because `Settings` is a slice you walk it and switch on `Key`; there is no map lookup and no guarantee a key is present. ## When stamping happens The `go` command stamps this data only when it can honestly derive it. The conditions are narrow: the main package being built, and the main module containing it, must live inside a working tree of a supported version-control system, and the corresponding tool must be usable in that environment. The `-buildvcs` flag controls the behaviour and defaults to `auto` — meaning stamp when possible, and quietly proceed without the settings when not. `-buildvcs=false` disables it, which is a common workaround in stripped-down build environments where the repository metadata was never copied in, the tool is not installed, or the tool refuses the directory as untrusted because it is owned by another user. So the realistic ways a shipped binary ends up with no revision are all environmental, not code problems: - built from an unpacked source archive with no repository metadata present; - built in a minimal environment with no version-control tool available; - built with `-buildvcs=false`, sometimes added long ago to silence a build error; - the main package sits outside the module's repository. ## vcs.modified is repo-wide and it matters `vcs.modified=true` does not mean the binary was tampered with; it means the source that produced it does not exist as a commit anywhere. The commit hash beside it is then only an approximate answer — it identifies what the tree was *based on*, plus an unknown edit. It also reflects the state of tracked files in the working tree generally, not only the files that ended up compiled into this particular binary, so an unrelated stray edit will set it. For anything published, that flag is the difference between "we can rebuild exactly this" and "we cannot". It is worth reading in the release job rather than at 3am. ## The one thing Go will not record: build time `vcs.time` is frequently misread as the moment the binary was produced. It is not; it is the commit's own timestamp, and it is identical for every rebuild of that commit. Go records no wall-clock build time at all, and that is deliberate: a clock reading embedded in the output would make two builds of identical source differ byte for byte, defeating reproducible builds and cache reuse. If the organisation needs a build time, a pipeline identifier, a release channel or an environment name, those are facts about the build *event* rather than the source, and the build has to inject them at link time — the toolchain will not. ## Reading them From inside the process, walk `Settings` after a successful `debug.ReadBuildInfo()`. From outside, `go version -m` on the file prints the same `vcs.*` lines, which is how you check what a stamped artifact actually got before shipping it rather than after. A useful habit for a CLI tool's `version` output is to print the revision and, when `vcs.modified` is `true`, say so loudly — appending a marker such as `+dirty` to the revision, so nobody quotes a clean-looking hash that never existed as a build.

  • Why does Go record the commit time but never the build time?
    Reproducibility. A wall-clock reading baked into the output would make two builds of identical source differ byte for byte and defeat build caching. The commit time is a property of the source, so it is stable across rebuilds. A build timestamp, if you need one, has to be injected at link time by the build itself.
  • What does -buildvcs=false change, and why do build environments end up using it?
    It disables version-control stamping entirely, so no `vcs.*` settings are recorded. Teams reach for it when repository metadata is not present in the build environment, the tool is missing, or the tool rejects the checkout as untrusted because it is owned by a different user. The default `auto` omits the data silently instead of failing.
  • Does -trimpath remove the vcs settings?
    No. `-trimpath` removes filesystem paths from the binary and is itself recorded as a build setting; the `vcs.*` entries are unaffected. Confusing the two leads people to disable a useful flag while chasing a missing revision that was never stamped in the first place.

saying these in an interview costs you the question

  • Reads vcs.time as the time the binary was built
  • Thinks vcs.modified means the binary was tampered with
  • Assumes every Go build carries vcs.revision
  • Believes -trimpath strips the version-control settings
  • Expects Settings to be a map keyed by setting name