Cobra vs urfave cli vs Kong for Go CLIs and Which to Pick
Compare Cobra, urfave/cli and Kong for Go command line tools: how each declares commands, flags, help and completions, and which fits which project.

A Go tool needs to parse arguments, show help and report usage errors, and four options handle it. The choice affects code layout and binary size, and switching later means rewriting every command. This comparison uses the same small tool, greet, with a --name flag and a subcommand. It follows flags and Cobra basics, and the versions assumed are Go 1.23, Cobra 1.8, urfave/cli v3 and Kong 1.
Standard library flag
package main
import (
"flag"
"fmt"
)
func main() {
name := flag.String("name", "world", "who to greet")
flag.Parse()
fmt.Printf("hello, %s\n", *name)
}No dependency and a clear API. It stops at one command: no subcommands, no short and long flag pairs (-n and --name are not linked), and flags must come before positional arguments. For a script-sized tool this is enough, and it adds nothing to the binary beyond what is already there.
Cobra
var name string
var greet = &cobra.Command{
Use: "greet",
Short: "Print a greeting",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Printf("hello, %s\n", name)
return nil
},
}
func main() {
greet.Flags().StringVarP(&name, "name", "n", "world", "who to greet")
root := &cobra.Command{Use: "tool"}
root.AddCommand(greet)
if err := root.Execute(); err != nil {
os.Exit(1)
}
}Cobra models a tree of commands with persistent flags that children inherit. It generates help, usage errors and shell completion scripts for Bash, Zsh, Fish and PowerShell. The price is boilerplate and a larger dependency tree, plus the temptation to use globals as shown. It is the default for tools with many subcommands, such as docker-style interfaces.
urfave/cli
func main() {
cmd := &cli.Command{
Name: "tool",
Usage: "greeter",
Flags: []cli.Flag{
&cli.StringFlag{Name: "name", Aliases: []string{"n"}, Value: "world", Usage: "who to greet"},
},
Action: func(ctx context.Context, c *cli.Command) error {
fmt.Printf("hello, %s\n", c.String("name"))
return nil
},
}
if err := cmd.Run(context.Background(), os.Args); err != nil {
log.Fatal(err)
}
}Everything is declared in one literal, with aliases, environment variable bindings and defaults on each flag. Flag values are fetched by name at run time, which means a typo is caught only at run time. It is lighter than Cobra and quick to learn. Version 3 changed the API, so check which one examples use.
Kong
type CLI struct {
Name string `short:"n" default:"world" help:"Who to greet."`
}
func (c *CLI) Run() error {
fmt.Printf("hello, %s\n", c.Name)
return nil
}
func main() {
var cli CLI
ctx := kong.Parse(&cli)
ctx.FatalIfErrorf(ctx.Run())
}The interface is a struct, and Kong fills it in. Types provide validation and the compiler catches field typos. Subcommands become nested structs with embedded cmd:"" tags. It is compact for medium tools, and a good fit if you like declarations over builders. Completions need an add-on package.
Choosing
One command, a few flags: flag. Many subcommands, completions, a long life: Cobra, since contributors will know it. A mid-size tool where you want little ceremony: urfave/cli or Kong. Whichever, keep the command wiring thin and put logic in testable functions; see testing a Go CLI. Measure the binary cost with the steps in why Go binaries are large before deciding a library is free.
Start with the standard library
A CLI with a couple of flags and no subcommands does not need a framework. The flag package parses short and long options, generates usage text and has no dependencies. Its limits are real: it supports a single dash style, so -v and --v are equivalent but combined short flags such as -abc are not recognised, there are no subcommands, and the usage output is plain. For tools with one purpose, this is fine, and it keeps the binary small and the build fast. A common pattern for modest subcommands is a switch on os.Args[1] with a separate flag.FlagSet for each, which stays dependency-free and works well up to three or four commands.
Outgrowing it is a signal, not a failure. When you find yourself writing help text by hand, manual alias handling and shell completion scripts, a framework will pay for itself.
What Cobra adds and what it costs
Cobra, used by kubectl, Hugo and the GitHub CLI, models commands as a tree. Each command has its own flags, arguments validation and help, and persistent flags apply to all children. It generates completion scripts for Bash, Zsh, Fish and PowerShell, man pages and Markdown documentation, and pairs with Viper for configuration from files and environment variables. The cost is size and structure: it adds a few megabytes to the binary through its dependencies, encourages a particular file layout, and has a lot of surface to learn. Initialisation order of flags and configuration trips up newcomers, so keep the setup code in one place and test it.
For a tool that will have a dozen subcommands and long life, Cobra is the safe, familiar choice, and contributors will recognise the layout. For a small tool it is more than needed.
urfave/cli and Kong
The urfave/cli library uses a declarative style: a slice of commands and flags with names, usage and actions. It is lighter than Cobra and easy to read, with built-in help, flag aliases, environment variable binding and completion support. Kong takes yet another approach, defining the CLI as a Go struct with tags. The struct fields become flags and arguments, nested structs become subcommands, and the parser fills them in. This reads nicely and is easy to test, since the configuration is a plain type. The trade-offs are less ecosystem than Cobra, and a style that some find too magical.
All three produce a good help screen. Compare them on the features you need: completion, environment binding, validation hooks and how they report errors, which matters for scripts that parse messages.
Making the choice
Match the library to the project's expected lifetime and size. A throwaway utility uses flag. A tool with a handful of subcommands and an expectation of stability suits urfave/cli or Kong. A product with a large command tree, plugins and generated documentation suits Cobra. Check maintenance: recent releases, open issues and the number of maintainers. Check the licence and the dependency footprint, which affects supply-chain exposure and build time. Prototype one command in the candidate libraries and look at the code and the help output. Whichever you pick, keep command logic out of the framework's callbacks: put it in plain functions that take parsed values, so it can be unit checked without the parser and so a future change of library touches only the wiring.
Consider the testing story as part of the choice. A framework that builds the command tree in a function you can call with a slice of arguments lets tests run commands in-process and capture the output buffers, with no subprocess. Cobra supports this through SetArgs and SetOut, and the others offer similar hooks. Write a few such tests early: they pin the help text, the flags and the exit behaviour, and they make a later library change far less risky than it would otherwise be.
Where this does not apply
For a tool you will write once and never share, the difference is unimportant. Mature options also exist outside this list, such as Charm's Fang and ff, and the design questions in flags, subcommands and defaults matter more than the library. See the Cobra documentation, the urfave/cli site and the Kong repository.


