Right, so you’ve got your basic CLI tool working with some flags. Good for you. But now you’re thinking, “What if I want a flag that’s available to every single command and subcommand in my entire application?” Welcome to the world of persistent flags. This is where you stop just hanging pictures on the walls and start messing with the foundation and wiring of the house.

The core idea is simple: a persistent flag is a flag that gets attached not just to the command you define it on, but to that command and every single one of its children. It’s inheritance, but for flags. This is perfect for global configuration settings—things like verbosity levels, configuration file paths, API endpoints, or authentication tokens. You don’t want to copy-paste the --config flag definition into every subcommand; that’s a recipe for madness and typos. You define it once on the root command, and boom, it’s everywhere.

The Nuts and Bolts of PersistentFlags()

In Cobra, you don’t use cmd.Flags() for this. You use cmd.PersistentFlags(). The method signature is identical; it’s just a different bucket that Cobra knows to pour into all the child commands. Let’s look at this in action. Imagine we’re building a CLI for a fictional deployment tool called shipit.

package main

import (
	"fmt"
	"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
	Use:   "shipit",
	Short: "Deploys things. Obviously.",
}

var verbose bool
var configFile string

func init() {
	// This is the important bit
	rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output")
	rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "config.yaml", "Path to config file")
}

var deployCmd = &cobra.Command{
	Use:   "deploy",
	Short: "Deploys the thing",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Printf("Deploying with config: %s\n", configFile)
		if verbose {
			fmt.Println("DEBUG: Fetching artifacts, checking dependencies...")
		}
	},
}

func main() {
	rootCmd.AddCommand(deployCmd)
	rootCmd.Execute()
}

Now, because --verbose and --config are defined as persistent flags on the root command, the deploy command automatically gets them. You can run shipit deploy --verbose --config prod.yaml and it works exactly as you’d expect. This is the magic. This is why you use Cobra.

Where Inheritance Actually Happens (The Gotcha)

Here’s the first thing that trips everyone up, so pay attention. The variable that holds the flag’s value (verbose and configFile in our example) is still just a package-level global variable. Cobra’s persistence magic only applies to the flag’s definition and parsing. It makes the flag available on the command line for child commands. It does not, I repeat, does not, automatically propagate the parsed value of that flag into separate instances of variables for each command.

This means you must define your persistent flag variables in a shared scope—almost always the package scope. If you try to define a verbose variable inside the init() function of a subcommand’s file, you’re going to have a bad time. The root command’s init() runs, parses the flag, and sets the root’s package-scoped variable. Your subcommand’s different, locally-scoped variable will still be at its default value. Always use package-level variables for persistent flags.

Overriding Persistent Flags (Because Sometimes You’re a Rebel)

Just because a flag is inherited doesn’t mean a child command is stuck with it. A subcommand can override a persistent flag from its parent. Why would you do this? Maybe the default value doesn’t make sense for a specific subcommand. Let’s add a status command where verbose output is on by default.

var statusCmd = &cobra.Command{
	Use:   "status",
	Short: "Checks deployment status",
	Run: func(cmd *cobra.Command, args []string) {
		// verbose is already available thanks to inheritance
		if verbose {
			fmt.Println("DEBUG: Polling service health...")
		}
		fmt.Println("Status: OK")
	},
}

func init() {
	// Add the command first
	rootCmd.AddCommand(statusCmd)
	// Then override the persistent flag's default JUST for this command
	statusCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", true, "Enable verbose output")
}

Notice the subtle but crucial detail: we’re re-defining the flag on the statusCmd, but we’re pointing it at the same global variable (&verbose). We’re just changing the default value for this specific command. Now, shipit status will run verbosely by default, while shipit deploy will not. The user can still use --verbose and --no-verbose (if you’ve defined it) on either command to override these defaults. This is powerful stuff, but use it judiciously; inconsistent default behavior can confuse users.

The Configuration Conundrum

The most common use case for persistent flags is for a --config flag. The pattern goes like this:

  1. Define --config as a persistent flag on the root command.
  2. In the PersistentPreRun function of the root command, use the value of the configFile variable to load your configuration from a file into a global struct or viper.
  3. Because PersistentPreRun is inherited by children (unless they override it), this setup logic runs before any subcommand’s Run function.

This ensures your config is loaded and ready, no matter which subcommand the user finally executes. It’s the backbone of a well-structured CLI application. Just remember: if a subcommand defines its own PersistentPreRun, it overrides the parent’s. To keep the parent’s behavior, you have to manually call cmd.Parent().PersistentPreRun(cmd, args) inside the child’s function. It’s a bit clunky, but it works.