Sample Terminal aesthetic representing CLI tools

Building CLI Tools with Cobra

January 5, 2026

Cobra is the de facto standard for building command-line applications in Go. It powers kubectl, hugo, gh, and many other popular tools. Here's how to structure your own CLI.

Project Structure

A well-organized Cobra project separates commands from business logic:

myapp/
├── cmd/
│   ├── root.go      # Root command, global flags
│   ├── serve.go     # myapp serve
│   └── migrate.go   # myapp migrate
├── internal/
│   ├── server/      # Business logic
│   └── database/
├── main.go          # Entry point
└── go.mod

The Root Command

Start with a root command that sets up global configuration:

// cmd/root.go
package cmd

var cfgFile string

var rootCmd = &cobra.Command{
    Use:   "myapp",
    Short: "A brief description of your application",
}

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

func init() {
    cobra.OnInitialize(initConfig)
    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
}

Adding Subcommands

Each subcommand lives in its own file and registers itself:

// cmd/serve.go
package cmd

var port int

var serveCmd = &cobra.Command{
    Use:   "serve",
    Short: "Start the HTTP server",
    RunE: func(cmd *cobra.Command, args []string) error {
        return server.Start(port)
    },
}

func init() {
    rootCmd.AddCommand(serveCmd)
    serveCmd.Flags().IntVarP(&port, "port", "p", 8080, "port to listen on")
}

Flag Types

Cobra supports various flag patterns:

// Required flag
cmd.Flags().StringVarP(&name, "name", "n", "", "user name (required)")
cmd.MarkFlagRequired("name")

// Mutually exclusive
cmd.MarkFlagsMutuallyExclusive("json", "yaml")

Testing Commands

Cobra commands are testable by capturing output:

func TestServeCommand(t *testing.T) {
    buf := new(bytes.Buffer)
    rootCmd.SetOut(buf)
    rootCmd.SetArgs([]string{"serve", "--port", "9000"})

    err := rootCmd.Execute()
    assert.NoError(t, err)
}

Conclusion

Cobra handles the boilerplate of CLI parsing so you can focus on functionality. Keep commands thin, delegate to internal packages, and your CLI will be maintainable as it grows.