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.
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
Load extensions by path using the -e flag:
kit -e path/to/extension.go
Load multiple extensions:
kit -e ext1.go -e ext2.go
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
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
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.
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
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),
},
})
})
}
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.
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.