diff --git a/cmd/cmd.go b/cmd/cmd.go
index 480efa8..dfa80f7 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -60,10 +60,6 @@ func run(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	if conf.Completion != "" {
-		return completion(cmd, conf.Completion)
-	}
-
 	cmd.SilenceUsage = true
 
 	if len(args) == 0 {
diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go
index ae140c5..6f1d191 100644
--- a/cmd/cmd_test.go
+++ b/cmd/cmd_test.go
@@ -44,14 +44,6 @@ func Test_run(t *testing.T) {
 		require.Error(t, run(cmd, []string{}))
 	})
 
-	t.Run("completion flag enabled", func(t *testing.T) {
-		cmd := New()
-		if err := cmd.Flags().Set(config.CompletionFlag, "zsh"); !assert.NoError(t, err) {
-			return
-		}
-		require.NoError(t, run(cmd, []string{}))
-	})
-
 	t.Run("has config", func(t *testing.T) {
 		cmd := New()
 		conf, ok := config.FromContext(cmd.Context())
diff --git a/cmd/completion.go b/cmd/completion.go
deleted file mode 100644
index 5dec1c1..0000000
--- a/cmd/completion.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package cmd
-
-import (
-	"errors"
-	"fmt"
-
-	"github.com/clevyr/yampl/internal/config"
-	"github.com/spf13/cobra"
-)
-
-var ErrInvalidShell = errors.New("invalid shell")
-
-func completion(cmd *cobra.Command, shell string) error {
-	switch shell {
-	case config.Bash:
-		return cmd.Root().GenBashCompletion(cmd.OutOrStdout())
-	case config.Zsh:
-		return cmd.Root().GenZshCompletion(cmd.OutOrStdout())
-	case config.Fish:
-		return cmd.Root().GenFishCompletion(cmd.OutOrStdout(), true)
-	case config.Powershell:
-		return cmd.Root().GenPowerShellCompletionWithDesc(cmd.OutOrStdout())
-	default:
-		return fmt.Errorf("%w: %s", ErrInvalidShell, shell)
-	}
-}
diff --git a/cmd/completion_test.go b/cmd/completion_test.go
deleted file mode 100644
index 16cc0d6..0000000
--- a/cmd/completion_test.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package cmd
-
-import (
-	"io"
-	"testing"
-
-	"github.com/clevyr/yampl/internal/config"
-	"github.com/spf13/cobra"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-)
-
-func Test_completion(t *testing.T) {
-	r, w := io.Pipe()
-	_ = r.Close()
-
-	type args struct {
-		cmd   *cobra.Command
-		shell string
-	}
-	tests := []struct {
-		name    string
-		w       io.Writer
-		args    args
-		wantErr require.ErrorAssertionFunc
-	}{
-		{"bash", io.Discard, args{New(), "bash"}, require.NoError},
-		{"bash error", w, args{New(), "bash"}, require.Error},
-		{"zsh", io.Discard, args{New(), "zsh"}, require.NoError},
-		{"zsh error", w, args{New(), "zsh"}, require.Error},
-		{"fish", io.Discard, args{New(), "fish"}, require.NoError},
-		{"fish error", w, args{New(), "fish"}, require.Error},
-		{"powershell", io.Discard, args{New(), "powershell"}, require.NoError},
-		{"powershell error", w, args{New(), "powershell"}, require.Error},
-		{"other", io.Discard, args{New(), "other"}, require.Error},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			tt.args.cmd.SetOut(tt.w)
-
-			if err := tt.args.cmd.Flags().Set(config.CompletionFlag, tt.args.shell); !assert.NoError(t, err) {
-				return
-			}
-			err := completion(tt.args.cmd, tt.args.shell)
-			tt.wantErr(t, err)
-		})
-	}
-}
diff --git a/docs/yampl.md b/docs/yampl.md
index 59eb924..045ff11 100644
--- a/docs/yampl.md
+++ b/docs/yampl.md
@@ -14,7 +14,7 @@ yampl [files | dirs] [-v key=value...] [flags]
 ### Options
 
 ```
-      --completion string        Output command-line completion code for the specified shell. Can be 'bash', 'zsh', 'fish', or 'powershell'.
+      --completion string        Generate the autocompletion script for the specified shell (one of bash, zsh, fish, powershell)
   -h, --help                     help for yampl
       --ignore-template-errors   Continue processing a file even if a template fails
       --ignore-unset-errors      Exit with an error if a template variable is not set (default true)
diff --git a/go.mod b/go.mod
index 79b21d9..5e6069b 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/clevyr/yampl
 go 1.23.2
 
 require (
-	gabe565.com/utils v0.0.0-20241111053222-0f59399cbb3c
+	gabe565.com/utils v0.0.0-20241116061915-abe2278ecd5c
 	github.com/Masterminds/sprig/v3 v3.3.0
 	github.com/dmarkham/enumer v1.5.10
 	github.com/lmittmann/tint v1.0.5
@@ -23,7 +23,7 @@ require (
 	github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/fatih/color v1.18.0 // indirect
-	github.com/goccy/go-yaml v1.13.7 // indirect
+	github.com/goccy/go-yaml v1.13.9 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/huandu/xstrings v1.5.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
diff --git a/go.sum b/go.sum
index 7730b7c..8646a93 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,7 @@
 dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
 dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
-gabe565.com/utils v0.0.0-20241111053222-0f59399cbb3c h1:xKxZ9fGvYYihnqbyt1OE5Giz+rDQf5junHUxmvkFBfw=
-gabe565.com/utils v0.0.0-20241111053222-0f59399cbb3c/go.mod h1:ekV9VNVFXI1E8niHsgfb76RQHP2oJH2zYaCw1cEWrhA=
+gabe565.com/utils v0.0.0-20241116061915-abe2278ecd5c h1:1rmGsS/Sbm85ZELLkfOtXJ99v3YHdmqvkhMDLteLUug=
+gabe565.com/utils v0.0.0-20241116061915-abe2278ecd5c/go.mod h1:1WioSVukwGZYG4Q0LJBnRhgYyVljmW2Izl+RW36ALUc=
 github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
 github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
 github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
@@ -22,8 +22,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
 github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
-github.com/goccy/go-yaml v1.13.7 h1:5k2i973KptPV1mur30XMXwGepDmskip4gA2zHWzWmOY=
-github.com/goccy/go-yaml v1.13.7/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
+github.com/goccy/go-yaml v1.13.9 h1:D/LhDa7E5HS/iYxSZzikUSHt1U9q/TeymVBJwodaglc=
+github.com/goccy/go-yaml v1.13.9/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
diff --git a/internal/config/completions.go b/internal/config/completions.go
index 0051cfb..58443ae 100644
--- a/internal/config/completions.go
+++ b/internal/config/completions.go
@@ -6,13 +6,6 @@ import (
 	"github.com/spf13/cobra"
 )
 
-const (
-	Bash       = "bash"
-	Zsh        = "zsh"
-	Fish       = "fish"
-	Powershell = "powershell"
-)
-
 func (c *Config) RegisterCompletions(cmd *cobra.Command) {
 	if err := errors.Join(
 		cmd.RegisterFlagCompletionFunc(InplaceFlag, BoolCompletion),
@@ -33,11 +26,6 @@ func (c *Config) RegisterCompletions(cmd *cobra.Command) {
 				return LogFormatStrings(), cobra.ShellCompDirectiveNoFileComp
 			},
 		),
-		cmd.RegisterFlagCompletionFunc(CompletionFlag,
-			func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
-				return []string{Bash, Zsh, Fish, Powershell}, cobra.ShellCompDirectiveNoFileComp
-			},
-		),
 	); err != nil {
 		panic(err)
 	}
diff --git a/internal/config/flags.go b/internal/config/flags.go
index 41614b1..35d3b3a 100644
--- a/internal/config/flags.go
+++ b/internal/config/flags.go
@@ -5,6 +5,8 @@ import (
 	"errors"
 	"log/slog"
 
+	"gabe565.com/utils/cobrax"
+	"gabe565.com/utils/must"
 	"github.com/spf13/cobra"
 )
 
@@ -25,8 +27,6 @@ const (
 	LogLevelFlag  = "log-level"
 	LogFormatFlag = "log-format"
 
-	CompletionFlag = "completion"
-
 	// Deprecated: Replaced by VarFlag
 	ValueFlag = "value"
 	// Deprecated: Removed. Yampl will always recurse if a given path is a directory
@@ -36,6 +36,8 @@ const (
 )
 
 func (c *Config) RegisterFlags(cmd *cobra.Command) {
+	must.Must(cobrax.RegisterCompletionFlag(cmd))
+
 	cmd.Flags().Var(c.valuesStringToString, ValueFlag, "Define a template variable. Can be used more than once.")
 
 	cmd.Flags().BoolVarP(&c.Inplace, InplaceFlag, "i", c.Inplace, "Edit files in place")
@@ -52,8 +54,6 @@ func (c *Config) RegisterFlags(cmd *cobra.Command) {
 	cmd.Flags().StringVarP(&c.LogLevel, LogLevelFlag, "l", c.LogLevel, "Log level (trace, debug, info, warn, error, fatal, panic)")
 	cmd.Flags().StringVar(&c.LogFormat, LogFormatFlag, c.LogFormat, "Log format (auto, color, plain, json)")
 
-	cmd.Flags().StringVar(&c.Completion, CompletionFlag, c.Completion, "Output command-line completion code for the specified shell. Can be 'bash', 'zsh', 'fish', or 'powershell'.")
-
 	// Deprecated
 	cmd.Flags().VarP(c.valuesStringToString, VarFlag, "v", "Define a template variable. Can be used more than once.")
 	cmd.Flags().BoolP(RecursiveFlag, "r", true, "Recursively update yaml files in the given directory")
diff --git a/internal/config/load.go b/internal/config/load.go
index 30cd9aa..79ea441 100644
--- a/internal/config/load.go
+++ b/internal/config/load.go
@@ -6,6 +6,7 @@ import (
 	"slices"
 	"strings"
 
+	"gabe565.com/utils/cobrax"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 )
@@ -21,7 +22,7 @@ func Load(cmd *cobra.Command) (*Config, error) {
 	}
 
 	IgnoredEnvs := []string{
-		CompletionFlag,
+		cobrax.FlagCompletion,
 	}
 
 	var errs []error
diff --git a/internal/generate/completions/main.go b/internal/generate/completions/main.go
index 0e71959..42a2866 100644
--- a/internal/generate/completions/main.go
+++ b/internal/generate/completions/main.go
@@ -6,15 +6,10 @@ import (
 	"os"
 	"path/filepath"
 
+	"gabe565.com/utils/cobrax"
 	"github.com/clevyr/yampl/cmd"
 )
 
-const (
-	shellBash = "bash"
-	shellZsh  = "zsh"
-	shellFish = "fish"
-)
-
 func main() {
 	if err := os.RemoveAll("completions"); err != nil {
 		panic(err)
@@ -24,23 +19,22 @@ func main() {
 	var buf bytes.Buffer
 	rootCmd.SetOut(&buf)
 
-	for _, shell := range []string{shellBash, shellZsh, shellFish} {
-		rootCmd.SetArgs([]string{"--completion=" + shell})
-		if err := rootCmd.Execute(); err != nil {
+	for _, shell := range []cobrax.Shell{cobrax.Bash, cobrax.Zsh, cobrax.Fish} {
+		if err := cobrax.GenCompletion(rootCmd, shell); err != nil {
 			panic(err)
 		}
 
-		path := filepath.Join("completions", shell)
+		path := filepath.Join("completions", string(shell))
 		if err := os.MkdirAll(path, 0o777); err != nil {
 			panic(err)
 		}
 
 		switch shell {
-		case shellBash:
+		case cobrax.Bash:
 			path = filepath.Join(path, rootCmd.Name())
-		case shellZsh:
+		case cobrax.Zsh:
 			path = filepath.Join(path, "_"+rootCmd.Name())
-		case shellFish:
+		case cobrax.Fish:
 			path = filepath.Join(path, rootCmd.Name()+".fish")
 		}