v1 docs architecture go 1.26

[KongBaseSetup]

Kong models the entire command tree as nested Go structs — there is no imperative flag registration. The struct is the schema. This document covers the entrypoint, dependency injection via BindTo, the command tree layout, and how to extend it.

3parse-time bindings
5command groups
10struct tags documented
1entrypoint

// 01CLI Framework

scut uses kong for CLI parsing. Kong models the entire command tree as nested Go structs — there is no imperative flag registration. The struct is the schema.

// 02Entrypoint

cmd/scut/main.go defines the root cli struct, builds the parser with kong.Must, then parses os.Args[1:] in a separate step so parse failures can be logged before the process exits:

cmd/scut/main.go GO
type cli struct {
    Version versionFlag    `name:"version" help:"Print version and exit." short:"v"`
    Claude  claude.Cmd     `cmd:"claude" help:"Claude Code agent commands — hooks, status line, and configuration."`
    Format  formatcmd.Cmd  `cmd:"format" help:"Format source code files."`
    Logging loggingcmd.Cmd `cmd:"logging" help:"Manage scut log files."`
}

func main() {
    var c cli
    parser := kong.Must(&c,
        kong.Name("scut"),
        kong.Description("CLI tool for managing AI coding agents. Called as a subprocess by agent hooks — reads JSON from stdin, writes JSON to stdout."),
        kong.Vars{"version": version.String()},
        kong.BindTo(os.Stdin, (*io.Reader)(nil)),
        kong.BindTo(os.Stdout, (*io.Writer)(nil)),
        kong.BindTo(afero.NewOsFs(), (*afero.Fs)(nil)),
        kong.HelpOptions{
            NoExpandSubcommands: true,
            FlagsLast:           true,
            Compact:             true,
        },
    )

    ctx, err := parser.Parse(os.Args[1:])
    if err != nil {
        logging.LogParseError(os.Args, err)
        parser.FatalIfErrorf(err)
    }

    logger, logCloser := c.Claude.OpenLogger(ctx.Command())
    if logCloser != nil {
        defer logCloser.Close()
    }

    logger.Debug("invoked", "args", os.Args, "command", ctx.Command())

    ctx.FatalIfErrorf(ctx.Run(logger))
}

kong.Must builds a *kong.Kong parser without parsing arguments. The separate parser.Parse(os.Args[1:]) call returns a *kong.Context plus an error — splitting parse from build lets us run logging.LogParseError so bad hook invocations are diagnosable from the log file (see logging). ctx.Run() finds the selected command's Run() method and calls it, injecting any bound dependencies as arguments.

// 03Dependency Injection via Bind

We use kong.BindTo to wire dependencies that commands receive through their Run() method signature. Kong matches Run() parameters by type — if a parameter is io.Reader, Kong looks up what was bound to that interface and injects it.

Current bindings registered at parse time:

BindingInterfaceConcrete ValuePurpose
kong.BindTo(os.Stdin, (*io.Reader)(nil)) io.Reader os.Stdin Commands read input (e.g., hook JSON payloads) from stdin
kong.BindTo(os.Stdout, (*io.Writer)(nil)) io.Writer os.Stdout Commands write output (e.g., hook JSON responses) to stdout
kong.BindTo(afero.NewOsFs(), (*afero.Fs)(nil)) afero.Fs afero.OsFs Commands that need filesystem access (e.g., post-tool-use formatting)

The (*io.Reader)(nil) syntax is a nil pointer used only for its type — Kong reflects on it to determine the interface. The concrete value (os.Stdin) is what gets injected at runtime.

Run-time bindings

The *slog.Logger binding is created after parsing, based on --log / --log-level flags, and passed as an extra binding to ctx.Run():

cmd/scut/main.go GO
logger, logCloser := c.Claude.OpenLogger(ctx.Command())
if logCloser != nil {
    defer logCloser.Close()
}

logger.Debug("invoked", "args", os.Args, "command", ctx.Command())

ctx.FatalIfErrorf(ctx.Run(logger))

The logger.Debug("invoked", ...) call records every invocation at Debug level — useful when diagnosing whether a hook subprocess ran at all.

This makes commands testable without touching real stdio — pass logging.Discard as the logger:

preToolUse_test.go GO
func TestPreToolUse(t *testing.T) {
    input := strings.NewReader(`{"session_id":"test",...}`)
    var output bytes.Buffer
    cmd := &preToolUseCmd{}
    if err := cmd.Run(input, &output, logging.Discard); err != nil {
        t.Fatal(err)
    }
    // assert on output.Bytes()
}

Adding New Bindings

To add a new dependency available to all commands:

  1. Register it in main() with kong.BindTo(impl, (*InterfaceType)(nil)) for interfaces, kong.Bind(value) for concrete types, or pass as an extra binding to ctx.Run(value).
  2. Add the type as a parameter to any command's Run() method. Kong resolves it automatically.
cmd/scut/main.go GO
// In main() — static binding:
kong.Bind(logger)

// In main() — run-time binding:
ctx.FatalIfErrorf(ctx.Run(logger))

// In a command:
func (c *myCmd) Run(stdin io.Reader, stdout io.Writer, log *slog.Logger) error {

Version Flag

The --version / -v flag uses Kong's BeforeReset lifecycle hook. When the flag is set, BeforeReset fires before any command resolution — it prints the version string and exits. This avoids requiring a dedicated version subcommand.

Kong Vars

kong.Vars{"version": version.String()} registers key-value pairs available to struct tags via ${version} interpolation and to lifecycle hooks via the kong.Vars map. Currently used only for version output.

// 04Command Structure

Layout

Commands live under internal/cmd/ organized by agent, then by capability:

internal/cmd/ TREE
cmd/scut/main.go                    # Entrypoint, root cli struct, bindings
internal/cmd/claude/claude.go          # "scut claude" agent group (--log, --log-level flags)
internal/cmd/claude/hook/hook.go       # "scut claude hook" subcommands
internal/cmd/claude/config/config.go   # "scut claude config" subcommands (install, uninstall, status)
internal/cmd/format/format.go          # "scut format" group (formatter commands)
internal/cmd/logging/logging.go        # "scut logging" group (clean command)

Each level exports a Cmd struct that the parent embeds with a cmd:"" tag.

How Commands Map to Structs

Kong's command tree is a direct reflection of struct nesting:

command-tree.txt ASCII
scut claude hook pre-tool-use
   │      │     │        │
   cli    │     │        └─ preToolUseCmd (leaf — has Run())
          │     └─ hook.Cmd struct field
          └─ claude.Cmd struct field
  • Group nodes (claude, hook) are exported Cmd structs with cmd:"" tagged fields for their children. They have no Run() method.
  • Leaf commands (pre-tool-use, session-start, etc.) are unexported structs with a Run(...) error method. They are the actual execution targets.

// 05Adding Commands

Adding a leaf command to an existing group

  1. Define an unexported struct in the group's package:

    internal/cmd/mygroup/mygroup.go GO
    type myNewCmd struct{}
    
    func (c *myNewCmd) Run(stdin io.Reader, stdout io.Writer) error {
        // implementation
        return nil
    }
  2. Add it as a field on the group's Cmd struct:

    internal/cmd/mygroup/mygroup.go GO
    type Cmd struct {
        // ... existing commands
        MyNew myNewCmd `cmd:"my-new" help:"Does the new thing."`
    }

    The cmd:"my-new" tag sets the CLI name. Kong lowercases and hyphenates automatically if you omit the tag value, but explicit names are clearer.

Adding a new command group

  1. Create a new package under internal/cmd/:

    internal/cmd/mygroup/mygroup.go GO
    package mygroup
    
    type Cmd struct {
        SubCmd subCmd `cmd:"sub" help:"A subcommand."`
    }
    
    type subCmd struct{}
    
    func (c *subCmd) Run(stdin io.Reader, stdout io.Writer) error {
        return nil
    }
  2. Add the group to its parent's Cmd struct:

    cmd/scut/main.go GO
    // In cmd/scut/main.go or the parent group package:
    type cli struct {
        // ... existing
        MyGroup mygroup.Cmd `cmd:"my-group" help:"My new group."`
    }

// 06Struct Tag Reference

Tags used on command/flag struct fields:

TagPurposeExample
cmd:"" Marks a struct field as a subcommand cmd:"pre-tool-use"
help:"" Short help text shown in --help help:"Handle PreToolUse events."
name:"" Overrides the flag/command name name:"version"
short:"" Single-character flag alias short:"v"
arg:"" Marks a field as a positional argument arg:""
required:"" Makes a flag/arg mandatory required:""
default:"" Sets a default value default:"json"
enum:"" Restricts to a set of values enum:"json,text"
env:"" Reads default from env var env:"BOTCTRL_FORMAT"
hidden:"" Hides from help output hidden:""

// 07Build

See magefiles/ for build targets. mage build compiles with ldflags that inject build metadata into internal/version. All mage targets set GOEXPERIMENT=jsonv2 via init() in magefiles/helpers.go.

noteDo not run go test, go build, or go vet directly — always use the corresponding Mage target. Magefiles set GOEXPERIMENT=jsonv2 and other required environment configuration automatically. Running Go toolchain commands directly will produce incorrect results or miss build tags.