Kong + CUE 08 Dec 2025

Kong is my go-to CLI parser in go, so I used it for epithet. It has pretty good built-in configuration handling, I appreciate how it lines up config values with CLI flags by basically treating config as a serialized tree of it's structs. This is great for simple things, but for epithet I needed slightly more complex mapping, with dynamic keys (user identities) and such. I wanted to keep the core of its model though as it's really nice.

So, I wrote a thing to configure it using CUE. CUE seems complicated if you read its docs, because it dives straight into it's prolog-y evaluation roots, but you can basicaly ignore that for simple use cases (and then use it for hairy ones). It turns out I really liked this, so extracted it to a library, meet kongcue.

It works mostly like the built-in Kong config mechanism, except it happily handles YAML, JSON, and CUE syntax, unifies across multiple files elegantly, and allows me to treat validation issues as configuration issues (with useful error messages, lone and column pointers, etc) if you use a config file, or fallback to kong's flag based errors if you don't use a config file. It can also simply generate a CUE schema file for artbitrary config documentation and validation!

Example time! Let's take a small hello-world style kong app:

package main

import (
	"fmt"

	"github.com/alecthomas/kong"
	"github.com/brianm/kongcue"
)

func main() {
	var c cli
	ktx := kong.Parse(&c, kongcue.AllowUnknownFields("messy"))
	ktx.FatalIfErrorf(ktx.Run(c))
}

type cli struct {
	Name      string            `default:"world" help:"The name of the person to greet"`
	Stuff     bool              `required:"" help:"A required flag, about stuff"`

	Greet     GreetCmd          `cmd:"" help:"Issue a greeting"`
	Depart    DepartCmd         `cmd:"" help:"Issue a valediction"`

	Config    kongcue.Config    `default:"./example.{yml,json,cue}" sep:";" help:"Config file paths"`
	ConfigDoc kongcue.ConfigDoc `cmd:"" help:"Print config schema"`

	message string
}

type GreetCmd struct {
	Excited int `default:"0" help:"How excited are you to see this person?"`
}

func (g GreetCmd) Run(c *cli) error {
	// elided
}

type DepartCmd struct {
	Sadness int `help:"How sad are you to be leaving?" required:""`
}

func (g DepartCmd) Run(c *cli) error {
	// elided
}

It has a couple sub-commands, some flags, all of it is just demo-ware. Note the lines:

	Config    kongcue.Config    `default:"./example.{yml,json,cue}" sep:";" help:"Config file paths"`
	ConfigDoc kongcue.ConfigDoc `cmd:"" help:"Print config schema"`

These set up configuration handling, and a sub-command to dump the schema and documentation! This does what it looks like, globs across the possible config file names, loads all that match, etc. The schema part is nifty, as it generats config docs, effectively. Invoke example config-doc and you get:

// Configuration schema for validating config files.
//
// This schema is written in CUE, a configuration language that
// validates and defines data. Learn more at https://cuelang.org
//
// To validate your config file against this schema:
//   1. Save this schema to a file (e.g., schema.cue)
//   2. Run: cue vet -d '#Root' schema.cue your-config.yaml
//
// Fields marked with ? are optional. Fields without ? are required.
#Root: close({
	// The name of the person to greet
	name?: string
	// A required flag, about stuff
	stuff: bool
	// Issue a greeting
	greet?: #Greet
	// Issue a valediction
	depart?: #Depart
	messy?:  _
})
#Depart: close({
	// How sad are you to be leaving?
	sadness: int
})
#Greet: close({
	// How excited are you to see this person?
	excited?: int
})

While this is a bit obscure, it is not that hard to follow even if you are not familiar with CUE's schema/constraint declarations.

If you invoke it with a bad config file, you get a nice config error:

kongcue/example on  main ❯ cat bad.yml
name: "test"
stuff: "walrus"
kongcue/example on  main ❯
kongcue/example on  main ❯ go build && ./example --config ./bad.yml
example: error: stuff: conflicting values "walrus" and bool (mismatched types string and bool):
                    /Users/brianm/src/github.com/brianm/kongcue/example/bad.yml:2:8
kongcue/example on  main ❯

Without a config file (default is not present, none specified) it gives you CLI oriented errors:

kongcue/example on  main ❯ ./example
example: error: missing flags: --stuff
kongcue/example on  main ❯

It has a simple escape hatch for non-CLI compatible config sections, which you see in the above example:

	ktx := kong.Parse(&c, kongcue.AllowUnknownFields("messy"))

This allows the messy thing in the example config, even though it does not appear in the kong struct tree.

name: "Brian"
stuff: true
depart:
  sadness: 3
messy:
  woof: "meow"
  splat: 7

The kongcue.AllowUnknownFields("messy") tells it to just accept anything in the messy value. At some point I'll add a mechanism to define a schema for these, but for now it just relies on deserializing to structs erroring out.

The cue.Value for the config tree is made available as a kong binding, so you can get it in whatever command you need to pull additional values from. This allows epithet policy configs like:

users:
  alice@example.com: [admin, eng]
  bob@example.com: [eng]
  charlie@example.com: [ops]
  diana@example.com: [admin, ops]

which can be defined nicely in CUE, but not naturally kong's cli constraints.

For a more complex, and real, schema generated from epithet, take a look at epithet's:

// Configuration schema for validating config files.
//
// This schema is written in CUE, a configuration language that
// validates and defines data. Learn more at https://cuelang.org
//
// To validate your config file against this schema:
//   1. Save this schema to a file (e.g., schema.cue)
//   2. Run: cue vet -d '#Root' schema.cue your-config.yaml
//
// Fields marked with ? are optional. Fields without ? are required.
#Root: close({
	// Print version information
	version?: bool
	// Increase verbosity (-v for debug, -vv for trace)
	verbose?: int
	// Path to log file (supports ~ expansion)
	log_file?: string
	// Disable TLS certificate verification (NOT RECOMMENDED)
	insecure?: bool
	// Path to PEM file with trusted CA certificates
	tls_ca_cert?: string
	// Start the epithet agent (or use 'agent inspect' to inspect state)
	agent?: #Agent
	// Invoked during ssh invocation in a 'Match exec ...'
	match?: #Match
	// Run the epithet CA server
	ca?: #Ca
	// Run the policy server with OIDC-based authorization
	policy?: #Policy
	// Authentication commands (OIDC, SAML, etc.)
	auth?: #Auth
})
#Agent: close({
	// Match patterns
	match?: [...string]
	// CA URL (repeatable, format: priority=N:https://url or https://url)
	ca_url?: [...string]
	// Authentication command
	auth?: string
	// Per-request timeout for CA requests
	ca_timeout?: int
	// Circuit breaker cooldown for failed CAs
	ca_cooldown?: int
	// Start the epithet agent
	start?: #AgentStart
	// Inspect broker state (certificates, agents)
	inspect?: #AgentInspect
})
#AgentInspect: close({
	// Broker socket path (overrides config-based discovery)
	broker?: string
	// Output in JSON format
	json?: bool
})
#AgentStart: close({})
#Auth: close({
	// Authenticate using OIDC/OAuth2 (Google Workspace, Okta, Azure AD, etc.)
	oidc?: #AuthOidc
})
#AuthOidc: close({
	// OIDC issuer URL (e.g., https://accounts.google.com)
	issuer: string
	// OAuth2 client ID
	client_id: string
	// OAuth2 client secret (optional if using PKCE)
	client_secret?: string
	// OAuth2 scopes (comma-separated)
	scopes?: [...string]
})
#Ca: close({
	// URL for policy service
	policy: string
	// Path to ca private key
	key?: string
	// Address to listen on
	listen?: string
})
#Match: close({
	// Remote host (%h)
	host: string
	// Remote port (%p)
	port: int
	// Remote user (%r)
	user: string
	// Connection hash (%C)
	hash: string
	// ProxyJump configuration (%j)
	jump?: string
	// Broker socket path
	broker?: string
})
#Policy: close({
	// Address to listen on
	listen?: string
	// OIDC issuer URL
	oidc_issuer?: string
	// OIDC audience (client ID)
	oidc_audience?: string
	// CA public key (URL, file path, or literal SSH key)
	ca_pubkey?: string
	// Default certificate expiration (e.g., 5m)
	default_expiration?: string
	users?:              _
	defaults?:           _
	hosts?:              _
})

It's not good docs, but it directly generated from the code, so it is at least accurate, and close to free :-)

I think the CUE website does a pretty bad job of explaining its value, to be honest. It's a darned useful tool even in the small. The website just hides that basic usefulness behind piles of articles of the "look at all the really powerful stuff it can do" for advanced cases.