Introduction
If you've ever used kubectl, hugo, gh (GitHub CLI), or helm, you've already used a CLI built with Cobra. It's the standard library for building command-line interfaces in Go — and for good reason.
In this blog, we'll walk through everything you need to know about Cobra 🐍 — from installing it and creating your first command, all the way to subcommands, flags, argument validation and lifecycle hooks. Plenty of examples along the way.
Installation & Project Setup 🛠️
Step 1: Install Cobra
# Add cobra to your Go module
go get github.com/spf13/cobra@latest
Step 2: (Optional) Install cobra-cli generator
The cobra-cli tool generates command boilerplate so you don't have to write it from scratch every time
go install github.com/spf13/cobra-cli@latest
Step 3: Recommended project structure
Keep your CLI commands organized — one file per command inside cmd/:
myapp/
├── main.go ← tiny entry point
├── cmd/
│ ├── root.go ← rootCmd lives here
│ ├── serve.go ← "serve" subcommand
│ └── version.go ← "version" subcommand
└── go.mod
Your First Command 🚀
main.go — keep it as thin as possible
The entry point does one thing: call cmd.Execute().
package main
import "myapp/cmd"
func main() {
cmd.Execute()
}
cmd/root.go — define the root command
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "A brief description of myapp",
Long: `myapp is a demo CLI built with Cobra.
It shows how to structure a Go CLI application.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Welcome to myapp! Run --help for usage.")
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Let's run it:
~ go run . --help
myapp is a demo CLI built with Cobra.
It shows how to structure a Go CLI application.
Usage:
myapp [flags]
myapp [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
user A brief description of your command
Flags:
-h, --help help for myapp
Use "myapp [command] --help" for more information about a command.
You already get a fully formatted --help page for free. ✅
Subcommands 🌲
Subcommands are just more cobra.Command values, attached to a parent with AddCommand(). This is how you build git-style CLIs like myapp server start.
Example: nested subcommands — myapp server start / stop
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var serverCmd = &cobra.Command{
Use: "server",
Short: "Manage the server",
}
var serverStartCmd = &cobra.Command{
Use: "start",
Short: "Start the server",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("🚀 Server starting...")
},
}
var serverStopCmd = &cobra.Command{
Use: "stop",
Short: "Stop the server",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("🛑 Server stopped.")
},
}
func init() {
serverCmd.AddCommand(serverStartCmd, serverStopCmd)
rootCmd.AddCommand(serverCmd)
}
~ go run . server
Manage the server
Usage:
myapp server [command]
Available Commands:
start Start the server
stop Stop the server
Flags:
-h, --help help for server
Use "myapp server [command] --help" for more information about a command.
~ go run . server start
🚀 Server starting...
~ go run . server stop
🛑 Server stopped.
Flags 🚩
Cobra supports two kinds of flags: local (only on that command) and persistent (inherited by all subcommands).
But before we get there, you'll notice Cobra has four naming patterns for every flag type. People often get confused by this, so let's clear it up first.
The 4 naming patterns
Take String as an example — the same pattern applies to every type (Int, Bool, Duration, etc.):
| Method | Returns | Shorthand | Who holds the value |
|---|---|---|---|
String(name, value, usage) |
*string |
❌ | Cobra owns the pointer |
StringP(name, short, value, usage) |
*string |
✅ | Cobra owns the pointer |
StringVar(p, name, value, usage) |
nothing | ❌ | You own the pointer |
StringVarP(p, name, short, value, usage) |
nothing | ✅ | You own the pointer |
The naming rules are simple:
-
Psuffix → adds a single-character shorthand (e.g.-nalongside--name) -
Varsuffix → you pass in your own variable; Cobra writes into it
String and StringP — Cobra gives you the pointer
var serveCmd = &cobra.Command{
Use: "serve",
Run: func(cmd *cobra.Command, args []string) {
// retrieve via .GetString()
host, _ := cmd.Flags().GetString("host")
fmt.Println("host:", host)
},
}
func init() {
// String — no shorthand
serveCmd.Flags().String("host", "localhost", "Host to bind")
// StringP — adds -H as shorthand
serveCmd.Flags().StringP("host", "H", "localhost", "Host to bind")
}
~ go run . serve
host: localhost
~ go run . serve --host 127.0.0.1
host: 127.0.0.1
You don't have a variable to read directly — you have to call cmd.Flags().GetString("name") inside Run. Fine for simple scripts, but it gets noisy fast.
StringVar and StringVarP — you own the variable
var host string // declare at package level
var serveCmd = &cobra.Command{
Use: "serve",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("host:", host) // just use it directly ✅
},
}
func init() {
// StringVar — no shorthand
serveCmd.Flags().StringVar(&host, "host", "localhost", "Host to bind")
// StringVarP — adds -H as shorthand
serveCmd.Flags().StringVarP(&host, "host", "H", "localhost", "Host to bind")
}
You pass a pointer to your own variable. Cobra fills it in before Run is called. No GetString needed.
💡 In practice, always use
Var/VarP. Declaring your flags as package-level variables is cleaner, easier to test, and works well with Viper bindings.
When to add P (shorthand)
Add a shorthand when the flag is frequently typed. Common conventions:
cmd.Flags().StringVarP(&output, "output", "o", "", "Output file")
cmd.Flags().StringVarP(&config, "config", "c", "", "Config file")
cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output")
cmd.Flags().IntVarP(&port, "port", "p", 8080, "Port to listen on")
Local vs Persistent flags
Local — only visible on the command they're defined on:
func init() {
// --port is only available on serveCmd
serveCmd.Flags().IntVarP(&port, "port", "p", 8080, "Port to listen on")
}
Persistent — inherited by the command and all its subcommands:
func init() {
// --verbose works on rootCmd AND every subcommand
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output")
}
A common pattern is to put global flags like --config, --verbose, and --env as persistent on rootCmd, and command-specific flags like --port or --output as local on their own command.
Check if a flag was explicitly set
Sometimes you need to know whether the user actually passed a flag, or if you're just seeing the default value:
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("port") {
fmt.Println("user explicitly set --port to", port)
} else {
fmt.Println("using default port:", port)
}
return nil
},
This is especially useful when you want environment variables or a config file to take precedence over the default, but not over an explicit flag.
Arguments & Validation 🔒
Set the Args field on a command to validate positional arguments before Run is called.
-
MinimumNArgs
var greetCmd = &cobra.Command{
Use: "greet [names...]",
Short: "Greet one or more people",
Args: cobra.MinimumNArgs(1), // at least one name required
Run: func(cmd *cobra.Command, args []string) {
for _, name := range args {
fmt.Printf("Hello, %s!\n", name)
}
},
}
~ go run . greet Dev.to Reader
Hello, Dev.to!
Hello, Reader!
-
OnlyValidArgs
var greetCmd = &cobra.Command{
Use: "greet [names...]",
Short: "Greet one or more people",
ValidArgs: []string{"Dev.to"},
Args: cobra.OnlyValidArgs,
Run: func(cmd *cobra.Command, args []string) {
for _, name := range args {
fmt.Printf("Hello, %s!\n", name)
}
},
}
~ go run . greet Dev.to Reader
Error: invalid argument "Reader" for "myapp greet"
Usage:
myapp greet [names...] [flags]
Flags:
-h, --help help for greet
invalid argument "Reader" for "myapp greet"
exit status 1
| Validator | Meaning |
|---|---|
ArbitraryArgs |
Any number of arguments (default) |
MinimumNArgs(n) |
At least n arguments |
MaximumNArgs(n) |
At most n arguments |
ExactArgs(n) |
Exactly n arguments |
RangeArgs(min, max) |
Between min and max |
OnlyValidArgs |
Only values listed in ValidArgs
|
Lifecycle Hooks ⚙️
Cobra executes hooks in a well-defined order around your main Run function. This is useful for things like loading config, validating auth, or sending notifications.
var deployCmd = &cobra.Command{
Use: "deploy",
// 1. Runs before PreRun of this AND all children
PersistentPreRun: func(cmd *cobra.Command, args []string) {
fmt.Println("[1] PersistentPreRun — loading config...")
},
// 2. Runs just before Run (not inherited)
PreRun: func(cmd *cobra.Command, args []string) {
fmt.Println("[2] PreRun — validating credentials...")
},
// 3. The main logic
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("[3] Run — deploying...")
},
// 4. Runs after Run (not inherited)
PostRun: func(cmd *cobra.Command, args []string) {
fmt.Println("[4] PostRun — sending notification...")
},
// 5. Runs after PostRun for this AND all children
PersistentPostRun: func(cmd *cobra.Command, args []string) {
fmt.Println("[5] PersistentPostRun — cleanup done.")
},
}
~ go run . deploy
[1] PersistentPreRun — loading config...
[2] PreRun — validating credentials...
[3] Run — deploying...
[4] PostRun — sending notification...
[5] PersistentPostRun — cleanup done.
💡 Tip
Use theE-suffix variants (PersistentPreRunE,RunE, etc.) when a hook can fail — they return anerrorand stop execution if it's non-nil.
Error Handling 🛡️
Use RunE to return errors
var fetchCmd = &cobra.Command{
Use: "fetch [url]",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
resp, err := http.Get(args[0])
if err != nil {
return fmt.Errorf("fetch failed: %w", err)
}
defer resp.Body.Close()
fmt.Printf("Status: %s\n", resp.Status)
return nil
},
}
~ go run . fetch a
Error: fetch failed: Get "a": unsupported protocol scheme ""
Usage:
myapp fetch [url] [flags]
Flags:
-h, --help help for fetch
fetch failed: Get "a": unsupported protocol scheme ""
exit status 1
Optionally suppress Cobra's duplicate "Error: ..." print
var rootCmd = &cobra.Command{
Use: "myapp",
SilenceErrors: true, // quiet errors down stream
SilenceUsage: true, // silence usage when an error occurs
Short: "A brief description of myapp",
Long: `myapp is a demo CLI built with Cobra.
It shows how to structure a Go CLI application.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Welcome to myapp! Run --help for usage.")
},
}
~ go run . fetch a
fetch failed: Get "a": unsupported protocol scheme ""
exit status 1
📝 Conclusion
Cobra is the gold standard for building CLI tools in Go. It takes care of all the boring stuff — help pages, flag parsing, shell completion — so you can focus on what your tool actually does.
Here's what we covered:
- ✅ Installing Cobra and setting up a project
- ✅ Creating root and subcommands
- ✅ Flags
- ✅ Argument validation
- ✅ Lifecycle hooks
- ✅ Error handling with RunE
The next step? Try pairing Cobra with Viper for configuration management — they're built to work together. Happy coding! 🥂
Support My Work ☕
If you enjoy my work, consider buying me a coffee! Your support helps me keep creating valuable content and sharing knowledge. ☕


Top comments (0)