kit

Loading Extensions

Auto-discovery

Kit automatically discovers and loads extensions from these paths, in order:

Path Scope
/usr/share/kit/extensions/*.go System-wide single files
/usr/share/kit/extensions/*/main.go System-wide subdirectory extensions
~/.config/kit/extensions/*.go User single files
~/.config/kit/extensions/*/main.go User subdirectory extensions
~/.local/share/kit/git/ Global git-installed packages
.kit/extensions/*.go Project-local single files
.kit/extensions/*/main.go Project-local subdirectory extensions
.kit/git/ Project-local git-installed packages

Kit loads every extension it finds. The order in the table is the order in which extensions load and in which their event handlers run, thus a project-local extension runs after a user one, and a user extension runs after a system-wide one.

System-wide extensions

The system-wide directory holds extensions that come with a packaged install of Kit (rpm, deb, Homebrew, and so on) and are shared by every user of the machine. It defaults to /usr/share/kit/extensions.

Set KIT_SYSTEM_EXTENSIONS_DIR to use different directories. Give more than one directory with the platform list separator (: on Unix, ; on Windows):

# Unix
KIT_SYSTEM_EXTENSIONS_DIR=/opt/kit/extensions:/srv/kit/extensions kit
# Windows (PowerShell)
$env:KIT_SYSTEM_EXTENSIONS_DIR = "C:\ProgramData\kit\extensions;D:\kit\extensions"; kit

Set the variable to an empty value to turn off system-wide discovery:

KIT_SYSTEM_EXTENSIONS_DIR= kit

Packagers can change the compiled-in default at build time:

go build -ldflags \
  "-X github.com/mark3labs/kit/internal/extensions.SystemExtensionsDir=/opt/kit/extensions" \
  ./cmd/kit

Explicit loading

Load extensions by path using the -e flag:

kit -e path/to/extension.go

Load multiple extensions:

kit -e ext1.go -e ext2.go

Disabling extensions

Disable all auto-discovered extensions:

kit --no-extensions

You can combine --no-extensions with -e to load only specific extensions:

kit --no-extensions -e my-extension.go

Installing from git

Install extensions from git repositories using kit install:

# Install globally (to ~/.local/share/kit/git/)
kit install https://github.com/user/my-kit-extension.git

# Install project-locally (to .kit/git/)
kit install -l https://github.com/user/my-kit-extension.git

# Update an installed package
kit install -u https://github.com/user/my-kit-extension.git

# Remove
kit install --uninstall my-kit-extension

Extension structure

Single-file extensions

A single .go file with an Init function:

//go:build ignore

package main

import "kit/ext"

func Init(api ext.API) {
    // register handlers, tools, commands, etc.
}

The //go:build ignore directive prevents the Go toolchain from trying to compile the file as part of a normal build.

Subdirectory extensions

For more complex extensions, create a directory with a main.go entry point:

.kit/extensions/my-extension/
├── main.go      # Must contain Init(api ext.API)
├── helpers.go   # Additional source files
└── config.go

Package-level state

Yaegi supports package-level variables captured in closures. This is the standard way to maintain state across event callbacks:

package main

import "kit/ext"

var callCount int

func Init(api ext.API) {
    api.OnToolCall(func(_ ext.ToolCallEvent, ctx ext.Context) {
        callCount++
        ctx.SetFooter(ext.HeaderFooterConfig{
            Content: ext.WidgetContent{
                Text: fmt.Sprintf("Tools called: %d", callCount),
            },
        })
    })
}

Standard library access

Extensions can import the full Go standard library, plus os/exec for spawning subprocesses. Environment variables are also readable: os.Getenv, os.LookupEnv, and os.Environ return Kit’s process environment, so extensions can pick up CI-provided variables (for example GITHUB_EVENT_PATH or a provider API key) and any vars the user exported before launching Kit.

package main

import (
    "os"
    "kit/ext"
)

func Init(api ext.API) {
    api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {
        if eventPath := os.Getenv("GITHUB_EVENT_PATH"); eventPath != "" {
            ctx.PrintInfo("Running in GitHub Actions: " + eventPath)
        }
    })
}

Environment access is read-only from the host’s perspective: the environment is snapshotted when the extension loads, and calls to os.Setenv mutate only the extension’s sandboxed copy — they never change Kit’s process environment or the host. This keeps extensions from leaking state into Kit or other extensions while still letting them read the configuration they need.

Failure isolation

Each extension is loaded into its own interpreter, and a failure in one never stops the others. A syntax error, a bad Init signature, a panic while the source is evaluated, or a panic inside Init is reported as a load failure for that file only — Kit continues starting up with the remaining extensions.

At runtime the same applies to widget rendering: a panic inside a Render callback hides that widget and logs the error rather than taking down the TUI.

This is isolation, not sandboxing. Extensions run in-process with os/exec access, so only load a .go file you would be willing to run directly.