diff --git a/Makefile b/Makefile
index 6086799265e42ec59ffe1fd5df07449936162700..4745d195db612ace950380c2760d0fd3d4217a89 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,3 @@
-all: build
-
-GIT_COMMIT:=$(shell git rev-list -1 HEAD)
-GIT_LAST_TAG:=$(shell git describe --abbrev=0 --tags 2>/dev/null || true)
-GIT_EXACT_TAG:=$(shell git name-rev --name-only --tags HEAD)
 UNAME_S := $(shell uname -s)
 XARGS:=xargs -r
 ifeq ($(UNAME_S),Darwin)
@@ -11,10 +6,10 @@ endif
 
 SYSTEM=$(shell nix eval --impure --expr 'builtins.currentSystem' --raw 2>/dev/null || echo '')
 
-COMMANDS_PATH:=github.com/git-bug/git-bug/commands
-LDFLAGS:=-X ${COMMANDS_PATH}.GitCommit="${GIT_COMMIT}" \
-	-X ${COMMANDS_PATH}.GitLastTag="${GIT_LAST_TAG}" \
-	-X ${COMMANDS_PATH}.GitExactTag="${GIT_EXACT_TAG}"
+TAG:=$(shell git name-rev --name-only --tags HEAD)
+LDFLAGS:=-X main.version="${TAG}"
+
+all: build
 
 .PHONY: list-checks
 list-checks:
diff --git a/commands/root.go b/commands/root.go
index 1a4109a3cc6e057c92d9385258fcf75d1137d2c6..1b64b5090b624f29c923285667ebe7b2d8924174 100644
--- a/commands/root.go
+++ b/commands/root.go
@@ -1,25 +1,17 @@
-// Package commands contains the CLI commands
 package commands
 
 import (
-	"fmt"
 	"os"
-	"runtime/debug"
 
 	"github.com/spf13/cobra"
 
-	bridgecmd "github.com/git-bug/git-bug/commands/bridge"
-	bugcmd "github.com/git-bug/git-bug/commands/bug"
+	"github.com/git-bug/git-bug/commands/bridge"
+	"github.com/git-bug/git-bug/commands/bug"
 	"github.com/git-bug/git-bug/commands/execenv"
-	usercmd "github.com/git-bug/git-bug/commands/user"
+	"github.com/git-bug/git-bug/commands/user"
 )
 
-// These variables are initialized externally during the build. See the Makefile.
-var GitCommit string
-var GitLastTag string
-var GitExactTag string
-
-func NewRootCommand() *cobra.Command {
+func NewRootCommand(version string) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   execenv.RootCommandName,
 		Short: "A bug tracker embedded in Git",
@@ -33,7 +25,7 @@ the same git remote you are already using to collaborate with other people.
 
 		PersistentPreRun: func(cmd *cobra.Command, args []string) {
 			root := cmd.Root()
-			root.Version = getVersion()
+			root.Version = version
 		},
 
 		// For the root command, force the execution of the PreRun
@@ -80,64 +72,3 @@ the same git remote you are already using to collaborate with other people.
 
 	return cmd
 }
-
-func Execute() {
-	if err := NewRootCommand().Execute(); err != nil {
-		os.Exit(1)
-	}
-}
-
-func getVersion() string {
-	if GitExactTag == "undefined" {
-		GitExactTag = ""
-	}
-
-	if GitExactTag != "" {
-		// we are exactly on a tag --> release version
-		return GitLastTag
-	}
-
-	if GitLastTag != "" {
-		// not exactly on a tag --> dev version
-		return fmt.Sprintf("%s-dev-%.10s", GitLastTag, GitCommit)
-	}
-
-	// we don't have commit information, try golang build info
-	if commit, dirty, err := getCommitAndDirty(); err == nil {
-		if dirty {
-			return fmt.Sprintf("dev-%.10s-dirty", commit)
-		}
-		return fmt.Sprintf("dev-%.10s", commit)
-	}
-
-	return "dev-unknown"
-}
-
-func getCommitAndDirty() (commit string, dirty bool, err error) {
-	info, ok := debug.ReadBuildInfo()
-	if !ok {
-		return "", false, fmt.Errorf("unable to read build info")
-	}
-
-	var commitFound bool
-
-	// get the commit and modified status
-	// (that is the flag for repository dirty or not)
-	for _, kv := range info.Settings {
-		switch kv.Key {
-		case "vcs.revision":
-			commit = kv.Value
-			commitFound = true
-		case "vcs.modified":
-			if kv.Value == "true" {
-				dirty = true
-			}
-		}
-	}
-
-	if !commitFound {
-		return "", false, fmt.Errorf("no commit found")
-	}
-
-	return commit, dirty, nil
-}
diff --git a/commands/version.go b/commands/version.go
index 189a2e350f563be8432e9c275544476fb0c143a5..ffa4a180afd47e7a7227a713972241284a1ce745 100644
--- a/commands/version.go
+++ b/commands/version.go
@@ -1,63 +1,66 @@
 package commands
 
 import (
-	"runtime"
+	"log/slog"
 
 	"github.com/spf13/cobra"
 
 	"github.com/git-bug/git-bug/commands/execenv"
 )
 
-type versionOptions struct {
-	number bool
-	commit bool
-	all    bool
-}
+// TODO: 0.12.0: remove deprecated build vars
+var (
+	GitCommit   string
+	GitLastTag  string
+	GitExactTag string
+)
 
 func newVersionCommand(env *execenv.Env) *cobra.Command {
-	options := versionOptions{}
+	return &cobra.Command{
+		Use:     "version",
+		Short:   "Print version information",
+		Example: "git bug version",
+		Long: `
+Print version information.
 
-	cmd := &cobra.Command{
-		Use:   "version",
-		Short: "Show git-bug version information",
-		Run: func(cmd *cobra.Command, args []string) {
-			runVersion(env, options, cmd.Root())
-		},
-	}
+Format:
+  git-bug <version> [commit[/dirty]] <compiler version> <platform> <arch>
 
-	flags := cmd.Flags()
-	flags.SortFlags = false
+Format Description:
+  <version> may be one of:
+  	- A semantic version string, prefixed with a "v", e.g. v1.2.3
+  	- "undefined" (if not provided, or built with an invalid version string)
 
-	flags.BoolVarP(&options.number, "number", "n", false,
-		"Only show the version number",
-	)
-	flags.BoolVarP(&options.commit, "commit", "c", false,
-		"Only show the commit hash",
-	)
-	flags.BoolVarP(&options.all, "all", "a", false,
-		"Show all version information",
-	)
+  [commit], if present, is the commit hash that was checked out during the
+  build. This may be suffixed with '/dirty' if there were local file
+  modifications. This is indicative of your build being patched, or modified in
+  some way from the commit.
 
-	return cmd
-}
+  <compiler version> is the version of the go compiler used for the build.
 
-func runVersion(env *execenv.Env, opts versionOptions, root *cobra.Command) {
-	if opts.all {
-		env.Out.Printf("%s version: %s\n", execenv.RootCommandName, root.Version)
-		env.Out.Printf("System version: %s/%s\n", runtime.GOARCH, runtime.GOOS)
-		env.Out.Printf("Golang version: %s\n", runtime.Version())
-		return
-	}
+  <platform> is the target platform (GOOS).
 
-	if opts.number {
-		env.Out.Println(root.Version)
-		return
+  <arch> is the target architecture (GOARCH).
+`,
+		Run: func(cmd *cobra.Command, args []string) {
+			defer warnDeprecated()
+			env.Out.Printf("%s %s", execenv.RootCommandName, cmd.Root().Version)
+		},
 	}
+}
 
-	if opts.commit {
-		env.Out.Println(GitCommit)
-		return
+// warnDeprecated warns about deprecated build variables
+// TODO: 0.12.0: remove support for old build tags
+func warnDeprecated() {
+	msg := "please contact your package maintainer"
+	reason := "deprecated build variable"
+	if GitLastTag != "" {
+		slog.Warn(msg, "reason", reason, "name", "GitLastTag", "value", GitLastTag)
+	}
+	if GitExactTag != "" {
+		slog.Warn(msg, "reason", reason, "name", "GitExactTag", "value", GitExactTag)
+	}
+	if GitCommit != "" {
+		slog.Warn(msg, "reason", reason, "name", "GitCommit", "value", GitCommit)
 	}
-
-	env.Out.Printf("%s version: %s\n", execenv.RootCommandName, root.Version)
 }
diff --git a/doc/generate.go b/doc/generate.go
index 0001f1a77358010f53c98578fd49e088f16e0d97..005d9df7627e064ae26527d3b3381a0c01a0f471 100644
--- a/doc/generate.go
+++ b/doc/generate.go
@@ -34,7 +34,7 @@ func main() {
 		wg.Add(1)
 		go func(name string, f func(*cobra.Command) error) {
 			defer wg.Done()
-			root := commands.NewRootCommand()
+			root := commands.NewRootCommand("")
 			err := f(root)
 			if err != nil {
 				fmt.Printf("  - %s: FATAL\n", name)
diff --git a/doc/man/git-bug-version.1 b/doc/man/git-bug-version.1
index 7183b696e17be709632732b5dcc5b4956b74836c..988849e8afc14998940f467c2b9083bbaecd0f73 100644
--- a/doc/man/git-bug-version.1
+++ b/doc/man/git-bug-version.1
@@ -2,7 +2,7 @@
 .TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" ""
 
 .SH NAME
-git-bug-version - Show git-bug version information
+git-bug-version - Print version information
 
 
 .SH SYNOPSIS
@@ -10,25 +10,44 @@ git-bug-version - Show git-bug version information
 
 
 .SH DESCRIPTION
-Show git-bug version information
+Print version information.
 
+.PP
+Format:
+  git-bug  [commit[/dirty]]   
 
-.SH OPTIONS
-\fB-n\fP, \fB--number\fP[=false]
-	Only show the version number
+.PP
+Format Description:
+   may be one of:
+  	- A semantic version string, prefixed with a "v", e.g. v1.2.3
+  	- "undefined" (if not provided, or built with an invalid version string)
+
+.PP
+[commit], if present, is the commit hash that was checked out during the
+  build. This may be suffixed with '/dirty' if there were local file
+  modifications. This is indicative of your build being patched, or modified in
+  some way from the commit.
 
 .PP
-\fB-c\fP, \fB--commit\fP[=false]
-	Only show the commit hash
+ is the version of the go compiler used for the build.
 
 .PP
-\fB-a\fP, \fB--all\fP[=false]
-	Show all version information
+ is the target platform (GOOS).
 
 .PP
+ is the target architecture (GOARCH).
+
+
+.SH OPTIONS
 \fB-h\fP, \fB--help\fP[=false]
 	help for version
 
 
+.SH EXAMPLE
+.EX
+git bug version
+.EE
+
+
 .SH SEE ALSO
 \fBgit-bug(1)\fP
diff --git a/doc/md/git-bug.md b/doc/md/git-bug.md
index 03bebb65e7ae1588df7a93077a58f3822d5164bc..2ef0b77252bd5f7bc7ca4835a002688a51d871a6 100644
--- a/doc/md/git-bug.md
+++ b/doc/md/git-bug.md
@@ -31,7 +31,7 @@ git-bug [flags]
 * [git-bug push](git-bug_push.md)	 - Push updates to a git remote
 * [git-bug termui](git-bug_termui.md)	 - Launch the terminal UI
 * [git-bug user](git-bug_user.md)	 - List identities
-* [git-bug version](git-bug_version.md)	 - Show git-bug version information
+* [git-bug version](git-bug_version.md)	 - Print version information
 * [git-bug webui](git-bug_webui.md)	 - Launch the web UI
 * [git-bug wipe](git-bug_wipe.md)	 - Wipe git-bug from the git repository
 
diff --git a/doc/md/git-bug_version.md b/doc/md/git-bug_version.md
index ceba8790ff8d1dcbda7184f6803d98cba5f4f42e..a2569aff160794b6b145f62397804c52ebec5412 100644
--- a/doc/md/git-bug_version.md
+++ b/doc/md/git-bug_version.md
@@ -1,18 +1,46 @@
 ## git-bug version
 
-Show git-bug version information
+Print version information
+
+### Synopsis
+
+
+Print version information.
+
+Format:
+  git-bug <version> [commit[/dirty]] <compiler version> <platform> <arch>
+
+Format Description:
+  <version> may be one of:
+  	- A semantic version string, prefixed with a "v", e.g. v1.2.3
+  	- "undefined" (if not provided, or built with an invalid version string)
+
+  [commit], if present, is the commit hash that was checked out during the
+  build. This may be suffixed with '/dirty' if there were local file
+  modifications. This is indicative of your build being patched, or modified in
+  some way from the commit.
+
+  <compiler version> is the version of the go compiler used for the build.
+
+  <platform> is the target platform (GOOS).
+
+  <arch> is the target architecture (GOARCH).
+
 
 ```
 git-bug version [flags]
 ```
 
+### Examples
+
+```
+git bug version
+```
+
 ### Options
 
 ```
-  -n, --number   Only show the version number
-  -c, --commit   Only show the commit hash
-  -a, --all      Show all version information
-  -h, --help     help for version
+  -h, --help   help for version
 ```
 
 ### SEE ALSO
diff --git a/main.go b/main.go
index 7a2034baba99d4e9edff6c5b36e4ff683fc26e03..5b7a4caa6343152182bfd32492c48987ce71ad32 100644
--- a/main.go
+++ b/main.go
@@ -4,9 +4,15 @@
 package main
 
 import (
+	"os"
+
 	"github.com/git-bug/git-bug/commands"
 )
 
 func main() {
-	commands.Execute()
+	v, _ := getVersion()
+	root := commands.NewRootCommand(v)
+	if err := root.Execute(); err != nil {
+		os.Exit(1)
+	}
 }
diff --git a/misc/completion/generate.go b/misc/completion/generate.go
index b64c1034f672e3a4885f33ae9f8673809252e9bb..5c64681214fe88320119c2e36509734d762fa72b 100644
--- a/misc/completion/generate.go
+++ b/misc/completion/generate.go
@@ -26,7 +26,7 @@ func main() {
 		wg.Add(1)
 		go func(name string, f func(*cobra.Command) error) {
 			defer wg.Done()
-			root := commands.NewRootCommand()
+			root := commands.NewRootCommand("")
 			err := f(root)
 			if err != nil {
 				fmt.Printf("  - %s: %v\n", name, err)
diff --git a/version.go b/version.go
new file mode 100644
index 0000000000000000000000000000000000000000..de9bd8753b6d0b82a803e9031fc96167b3ec066c
--- /dev/null
+++ b/version.go
@@ -0,0 +1,94 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+	"runtime/debug"
+	"strings"
+
+	"github.com/git-bug/git-bug/commands"
+	"golang.org/x/mod/semver"
+)
+
+var (
+	version = "undefined"
+)
+
+// getVersion returns a string representing the version information defined when
+// the binary was built, or a sane default indicating a local build. a string is
+// always returned. an error may be returned along with the string in the event
+// that we detect a local build but are unable to get build metadata.
+//
+// TODO: support validation of the version (that it's a real version)
+// TODO: support notifying the user if their version is out of date
+func getVersion() (string, error) {
+	var arch string
+	var commit string
+	var modified bool
+	var platform string
+
+	var v strings.Builder
+
+	// this supports overriding the default version if the deprecated var used
+	// for setting the exact version for releases is supplied. we are doing this
+	// in order to give downstream package maintainers a longer window to
+	// migrate.
+	//
+	// TODO: 0.12.0: remove support for old build tags
+	if version == "undefined" && commands.GitExactTag != "" {
+		version = commands.GitExactTag
+	}
+
+	// automatically add the v prefix if it's missing
+	if version != "undefined" && !strings.HasPrefix(version, "v") {
+		version = fmt.Sprintf("v%s", version)
+	}
+
+	// reset the version string to undefined if it is invalid
+	if ok := semver.IsValid(version); !ok {
+		version = "undefined"
+	}
+
+	v.WriteString(version)
+
+	info, ok := debug.ReadBuildInfo()
+	if !ok {
+		v.WriteString(fmt.Sprintf(" (no build info)\n"))
+		return v.String(), errors.New("unable to read build metadata")
+	}
+
+	for _, kv := range info.Settings {
+		switch kv.Key {
+		case "GOOS":
+			platform = kv.Value
+		case "GOARCH":
+			arch = kv.Value
+		case "vcs.modified":
+			if kv.Value == "true" {
+				modified = true
+			}
+		case "vcs.revision":
+			commit = kv.Value
+		}
+	}
+
+	if commit != "" {
+		v.WriteString(fmt.Sprintf(" %.12s", commit))
+	}
+
+	if modified {
+		v.WriteString("/dirty")
+	}
+
+	v.WriteString(fmt.Sprintf(" %s", info.GoVersion))
+
+	if platform != "" {
+		v.WriteString(fmt.Sprintf(" %s", platform))
+	}
+
+	if arch != "" {
+		v.WriteString(fmt.Sprintf(" %s", arch))
+	}
+
+	return fmt.Sprint(v.String(), "\n"), nil
+}