Kit Extensions Development Guide
Kit extensions are single-file Go programs interpreted at runtime by Yaegi. They hook into Kit's lifecycle, register custom tools and slash commands, display widgets, intercept editor input, render tool output, register and switch color themes, and more.
Extensions can be distributed via git repositories using kit install. Repos can contain single extensions or collections of multiple extensions.
This file is the entry point. It gives the structure, a minimal example, and the constraints you must never miss. Detailed material lives in the references/ directory (see Reference files below). Read only the reference files that apply to your task.
Extension Structure
Every extension must export a package main with an Init(api ext.API) function:
//go:build ignore
package main
import "kit/ext"
func Init(api ext.API) {
// Register event handlers, tools, commands, etc.
}
The //go:build ignore tag prevents go build from compiling the file directly.
Extension Locations
Extensions are auto-loaded from these directories:
/usr/share/kit/extensions/*.go (system-wide, single files)
/usr/share/kit/extensions/*/main.go (system-wide, subdirectories)
~/.config/kit/extensions/*.go (user, single files)
~/.config/kit/extensions/*/main.go (user, subdirectories)
.kit/extensions/*.go (project-local, single files)
.kit/extensions/*/main.go (project-local, subdirectories)
Or loaded explicitly:
kit -e path/to/extension.go
kit --extension path/to/extension.go
Import Path
Extensions import the Kit API as "kit/ext". The full standard library is available plus os/exec for subprocess spawning.
API Overview
The Init function receives an ext.API object for registering handlers, and event handlers receive an ext.Context with runtime capabilities.
api.On* — subscribe to one of 30 lifecycle events (OnSessionStart, OnToolCall, OnAgentEnd, ...). See references/lifecycle-events.md.
api.RegisterTool / RegisterCommand / RegisterShortcut / RegisterOption — add LLM tools, /slash commands, key bindings, and config options. See references/tools-commands-shortcuts.md.
ctx.* — runtime capabilities: print output, inject messages, widgets, header/footer, prompts, overlays, editor interceptor, session data/state, model and tool management, LLM completions, themes. See references/context-api.md.
api.RegisterToolRenderer / api.RegisterMessageRenderer — custom rendering of tool calls and messages. See references/renderers.md.
Minimal Working Example
A tool, a slash command, and an event handler in one file:
//go:build ignore
package main
import (
"time"
"kit/ext"
)
var toolCalls int // package-level vars hold state across callbacks
func Init(api ext.API) {
api.RegisterTool(ext.ToolDef{
Name: "current_time",
Description: "Get the current date and time",
Parameters: `{"type":"object","properties":{}}`,
Execute: func(input string) (string, error) {
return time.Now().Format(time.RFC3339), nil
},
})
api.RegisterCommand(ext.CommandDef{
Name: "echo",
Description: "Echo back the provided text",
Execute: func(args string, ctx ext.Context) (string, error) {
ctx.PrintInfo("You said: " + args)
return "", nil
},
})
api.OnToolCall(func(e ext.ToolCallEvent, ctx ext.Context) *ext.ToolCallResult {
toolCalls++
return nil // nil = allow; return &ext.ToolCallResult{Block: true, Reason: "..."} to block
})
}
Run it with kit -e my-ext.go. Validate syntax with kit extensions validate.
Critical Yaegi Constraints (condensed — never skip)
Yaegi silently miscompiles some valid Go. Failures do not show an error; the code just does nothing. The full version with complete examples is in references/yaegi-constraints.md.
1. No named function references in struct fields or handler arguments
A named function assigned to a struct field (or passed directly as an argument) returns zero values across the interpreter boundary. Always use anonymous closure literals:
// WRONG - will silently return zero values:
func myHandler(key, text string) ext.EditorKeyAction { ... }
ctx.SetEditor(ext.EditorConfig{HandleKey: myHandler})
// CORRECT - use anonymous closure:
ctx.SetEditor(ext.EditorConfig{
HandleKey: func(key, text string) ext.EditorKeyAction { return myHandler(key, text) },
})
This applies to ALL struct fields that take function values: ToolDef.Execute, CommandDef.Execute, EditorConfig.HandleKey, EditorConfig.Render, ToolRenderConfig.RenderHeader, ToolRenderConfig.RenderBody, etc.
2. No comma-separated case lists in a tagless switch
In switch { case a, b, c: } Yaegi evaluates only the first expression. Join the conditions with || into one case expression, or use an if/else chain. A switch WITH a tag (switch n { case 1, 2, 3: }) is fine.
// WRONG - only the first condition is ever checked:
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9':
// CORRECT:
case (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9'):
3. No interfaces across the boundary
All extension-facing API types are concrete structs, never interfaces. Yaegi crashes on interface wrapper generation.
4. Package-level variables for state
Yaegi supports package-level variables captured in closures. This is the standard way to maintain state across event callbacks (var callCount int at file scope, then mutate inside handlers).
Other Rules You Must Know
- Return
nil to pass through. Every On* handler that returns a *Result pointer treats nil as "no change". Return a non-nil pointer only to modify or block.
- Slash commands and shortcut handlers run in their own goroutine. They can block on
ctx.PromptSelect, I/O, or exec.Command safely.
- Do not bind
ctrl+c as a shortcut (rejected at load). Prefer modifier combinations over bare keys.
e.StopReason == "error" is the error check in OnAgentEnd. Do NOT compare against "completed" for success.
- Tool
Parameters is a JSON Schema string. The input argument to Execute is the JSON-encoded parameters from the LLM.
- Use rune counts (
len([]rune(s))) not byte length when aligning widget text that contains box-drawing or multi-byte characters.
Reference files
Read the file that matches the task. Paths are relative to this skill's root directory.
| File |
Read it when you need... |
references/lifecycle-events.md |
The full list of all 30 events (session, agent turn, tool, tool-call streaming, input, streaming, model, UI, context filtering, session control, custom events), their fields, and return types. |
references/tools-commands-shortcuts.md |
Registering tools (including ExecuteWithContext with cancellation/progress), slash commands with tab-completion, keyboard shortcuts (key-name normalization and reserved keys), and options. |
references/context-api.md |
The complete ext.Context API: output, message injection, widgets, header/footer, status bar, prompts, overlays, editor interceptor, terminal size, thinking level, UI visibility, session data/state, model and tool management, LLM completions, TUI suspension, themes, application control, context fields. |
references/renderers.md |
Custom tool renderers (RenderHeader / RenderBody) and message renderers. |
references/yaegi-constraints.md |
The full Yaegi constraints section with complete code examples for each pitfall. |
references/common-patterns.md |
Recipes: tool call blocking, system prompt injection, background processing with SendMessage, ephemeral context injection, live widget updates, custom theme with slash command, spawning Kit as a sub-agent. |
references/testing-and-distribution.md |
The pkg/extensions/test harness and assertions, CLI testing commands, and distributing extensions via git repositories (kit install, repo structure, README template, storage locations). |
references/plan-mode-example.md |
A complete, end-to-end extension (Plan Mode) that combines shortcuts, widgets, tool blocking, and state. |
references/bridged-sdk-apis.md |
Bridged SDK capabilities: conversation tree navigation, skill loading, template parsing, model resolution, model pricing. |
Key Files for Reference
1---2name: kit-extensions3description: Guide for creating Kit extensions. Use when the user asks to build, create, or modify a Kit extension, add a custom tool, slash command, widget, keyboard shortcut, editor interceptor, tool renderer, or hook into any Kit lifecycle event.4---56# Kit Extensions Development Guide78Kit extensions are single-file Go programs interpreted at runtime by Yaegi. They hook into Kit's lifecycle, register custom tools and slash commands, display widgets, intercept editor input, render tool output, register and switch color themes, and more.910Extensions can be distributed via git repositories using `kit install`. Repos can contain single extensions or collections of multiple extensions.1112This file is the entry point. It gives the structure, a minimal example, and the constraints you must never miss. Detailed material lives in the `references/` directory (see [Reference files](#reference-files) below). Read only the reference files that apply to your task.1314## Extension Structure1516Every extension must export a `package main` with an `Init(api ext.API)` function:1718```go19//go:build ignore2021package main2223import "kit/ext"2425func Init(api ext.API) {26 // Register event handlers, tools, commands, etc.27}28```2930The `//go:build ignore` tag prevents `go build` from compiling the file directly.3132## Extension Locations3334Extensions are auto-loaded from these directories:3536- `/usr/share/kit/extensions/*.go` (system-wide, single files)37- `/usr/share/kit/extensions/*/main.go` (system-wide, subdirectories)38- `~/.config/kit/extensions/*.go` (user, single files)39- `~/.config/kit/extensions/*/main.go` (user, subdirectories)40- `.kit/extensions/*.go` (project-local, single files)41- `.kit/extensions/*/main.go` (project-local, subdirectories)4243Or loaded explicitly:4445```bash46kit -e path/to/extension.go47kit --extension path/to/extension.go48```4950## Import Path5152Extensions import the Kit API as `"kit/ext"`. The full standard library is available plus `os/exec` for subprocess spawning.5354## API Overview5556The `Init` function receives an `ext.API` object for registering handlers, and event handlers receive an `ext.Context` with runtime capabilities.5758- **`api.On*`** — subscribe to one of 30 lifecycle events (`OnSessionStart`, `OnToolCall`, `OnAgentEnd`, ...). See `references/lifecycle-events.md`.59- **`api.RegisterTool` / `RegisterCommand` / `RegisterShortcut` / `RegisterOption`** — add LLM tools, `/slash` commands, key bindings, and config options. See `references/tools-commands-shortcuts.md`.60- **`ctx.*`** — runtime capabilities: print output, inject messages, widgets, header/footer, prompts, overlays, editor interceptor, session data/state, model and tool management, LLM completions, themes. See `references/context-api.md`.61- **`api.RegisterToolRenderer` / `api.RegisterMessageRenderer`** — custom rendering of tool calls and messages. See `references/renderers.md`.6263## Minimal Working Example6465A tool, a slash command, and an event handler in one file:6667```go68//go:build ignore6970package main7172import (73 "time"7475 "kit/ext"76)7778var toolCalls int // package-level vars hold state across callbacks7980func Init(api ext.API) {81 api.RegisterTool(ext.ToolDef{82 Name: "current_time",83 Description: "Get the current date and time",84 Parameters: `{"type":"object","properties":{}}`,85 Execute: func(input string) (string, error) {86 return time.Now().Format(time.RFC3339), nil87 },88 })8990 api.RegisterCommand(ext.CommandDef{91 Name: "echo",92 Description: "Echo back the provided text",93 Execute: func(args string, ctx ext.Context) (string, error) {94 ctx.PrintInfo("You said: " + args)95 return "", nil96 },97 })9899 api.OnToolCall(func(e ext.ToolCallEvent, ctx ext.Context) *ext.ToolCallResult {100 toolCalls++101 return nil // nil = allow; return &ext.ToolCallResult{Block: true, Reason: "..."} to block102 })103}104```105106Run it with `kit -e my-ext.go`. Validate syntax with `kit extensions validate`.107108---109110## Critical Yaegi Constraints (condensed — never skip)111112Yaegi silently miscompiles some valid Go. Failures do not show an error; the code just does nothing. The full version with complete examples is in `references/yaegi-constraints.md`.113114### 1. No named function references in struct fields or handler arguments115116A named function assigned to a struct field (or passed directly as an argument) returns zero values across the interpreter boundary. Always use anonymous closure literals:117118```go119// WRONG - will silently return zero values:120func myHandler(key, text string) ext.EditorKeyAction { ... }121ctx.SetEditor(ext.EditorConfig{HandleKey: myHandler})122123// CORRECT - use anonymous closure:124ctx.SetEditor(ext.EditorConfig{125 HandleKey: func(key, text string) ext.EditorKeyAction { return myHandler(key, text) },126})127```128129This applies to ALL struct fields that take function values: `ToolDef.Execute`, `CommandDef.Execute`, `EditorConfig.HandleKey`, `EditorConfig.Render`, `ToolRenderConfig.RenderHeader`, `ToolRenderConfig.RenderBody`, etc.130131### 2. No comma-separated case lists in a tagless switch132133In `switch { case a, b, c: }` Yaegi evaluates **only the first expression**. Join the conditions with `||` into one case expression, or use an `if`/`else` chain. A switch WITH a tag (`switch n { case 1, 2, 3: }`) is fine.134135```go136// WRONG - only the first condition is ever checked:137case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9':138139// CORRECT:140case (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9'):141```142143### 3. No interfaces across the boundary144145All extension-facing API types are concrete structs, never interfaces. Yaegi crashes on interface wrapper generation.146147### 4. Package-level variables for state148149Yaegi supports package-level variables captured in closures. This is the standard way to maintain state across event callbacks (`var callCount int` at file scope, then mutate inside handlers).150151---152153## Other Rules You Must Know154155- **Return `nil` to pass through.** Every `On*` handler that returns a `*Result` pointer treats `nil` as "no change". Return a non-nil pointer only to modify or block.156- **Slash commands and shortcut handlers run in their own goroutine.** They can block on `ctx.PromptSelect`, I/O, or `exec.Command` safely.157- **Do not bind `ctrl+c`** as a shortcut (rejected at load). Prefer modifier combinations over bare keys.158- **`e.StopReason == "error"`** is the error check in `OnAgentEnd`. Do NOT compare against `"completed"` for success.159- **Tool `Parameters` is a JSON Schema string.** The `input` argument to `Execute` is the JSON-encoded parameters from the LLM.160- **Use rune counts (`len([]rune(s))`) not byte length** when aligning widget text that contains box-drawing or multi-byte characters.161162---163164## Reference files165166Read the file that matches the task. Paths are relative to this skill's root directory.167168| File | Read it when you need... |169|------|--------------------------|170| `references/lifecycle-events.md` | The full list of all 30 events (session, agent turn, tool, tool-call streaming, input, streaming, model, UI, context filtering, session control, custom events), their fields, and return types. |171| `references/tools-commands-shortcuts.md` | Registering tools (including `ExecuteWithContext` with cancellation/progress), slash commands with tab-completion, keyboard shortcuts (key-name normalization and reserved keys), and options. |172| `references/context-api.md` | The complete `ext.Context` API: output, message injection, widgets, header/footer, status bar, prompts, overlays, editor interceptor, terminal size, thinking level, UI visibility, session data/state, model and tool management, LLM completions, TUI suspension, themes, application control, context fields. |173| `references/renderers.md` | Custom tool renderers (`RenderHeader` / `RenderBody`) and message renderers. |174| `references/yaegi-constraints.md` | The full Yaegi constraints section with complete code examples for each pitfall. |175| `references/common-patterns.md` | Recipes: tool call blocking, system prompt injection, background processing with `SendMessage`, ephemeral context injection, live widget updates, custom theme with slash command, spawning Kit as a sub-agent. |176| `references/testing-and-distribution.md` | The `pkg/extensions/test` harness and assertions, CLI testing commands, and distributing extensions via git repositories (`kit install`, repo structure, README template, storage locations). |177| `references/plan-mode-example.md` | A complete, end-to-end extension (Plan Mode) that combines shortcuts, widgets, tool blocking, and state. |178| `references/bridged-sdk-apis.md` | Bridged SDK capabilities: conversation tree navigation, skill loading, template parsing, model resolution, model pricing. |179180## Key Files for Reference181182- [`internal/extensions/api.go`](https://github.com/mark3labs/kit/blob/main/internal/extensions/api.go) — Complete API type definitions183- [`internal/extensions/runner.go`](https://github.com/mark3labs/kit/blob/main/internal/extensions/runner.go) — Event dispatch and state management184- [`internal/extensions/loader.go`](https://github.com/mark3labs/kit/blob/main/internal/extensions/loader.go) — Yaegi interpreter setup185- [`internal/extensions/symbols.go`](https://github.com/mark3labs/kit/blob/main/internal/extensions/symbols.go) — All types exported to extensions186- [`pkg/extensions/test/`](https://github.com/mark3labs/kit/tree/main/pkg/extensions/test) — Testing package with harness, mocks, and assertions187- [`examples/extensions/tool-logger_test.go`](https://github.com/mark3labs/kit/blob/main/examples/extensions/tool-logger_test.go) — Complete test example188- [`examples/extensions/`](https://github.com/mark3labs/kit/tree/main/examples/extensions) — 25+ working example extensions