Skip to content

What exactly is the difference between Cobra PreRun and PersistentPreRun? Understand the execution order with one diagram. ​

Diagram showing exactly what the difference is between Cobra PreRun and PersistentPreRun

2026 Latest Version | Includes Complete Code Examples | Resolves 90% of Beginner Confusion

If you have used the Cobra framework, you must have seen these 5 hook functions:PersistentPreRun、PreRun,Run,PostRun,PersistentPostRun.

But do you really understand the difference between them?

  • Why did the subcommand not execute the parent command's PreRun?

  • PreRun and PersistentPreRun Who exactly executes first?

  • defined RunE Do we still need a definition?Run?

Don't rush,One diagram + 6 practical case studies, help you thoroughly understand it once and for all.

1. Understand the complete execution sequence in one diagram ​

Let’s get straight to the core essentials! The figure below shows the complete call chain of the 5 hooks when you execute a Cobra command:

Important Prerequisites

PreRun,Run,PostRun 系列钩子Only the current command defines Run or RunE will only execute when If a command only serves as a parent container (without a Run function), these hooks will not be triggered.

II. Core Differences Among the 5 Hooks (Comparison Table) ​

hook functionInherit to subcommands?Execution timingTypical scenariosIs there an E version?
PersistentPreRunYesBefore RunRead the configuration file, connect to the database, and set the log levelYes
PreRunNoBefore RunParameter validation, permission check, input validationYes
RunNoCore execution pointThe actual business logic of the commandYes
PostRunNoAfter RunClose file handles and print elapsed time statistics.Yes
PersistentPostRunYesAfter RunGlobal audit logs, report monitoring dataYes

Key conclusions:

  • belt Persistent of the meeting Inheritance Give subcommand (like an heirloom)

  • without Persistent Belongs only to the current command(like personal items)

  • bring E suffix return error, recommended to use

III. Breaking down the 5 hooks one by one (with code examples) ​

3.1 PersistentPreRun / PersistentPreRunE: The Best Place for Global Initialization ​

effect: Do some global preparatory work before executing the command,All subcommands will inherit.

Typical scenarios:

  • Read configuration file (e.g..env,config.yaml)

  • Initialize database connection

  • Set global log format (JSON/Text)

  • Inject common dependencies (such as Logger, Metrics)

Code example:

go
package main

import (
    "fmt"
    "log"
    "os"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var rootCmd = &cobra.Command{
    Use:   "mycli",
    Short: My CLI tool,
    PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
        // 1. Load the configuration file (executed for all subcommands)
        viper.SetConfigName("config")
        viper.AddConfigPath(".")
        if err := viper.ReadInConfig(); err != nil {
            return fmt.Errorf("failed to load configuration file: %w", err)
        }

        // 2. Set the global log format
        log.SetFlags(log.LstdFlags | log.Lshortfile)
        log.Println("Global configuration loaded")

        return nil
    },
}

// Subcommand: serve
var serveCmd = &cobra.Command{
    Use:   "serve",
    Start service
    RunE: func(cmd *cobra.Command, args []string) error {
        port := viper.GetString("server.port")
        log.Printf("Service started on port: %s", port)
        return nil
    },
}

func init() {
    rootCmd.AddCommand(serveCmd)
}

func main() {
    if err := rootCmd.Execute(); err != nil {
        os.Exit(1)
    }
}

Execute mycli serve的输出:

Global configuration loaded        <- PersistentPreRun executed
Service started on port: 8080   <- Run executes

Best Practices

  • Use consistently PersistentPreRunE(return error), do not use PersistentPreRun

  • Don't If you perform time-consuming operations (such as complex calculations) in PersistentPreRun, it will execute before every command.

  • If the subcommand requires Coverage For the parent's PersistentPreRun, simply write the logic directly in your own hook.

3.2 PreRun / PreRunE: Command-specific "security gate" ​

Effect: Do it before the current command is executed Dedicated verification, will not be inherited by subcommands.

Typical scenario:

  • Verify whether specific parameters exist or are valid (e.g.--port Is it between 1 and 65535)

  • Check whether the input file exists

  • Permission verification (e.g., checking whether the user has operation permissions)

Code example:

go
var startCmd = &cobra.Command{
    Use:   "start",
    Short: "Start a task",
    PreRunE: func(cmd *cobra.Command, args []string) error {
        // 1. Validate parameters
        if len(args) == 0 {
            return fmt.Errorf("please specify task name")
        }

        // 2. Validate port range
        port, _ := cmd.Flags().GetInt("port")
        if port < 1 || port > 65535 {
            return fmt.Errorf("port must be between 1-65535")
        }

        fmt.Printf("Validation passed, preparing to start task: %s\n", args[0])
        return nil
    },
    RunE: func(cmd *cobra.Command, args []string) error {
        fmt.Printf("Executing task: %s\n", args[0])
        // Business logic...
        return nil
    },
}

func init() {
    startCmd.Flags().Int("port", 8080, "service port")
    rootCmd.AddCommand(startCmd)
}

Execute mycli start mytask --port 70000 the output of:

Error: The port must be between 1 and 65535

(The Run function will never execute because PreRunE intercepts it in advance.)

Note

  • PreRun 里的错误会中断执行, Run and PostRun will no longer be executed

  • Do not confuse PreRun and PersistentPreRun The former only validates the current command, while the latter performs global initialization.

3.3 Run / RunE: Core business logic ​

effect:The vast majority of commands only need to implement this function., it is the "heart" of the command.

Typical scenarios:

  • Perform file read and write

  • Call API

  • Start service

  • data processing

Code example (prefer using RunE):

go
var deployCmd = &cobra.Command{
    Use:   "deploy",
    Short: "Deploy application",
    RunE: func(cmd *cobra.Command, args []string) error {
        // Core deployment logic
        fmt.Println("Packaging...")
        fmt.Println("Uploading to server...")
        fmt.Println("Deployment successful!")
        return nil
    },
}

Standard Edition vs E Edition:

  • Run: No return value; if an error occurs, can only log.Fatal or os.Exit, the program exits directly

  • RunE: Return error, Cobra handles it uniformly,Recommended for use

If both are defined Run and RunE,RunE will overwrite Run Only execute RunE.

3.4 PostRun / PostRunE: The cleanup sappers after command execution ​

effect: Perform cleanup work after the current command finishes executing,will not inherit.

Typical scenario:

  • Close the file handle

  • Disconnect the database connection

  • Print execution time

  • Send notifications (e.g., Slack messages)

Code example:

go
var backupCmd = &cobra.Command{
    Use:   "backup",
    Backup database
    RunE: func(cmd *cobra.Command, args []string) error {
        startTime := time.Now()
        // Backup logic...
        fmt.Println("Backup complete")
        return nil
    },
    PostRunE: func(cmd *cobra.Command, args []string) error {
        // Print elapsed time
        fmt.Printf("Backup time: %v\n", time.Since(startTime))
        return nil
    },
}

Tips

In PostRun, you can access variables defined in the Run function (via closures or the command context) to collect execution statistics.

3.5 PersistentPostRun / PersistentPostRunE: Global audit log ​

effect: After all commands have finished executing, perform global cleanup or reporting,All subcommands will inherit.

Typical scenario:

  • Report audit logs (which user executed what command)

  • Close global resources (such as closing the database connection pool).

  • Send monitoring metrics

Code example:

go
var rootCmd = &cobra.Command{
    Use:   "mycli",
    PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
        // 1. Close the database connection
        if db != nil {
            db.Close()
            log.Println("Database connection closed")
        }

        // 2. Report audit logs
        auditLog := fmt.Sprintf("User %s executed command: %s", os.Getenv("USER"), cmd.Name())
        log.Println("Audit log:", auditLog)

        return nil
    },
}

Important rules:PersistentPostRun at PostRun after Execute, the order is:

PreRun -> Run -> PostRun (current command) -> PersistentPostRun (inherited from parent)

IV. The 3 Biggest Pitfalls You’re Most Likely to Fall Into (with a Pitfall-Avoidance Guide) ​

Pitfall 1: Defined RunE but forgot to handle the returned error ​

error code:

go
var badCmd = &cobra.Command{
    Use: "bad",
    RunE: func(cmd *cobra.Command, args []string) error {
        if err := doSomething(); err != nil {
            return err  // An error was returned, but it was not handled in the main function.
        }
        return nil
    },
}

func main() {
    badCmd.Execute() // ignored the error
}

Correct approach:

go
func main() {
    if err := badCmd.Execute(); err != nil {
        fmt.Printf("Execution failed: %v\n", err)
        os.Exit(1)
    }
}

Pitfall 2: Confusing the override rules for PreRun and PersistentPreRun ​

Scene: The parent command defines PersistentPreRun, the subcommand defines PreRun.

go
// parent command
rootCmd.PersistentPreRun = func(...) {
    fmt.Println("Parent: PersistentPreRun")
}
rootCmd.PreRun = func(...) {
    fmt.Println("Parent: PreRun")
}

// subcommand
childCmd.PreRun = func(...) {
    fmt.Println("Child: PreRun")
}

Execute childCmd output:

Parent: PersistentPreRun   <- inherited from parent (because parent was not overridden)
Child: PreRun              <- Child's own PreRun (overrides the parent's PreRun)

Remember the mnemonic.:

  • Persistent Series: If the subcommand is not defined, use the parent's.

  • Non-Persistent Series: A subcommand always uses its own, and the parent's is overridden.

Pitfall 3: The command does not define Run, yet expects PreRun and PostRun to execute. ​

Incorrect example:

go
var containerCmd = &cobra.Command{
    Use:   "container",
    Short: Container management (just a group, no Run),
    PreRun: func(...) {  // Will never execute
        fmt.Println("Preparing to execute...")
    },
}

var childCmd = &cobra.Command{
    Use:   "list",
    Run: func(...) {
        fmt.Println("List containers")
    },
}
containerCmd.AddCommand(childCmd)

Execute container list:

  • containerCmd.PreRun will not execute(because containerCmd does not have Run)

  • Only execute childCmd one's own lifecycle

Solution: Move the PreRun logic to the subcommand, or use PersistentPreRun(Because even if the container command does not have Run, PersistentPreRun will still be inherited by subcommands.)

V. Practical Case: Building a Complete CLI Tool ​

Let's integrate the above knowledge points and build a real mycli Tool:

go
package main

import (
    "fmt"
    "log"
    "os"
    "time"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

// Global variable
var startTime time.Time
var db *fakeDB

type fakeDB struct{}

func (d *fakeDB) Close() error {
    log.Println("Database connection closed")
    return nil
}

var rootCmd = &cobra.Command{
    Use:   "mycli",
    Short: A complete CLI example
    Long:  "mycli demonstrates the correct usage of all Cobra lifecycle hooks",
    // 1. Global initialization
    PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
        // Record start time
        startTime = time.Now()

        // Load configuration
        viper.SetConfigName("config")
        viper.AddConfigPath(".")
        if err := viper.ReadInConfig(); err != nil {
            return fmt.Errorf("failed to load config: %w", err)
        }

        // Initialize database
        db = &fakeDB{}
        log.Println("Global initialization complete")
        return nil
    },
    // 2. Global finalization (Note: executed after PostRun)
    PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
        // Close the database
        if db != nil {
            db.Close()
        }

        // Print total elapsed time
        log.Printf("Total elapsed time: %v\n", time.Since(startTime))
        return nil
    },
}

// Subcommand: init (initialize project)
var initCmd = &cobra.Command{
    Use:   "init [project-name]",
    Initialize new project
    Args:  cobra.ExactArgs(1),
    PreRunE: func(cmd *cobra.Command, args []string) error {
        // Validate project name
        if len(args[0]) < 3 {
            return fmt.Errorf("Project name must be at least 3 characters")
        }
        log.Printf("Preparing to initialize project: %s", args[0])
        return nil
    },
    RunE: func(cmd *cobra.Command, args []string) error {
        projectName := args[0]
        log.Printf("Creating project: %s", projectName)
        // Business logic...
        return nil
    },
    PostRunE: func(cmd *cobra.Command, args []string) error {
        log.Println("Project initialization complete")
        return nil
    },
}

// Subcommand: deploy (deployment)
var deployCmd = &cobra.Command{
    Use:   "deploy",
    Short: "Deploy application",
    RunE: func(cmd *cobra.Command, args []string) error {
        log.Println("Deploying...")
        // Business logic...
        return nil
    },
}

func init() {
    rootCmd.AddCommand(initCmd, deployCmd)
}

func main() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Printf("Execution failed: %v\n", err)
        os.Exit(1)
    }
}

Execute mycli init myapp Output:

2026/08/31 10:00:00 Global initialization completed          <- PersistentPreRun
2026/08/31 10:00:00 Preparing to initialize project: myapp  <- PreRun (subcommand)
2026/08/31 10:00:01 Creating project: myapp    <- Run
2026/08/31 10:00:02 Project initialization completed          <- PostRun (subcommand)
2026/08/31 10:00:02 Database connection closed        <- PersistentPostRun
2026/08/31 10:00:02 Total elapsed time: 2.001s         <- PersistentPostRun

A perfect demonstration of the complete execution chain!

VI. Summary of Best Practices ​

SceneRecommendation hookreason
Read the configuration file and connect to the databasePersistentPreRunEAll commands are required; avoid code duplication.
Parameter validation, permission checkingPreRunEOnly applies to the current command and does not pollute the global scope.
Core business logicRunEReturn an error and handle it gracefully
Close connection, print elapsed timePostRunECleanup specific to the current command
Global audit logPersistentPostRunEAll commands are required and should be finalized uniformly.
All hooksUse XxxE VersionUnified error handling makes code more robust.

Practices to avoid ​

  • Do not perform time-consuming operations in PreRun.(e.g., calling an external API), it will block before Run

  • Don't modify global state in PreRun, making side effects difficult to troubleshoot

  • Do not mix the regular version and the E version.(if defined simultaneously PreRun and PreRunE), the E version will overwrite the standard version

  • Do not use in hooks.os.Exit(1), it should return an error and let the upper layer handle it

  • Don't put business logic in PersistentPreRun, it should only perform initialization

VII. Conclusion ​

Let's review the core knowledge points:

  • Execution order:PersistentPreRun -> PreRun -> Run -> PostRun -> PersistentPostRun

  • Inheritance rules: bring Persistent Those with it will be inherited by subcommands; those without it belong only to the current command.

  • Error handling: Prioritize use XxxE Version (returns error)

  • Precondition:PreRun/PostRun Only the current command defines Run/RunE will only execute

  • Override Rules: The subcommand defines its own PreRun, parent's PreRun was overwritten (but PersistentPreRun still execute)

Remember it in one sentence.:

Persistent is a family heirloom (inherited); without it, it is a personal item (not inherited); E is a gentleman (elegantly returns an error); without E, it is a brute (exits directly).

Reference Resources ​

Last updated on11/07/2026, 02:16 PM
@fishfinal | @fishfinals | [email protected]