Plugins
Plugins are trusted local Ruby extensions for Kward. Use them when you need behavior that prompts, skills, or instructions cannot provide.
Good plugin use cases:
- add a slash command for a personal workflow,
- show project/session status in the terminal footer,
- add concise local context to prompts,
- log or observe transcript events,
- expose local commands to an RPC client.
Plugins run inside the Kward process with your user permissions. Install only plugins you trust.
When to use a plugin
| Need | Better choice |
|---|---|
| Reusable prompt text | prompt template |
| Reusable model instructions | skill |
| Repository rules | AGENTS.md |
| Local Ruby code or integration | plugin |
See Extensibility for the full overview of Kward's extension points and prompt assembly order.
Where plugins live
Kward loads top-level Ruby files from:
~/.kward/plugins/*.rb
Plugins are not loaded from the current workspace or a custom KWARD_CONFIG_PATH directory. This prevents a project checkout from silently adding executable Ruby code to Kward.
A first plugin
Create the plugin directory:
mkdir -p ~/.kward/plugins
Create ~/.kward/plugins/hello.rb:
Kward.plugin do |plugin|
plugin.command "hello", description: "Say hello", argument_hint: "[name]" do |args, ctx|
name = args.strip.empty? ? "there" : args.strip
ctx.say("Hello, #{name}.")
end
end
Start Kward and run:
/hello World
When developing plugins, use /reload inside Kward to reload all plugin files without restarting. This picks up changes to existing plugins and registers new ones, then rebuilds the system message.
Add a slash command
Use plugin commands for local actions that should not call the model.
Kward.plugin do |plugin|
plugin.command "session-info", description: "Show session details" do |_args, ctx|
ctx.say("Session: #{ctx.session_name || ctx.session_id || 'unnamed'}")
ctx.say("Workspace: #{ctx.workspace_root}")
end
end
Command names do not include /. They must start with a letter or number and may contain letters, numbers, _, and -.
A plugin command cannot replace a built-in command or prompt-template command.
Add prompt context
Prompt context is short text injected into future model requests.
Use it for stable facts the model should know, not for large files or secrets. The block should return a string (injected into the system prompt) or nil (skipped). The example below uses Ruby's next to return nil early when the condition does not match.
Kward.plugin do |plugin|
plugin.prompt_context do |ctx|
next unless File.exist?(File.join(ctx.workspace_root, "Gemfile"))
"Workspace note: this project uses Ruby. Prefer bundle exec for project commands."
end
end
If plugin state changes and Kward should rebuild the active system message, call:
ctx.
Add a footer
A footer can show compact local status in the terminal UI:
Kward.plugin do |plugin|
plugin. do |ctx|
"#{ctx.session_name || 'unnamed'} • #{ctx.transcript..length} messages"
end
end
Only one footer is active. If multiple plugins register footers, the later one replaces the earlier one and Kward prints a warning.
Add an interactive command
Interactive commands take over the composer region with a Kward-driven render and input loop. The plugin receives a controller object with a canvas API for drawing colored cells and reading keys. This is useful for games, dashboards, viewers, and similar full-region interactive experiences.
interactive_command accepts description: and argument_hint: keyword arguments, which appear in the slash command list and completion overlay just like regular plugin commands. rows: sets the fixed canvas height (minimum 1), and fps: sets the target frame rate (1–120, default 30).
Kward.plugin do |plugin|
plugin.interactive_command "demo", rows: 10, fps: 30, description: "Canvas demo" do |ui, ctx|
x = 0
ui.on_tick do |ui|
ui.clear_frame
ui.put(0, x, "X", :red)
x = (x + 1) % ui.width
key = ui.poll_key
return :exit if key == :ctrl_c || key == "q"
end
end
end
Run it with /demo from the interactive TUI. The canvas renders inside the
composer area for the specified number of rows. The transcript above stays
intact.
Controller API
The ui controller object passed to the handler block exposes:
| Method | Description | ||
|---|---|---|---|
put(row, col, char, *colors) |
Place a character at a zero-based position with optional ANSI color styles | ||
clear_frame |
Reset all canvas cells to blank | ||
render |
Mark the canvas as ready for Kward to draw | ||
poll_key |
Return the next pending key (non-blocking, nil if none) | ||
exit |
Request that the interactive loop exit | ||
| `on_tick { \ | ui\ | }` | Register a tick callback invoked each frame at the configured fps |
width |
Canvas width in terminal columns | ||
height |
Canvas height in terminal rows | ||
fps |
Target frame rate |
Keys are returned as symbols (:left, :right, :up, :down, :return,
:backspace, :space, :pageup, :pagedown) or raw strings for keys
without a named mapping. Ctrl+C always exits the loop immediately.
The tick callback runs at the configured frame rate (1–120 fps, default 30).
Returning :exit from the tick callback ends the loop, same as calling
ui.exit.
Lifecycle
- The composer state is saved on entry and fully restored on exit (input text, cursor, and transcript viewport).
- Interactive mode is a distinct state from the busy-input spinner lifecycle — no spinner or busy chrome is shown.
- Ctrl+C, the plugin calling
exit, or the tick callback returning:exitall exit cleanly and restore the prior composer state. - Resize during interactive mode forces a clean exit and restore.
Interactive commands require the TUI prompt interface. They are not available in piped/non-interactive mode or through RPC.
Observe transcript events
Use transcript events when you need to log or react to live activity:
Kward.plugin do |plugin|
plugin.on_transcript_event do |event, ctx|
next unless event.type == "assistant_delta"
File.open(File.join(ctx.workspace_root, ".assistant-stream.log"), "a") do |file|
file.write(event.payload[:delta])
end
end
end
Event payloads are read-only copies. Handler errors are caught and printed as warnings.
Common event types include:
reasoning_deltaassistant_deltaassistant_messagemodel_retryturn_steeredtool_calltool_resultanswer
Plugin context
Handlers receive a ctx object. Common methods:
ctx.workspace_rootctx.argsctx.say(message)ctx.transcript.messagesctx.session_idctx.session_namectx.session_pathctx.refresh_system_message!
These methods are available in all handler types: commands, footers, prompt context renderers, and transcript event observers. ctx.say outputs to the active frontend (terminal or RPC) wherever it is called.
The transcript is read-only. Use context methods instead of mutating Kward internals.
RPC support
Plugins are available in the CLI and experimental RPC backend.
RPC clients can:
- list plugin commands through
commands/list, - run plugin commands through
commands/run, - run plugin slash commands through
turns/startinput such as/hello World.
Plugin command output is emitted through normal turn events without calling the model.
Security
Plugins are local Ruby code. They can read files, write files, run commands, make network requests, and read environment variables as your user.
Recommended practices:
- Install plugins only from sources you trust.
- Keep plugins in your personal
~/.kward/pluginsdirectory. - Do not put secrets in shared plugin files.
- Prefer environment variables or private config for credentials.
- Keep prompt context short and never inject secrets into model prompts.
- Be careful with transcript observers that persist conversation content.