Cobra Advanced: Persistent Flags, Hooks, Completion

Updated

September 8, 2026

Cobra Advanced: Persistent Flags, Hooks, Completion

Overview

After basics: persistent flags (inherited by children), PreRun/PostRun hooks, required flags, groups, and shell completion. These are the features that justify Cobra over hand-rolled FlagSets.

Persistent flags

var (
    cfgFile string
    verbose bool
)

func init() {
    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
    rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose")
    // children see --config and -v without redefining
}
mytool --verbose get key
mytool get key --verbose     # also works for persistent flags

Local flags stay on one command:

getCmd.Flags().Bool("raw", false, "raw")

Required flags

getCmd.Flags().String("ns", "", "namespace")
_ = getCmd.MarkFlagRequired("ns")

PreRun / PostRun chain

Order for a subcommand:

PersistentPreRun → PreRun → Run → PostRun → PersistentPostRun
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
    return loadConfig(cfgFile)
}

getCmd.PreRunE = func(cmd *cobra.Command, args []string) error {
    if verbose {
        fmt.Fprintln(os.Stderr, "get", args)
    }
    return nil
}

Use PreRun for auth checks, config validation, and client construction.

Bind flags to structs cleanly

type GetOpts struct {
    Raw bool
    NS  string
}

func newGetCmd(store Store) *cobra.Command {
    var opts GetOpts
    cmd := &cobra.Command{
        Use:  "get KEY",
        Args: cobra.ExactArgs(1),
        RunE: func(cmd *cobra.Command, args []string) error {
            return store.Get(cmd.Context(), args[0], opts)
        },
    }
    cmd.Flags().BoolVar(&opts.Raw, "raw", false, "raw")
    cmd.Flags().StringVar(&opts.NS, "ns", "default", "namespace")
    return cmd
}

Factory functions make testing easier than package-level var.

Context on commands

// Cobra sets cmd.Context() from ExecuteContext
func Execute() error {
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
    defer stop()
    return rootCmd.ExecuteContext(ctx)
}

// in RunE:
req, _ := http.NewRequestWithContext(cmd.Context(), ...)

Command groups (Cobra 1.6+)

rootCmd.AddGroup(&cobra.Group{ID: "core", Title: "Core Commands"})
getCmd.GroupID = "core"
rootCmd.AddCommand(getCmd)

Help output clusters commands by group.

Custom usage / version templates

rootCmd.SetVersionTemplate(`{{printf "mytool %s\n" .Version}}`)
rootCmd.Version = buildinfo.Version
// enables --version on root when Version is non-empty

Shell completion

// cmd/completion.go
var completionCmd = &cobra.Command{
    Use:   "completion [bash|zsh|fish|powershell]",
    Short: "Generate completion script",
    Args:  cobra.ExactArgs(1),
    RunE: func(cmd *cobra.Command, args []string) error {
        switch args[0] {
        case "bash":
            return cmd.Root().GenBashCompletion(os.Stdout)
        case "zsh":
            return cmd.Root().GenZshCompletion(os.Stdout)
        case "fish":
            return cmd.Root().GenFishCompletion(os.Stdout, true)
        case "powershell":
            return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
        default:
            return fmt.Errorf("unsupported shell %q", args[0])
        }
    },
}

Dynamic args:

getCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
    keys := store.KeysWithPrefix(toComplete)
    return keys, cobra.ShellCompDirectiveNoFileComp
}

Traverse children for help

rootCmd.SetHelpCommand(helpCmd) // optional customize

Example: parent remote with children

remoteCmd := &cobra.Command{Use: "remote", Short: "Manage remotes"}
remoteAddCmd := &cobra.Command{
    Use:  "add NAME URL",
    Args: cobra.ExactArgs(2),
    RunE: func(cmd *cobra.Command, args []string) error {
        return remotes.Add(args[0], args[1])
    },
}
remoteCmd.AddCommand(remoteAddCmd)
rootCmd.AddCommand(remoteCmd)
// mytool remote add origin https://...

Testing Cobra commands

func TestHello(t *testing.T) {
    cmd := newHelloCmd()
    b := bytes.NewBufferString("")
    cmd.SetOut(b)
    cmd.SetErr(io.Discard)
    cmd.SetArgs([]string{"-n", "2", "Ada"})
    if err := cmd.Execute(); err != nil {
        t.Fatal(err)
    }
    // assert b.String()
}

Prefer constructing commands via factories rather than executing package-global rootCmd (mutable).

Rules of thumb

Do Don’t
Persistent flags for globals Copy the same flags on 20 commands
ExecuteContext + signals Ignore cancel in long RunE
Factories for commands Global mutable option vars only
Completion for public CLIs Skip docs for install paths

Try next

  1. Add --config persistent flag and load JSON in PersistentPreRunE.
  2. Mark a flag required; confirm error UX.
  3. Generate zsh completion and install to your fpath.