Creating Plugins¶
All plugins live in $KLANGK_PLUGINS_DIR/<name>/ directories (defaults to .devenv/state/klangk/plugins/). A plugin can contain any combination of:
extension.ts— Pi extension withpi.registerTool(). Copied into the workspace image at build time.klangk/— Dart package for client-side browser actions:klangk/pubspec.yaml— Package definition, depends onklangk_plugin_api(git)klangk/lib/plugin.dart— Class extendingToolPluginwith action handlersklangk/lib/*.dart— Supporting Dart files (widgets, utilities)tools/— Server-side scripts and commands. Copied to/opt/klangk/bin/in the workspace image (onPATH).on-image-build.sh— Lifecycle hook: runs at image build time (see Lifecycle Hooks)on-entrypoint.sh— Lifecycle hook: runs at container starton-shell-init.sh— Lifecycle hook: runs on every shell open
No single component is required — a plugin can be an extension + Dart UI, just lifecycle hooks + tools, or any combination. The klangk/ subdirectory is only needed for client-side browser actions (e.g., celebrate, beep, authenticated fetch) that are dispatched via the browser bridge.
Build Integration¶
scripts/import_dart_plugins.pyscans$KLANGK_PLUGINS_DIR/*/klangk/for plugin Dart packages and generates$KLANGK_PLUGINS_DIR/.dart/(theklangk_pluginspackage with path deps andcreateAllPlugins())build-workspace-imagestages files from all plugins into$KLANGK_PLUGINS_DIR/.docker/and passes them via named build contexts:plugin-extensions—extension.tsfilesplugin-tools— flat directory of alltools/*files (installed to/opt/klangk/bin/)plugin-hooks— per-plugin lifecycle hook directories (installed to/opt/klangk/hooks/<name>/)flutterbuildwebruns the codegen before compilingstub_dart_plugins.shcreates a minimal stub at$KLANGK_PLUGINS_DIR/.dart/soflutter pub getworks before plugins are fetched (runs automatically at devenv shell startup viaenterShell; skips ifpubspec_overrides.yamlalready exists)- Both build tasks are triggered automatically by
devenv upviaexecIfModified
Adding a Plugin¶
For local development, create files directly in $KLANGK_PLUGINS_DIR:
- Create
$KLANGK_PLUGINS_DIR/<name>/extension.tswithpi.registerTool() - For client-side browser actions, add
klangk/pubspec.yaml(depends onklangk_plugin_api) andklangk/lib/plugin.dartextendingToolPlugin - For server-side scripts, add files in
$KLANGK_PLUGINS_DIR/<name>/tools/ devenv uprebuilds automatically when$KLANGK_PLUGINS_DIRchanges
For remote plugins, add an entry to $KLANGK_PLUGINS_DIR/plugins.yaml and run update-plugins to fetch it.
Lifecycle Hooks¶
Plugins can include shell scripts at their root that run automatically at specific points in the container lifecycle. All hooks are optional.
on-image-build.sh¶
Runs once at image build time via RUN in the Dockerfile. Use for system-level configuration that applies to all users and persists in the image.
- Runs as root
- No runtime environment variables available (only build-time values)
- Examples:
git config --system, installing system packages, writing config files
#!/usr/bin/env bash
# plugins/git-credential/on-image-build.sh
set -e
git config --system credential.helper klangk
on-entrypoint.sh¶
Runs once per container start from the entrypoint, before any shell opens. Use for setup that depends on runtime environment variables but only needs to happen once.
- Runs as the container's initial user (root inside the user namespace, mapped to the host user outside)
- Runtime environment variables are available (
KLANGK_WORKSPACE_ID, etc.) - Examples: writing runtime config files, one-time service initialization
on-shell-init.sh¶
Runs on every shell open from bash.bashrc. Use for per-user, per-session setup.
- Runs as the
klangkuser - User environment is available (
HOME,KLANGK_USER_ID, etc.) - Runs after
setup-clankers(Pi agent config) - Keep it fast — this runs on every new terminal tab and window
- Examples: per-user symlinks, session-specific env setup
Execution Order¶
Hooks execute alphabetically by plugin name. Within each plugin, only the relevant hook for the current lifecycle phase runs. If ordering between plugins matters, use numeric prefixes on plugin directory names (e.g., 00-core, 50-git-credential).
Container Layout¶
/opt/klangk/hooks/
git-credential/
on-image-build.sh
some-other-plugin/
on-entrypoint.sh
on-shell-init.sh
Plugin Configuration¶
Plugins can declare configuration settings (environment variables) in their package.json. The system reads these declarations at build time and resolves values from the server environment at runtime.
Declaring Config Keys¶
Add a klangk.config section to your plugin's package.json:
{
"name": "@klangk/my-plugin",
"klangk": {
"config": {
"MY_PLUGIN_URL": {
"description": "URL for the my-plugin backend",
"default": "http://localhost:8080",
"scope": "frontend"
},
"MY_PLUGIN_API_KEY": {
"description": "API key for my-plugin",
"default": "",
"scope": "container"
}
}
}
}
Each key in the config object is an environment variable name. Fields:
| Field | Required | Description |
|---|---|---|
description |
No | Human-readable description of the setting |
default |
No | Default value if the env var is not set (defaults to "") |
scope |
No | Where the value is delivered (defaults to "container") |
Scopes¶
The scope field controls where the resolved value is made available:
container— Injected as an environment variable into workspace containers at startup. Available to Pi extensions viaprocess.env.VAR_NAMEand to any process running in the container.frontend— Included in theGET /api/configresponse as a lowercased key (e.g.,MY_PLUGIN_URL→my_plugin_url). Available to Dart plugins in the browser.both— Delivered to both containers and the frontend.
Setting Values¶
Values come from the server environment — admins set them in .env or as system environment variables, the same as all other Klangk configuration:
If an environment variable is not set, the default from the plugin manifest is used.
How It Works¶
- Startup: The backend scans
$KLANGK_PLUGINS_DIR/*/package.jsonforklangk.configentries and resolves each declared key from the server environment (with fallback to declared defaults). - Container creation: Keys with
scope: "container"or"both"are injected as env vars into workspace containers alongside system env vars likeKLANGK_BRIDGE_URL. - Frontend requests: Keys with
scope: "frontend"or"both"are included in theGET /api/configresponse. Dart plugins can fetch this endpoint to discover their configuration.
Example: Accessing Config in a Dart Plugin¶
import 'package:http/http.dart' as http;
import 'dart:convert';
// In your plugin's initialization:
final resp = await http.get(Uri.parse('$baseUrl/api/config'));
final config = jsonDecode(resp.body) as Map<String, dynamic>;
final myUrl = config['my_plugin_url'] as String? ?? '';