TL;DR: Cross-cutting concerns — timing, auth checks, panic recovery, logging — are the same problem in a CLI as in a web server, and web frameworks solved it years ago with middleware. go-tool-base brings the same functional Chain pattern to the Cobra command tree. A middleware wraps a command’s whole execution, so it can act before and after and decide whether the command runs at all. That’s strictly more than Cobra’s PreRun hooks can do.

The logic that belongs to no single command

Every CLI tool past a certain size grows a category of logic that doesn’t belong to any one command, yet needs to happen for many of them. Time how long the command took. Check the user is authenticated before a command that needs it. Recover from a panic so a crash becomes a clean error instead of a stack-trace vomit. Log that the command started and how it finished.

None of that is the command’s job. The deploy command’s job is to deploy. But timing and recovery and auth still have to happen around it, and around build, and around sync.

Put that logic in each command’s RunE and you’ve copied the same six lines into thirty functions, which means thirty places to fix when the logging format changes and thirty chances to forget one. Cross-cutting concerns copied by hand don’t stay consistent. They drift.

Web frameworks already solved this

This is not a new problem. It’s the oldest problem in web frameworks, and they settled on an answer a long time ago: middleware. Gin has it, Echo has it, every HTTP stack has it. A middleware is a wrapper that sits around a handler, runs its cross-cutting logic, and calls through to the handler in the middle.

A CLI command is, structurally, just a handler too. So go-tool-base brings the same pattern to the Cobra command tree, with the same functional Chain shape:

type Middleware func(
    next func(cmd *cobra.Command, args []string) error,
) func(cmd *cobra.Command, args []string) error

A middleware receives the next handler in the chain and returns a new handler that wraps it. You compose a stack of them, and each command’s real RunE runs in the middle of the onion. Write the timing logic once, as one middleware, and every command in the chain is timed. Change the log format once and all thirty commands change with it, because there was only ever one copy.

“But Cobra already has PreRun”

It does, and this is the objection worth answering properly, because Cobra ships PersistentPreRun and PreRun hooks and they look like they cover this.

They don’t, and the reason is structural. A PreRun hook is a thing that happens before the command. That’s all it is. It cannot run anything after. It cannot wrap the command in a defer. It cannot catch a panic the command throws. It cannot measure how long the command took, because measuring duration needs a start point and an end point and the hook only owns the start.

A middleware wraps the entire execution. Because it’s a function that calls next() in its own body, it straddles the command:

func TimingMiddleware(next HandlerFunc) HandlerFunc {
    return func(cmd *cobra.Command, args []string) error {
        start := time.Now()
        err := next(cmd, args)               // the command runs here
        log.Debug("command finished", "took", time.Since(start))
        return err
    }
}

Before, after, and around. A recovery middleware can put a defer recover() in place that a PreRun hook structurally cannot. An auth middleware can check a condition and return an error instead of calling next() at all, refusing to let the command run. PreRun can’t veto the command; it runs, then the command runs regardless.

PreRun is a notification that the command is about to happen. Middleware is control over whether and how it happens. For genuine cross-cutting concerns you need the second thing.

To sum up

Timing, auth, recovery and logging are cross-cutting concerns: necessary for many commands, owned by none. Hand-copied into every RunE, they drift out of sync. Web frameworks fixed this with middleware years ago, and a CLI command is structurally just another handler.

go-tool-base brings the functional Chain middleware pattern to the Cobra command tree. A middleware wraps a command’s whole execution, so it acts before and after and can decide whether the command runs at all — strictly more than Cobra’s PreRun hooks, which only fire beforehand and can’t wrap, recover, time, or veto. Write the concern once, wrap the chain, and every command inherits it consistently.