kobe

Writing kobe plugins

The developer-facing reference: everything a plugin can declare, every event kobe fires, every environment variable it injects, and every way to call back in. Design rationale lives in design/plugins.md and design/plugin-events.md; this page is the contract.

A plugin is a directory with a kobe-plugin.toml manifest plus any argv commands your machine can run: Bash, Node, Bun, Python, Rust, a prebuilt binary. There is no SDK: the whole kobe CLI and the daemon socket are the plugin API. Kobe owns the host surface (install, validation, event dispatch, env injection, panes, settings UI, run logs); you own the implementation.

Quickstart

mkdir my-plugin && cd my-plugin
cat > kobe-plugin.toml <<'EOF'
id = "you.hello"
name = "Hello"
version = "0.1.0"
min_kobe_version = "0.8.24"

[[events]]
on = "agent.turn-complete"
command = ["sh", "-c", "echo \"$KOBE_PLUGIN_TASK_TITLE finished a turn\" >> \"$KOBE_PLUGIN_STATE_DIR/log\""]
EOF

kobe plugin link .            # register your working directory (dev loop)
kobe plugin log you.hello     # inspect hook runs (exit codes, output, timing)

Optional SDK (TypeScript)

The contract above is the API: any language, no SDK required. For TypeScript/JavaScript authors, @sma1lboy/kobe-plugin-sdk wraps that same contract with types and autocomplete (zero deps, Node ≥ 18 or Bun):

import { pluginContext, pluginEvent, notify, Pane, KobeSocket } from "@sma1lboy/kobe-plugin-sdk"

const ctx = pluginContext()   // typed KOBE_PLUGIN_* env
const ev = pluginEvent()      // typed event envelope (null outside [[events]])
  • pluginContext() / pluginEvent(): the env contract, typed.
  • readSettings() / setting(): your [[settings]] values from config .env.
  • kobe() / kobeJson() + notify / dispatch / listTasks / openPane: $KOBE_BIN_PATH callbacks.
  • KobeSocket: daemon socket client: request(name, payload) + live channel subscribe (always role: "pane").
  • Pane: a tiny pane kit for [[panes]] pages: alt screen, raw-mode keys, resize, absolute-addressed draw(lines).
  • PLUGIN_EVENT_NAMES / DAEMON_CHANNELS: the catalogs as typed unions. These are the SINGLE source: the daemon itself imports them from the SDK's ./contract module, so host and SDK can't drift by construction.

Package README has full examples: packages/kobe-plugin-sdk/README.md.

Publish: push a public GitHub repo (one plugin per subdirectory is fine), add the topic kobe-plugin → it appears in the marketplace (kobe.sma1lboy.me/plugins and kobe plugin search) automatically. Users install with kobe plugin install owner/repo[/subdir] and stay fresh with kobe plugin outdated / kobe plugin update --all (an update is a clean reinstall of the managed checkout; config/state survive).

Manifest reference

id = "you.example"               # letters/digits/dot/colon/underscore/hyphen
name = "Example"
version = "0.1.0"
min_kobe_version = "0.8.24"      # install refuses older kobe
description = ""                # optional
platforms = ["macos", "linux"]   # optional; item-level `platforms` overrides

[[build]]                        # runs at GitHub install (after preview confirm), cwd = checkout
command = ["npm", "install"]     # self-provision deps INTO the plugin dir; `link` skips build

[[startup]]                      # once per daemon start, after the socket is ready; one-shot, not a daemon
command = ["node", "restore.js"]

[[actions]]                      # on-demand: kobe plugin action invoke you.example.greet [args…]
id = "greet"                     # local id, no dots; extra CLI args append to argv
title = "Say hello"
command = ["sh", "greet.sh"]

[[events]]                       # async observer fired by the daemon (catalog below)
on = "agent.turn-complete"
command = ["sh", "notify.sh"]

[[panes]]                        # a terminal surface in the task workspace
id = "board"
title = "Board"
placement = "split"              # split (default: joins the focused chattab's
                                 # split group beside the engine) | tab (own tab)
command = ["node", "$KOBE_PLUGIN_ROOT/board.js"]   # cwd = the TASK WORKTREE

[[settings]]                     # rendered as an editor in Settings → Plugins
key = "YOU_EXAMPLE_MODE"         # stored as KEY=value in your config .env
label = "Mode"
type = "enum"                    # string | number | boolean | enum
options = ["fast", "fancy"]
default = "fast"

[[file_handlers]]                # claim Files-pane opens by filename pattern
pattern = "\\.(png|jpg)$"        # JS regex, case-insensitive, vs the file name
action = "greet"                 # your action, invoked with the absolute path

command is always argv: never a shell, no expansion (panes expand only $KOBE_PLUGIN_ROOT). Unknown event names are warnings (forward compat); invalid types/patterns are install-time errors.

Event catalog

Declare [[events]] hooks; each fire runs your command with the envelope in KOBE_PLUGIN_EVENT_JSON. Events are asynchronous observers. Your exit code and output never block or change what happened. Support: C = Claude Code, X = Codex (Kimi adapter pending).

EventFires whenDetail highlights
task.created / task.deletedtask appears/disappears in the indextask context
task.landeda task's branch merged back into its base repostrategy, landedOn, commit
task.archiveda task was archived (restores don't fire)task context
worktree.createda task's worktree materializedtask context
issue.changeda daemon-tracker issue mutated (create/edit/status)repo, op
task.opened / project.openedthe user selects/enters a task / project row
file.will-open / file.opened / file.closedFiles-pane open, before/after; editor tab closedpath, via: plugin|editor|external
tab.opened / tab.closeda workspace tab appeared/went away (restores don't fire)tabId, kind, title, vendor, purpose
agent.running / agent.idle / agent.turn-complete / agent.permission-needed / agent.rate-limited / agent.erroractivity-STATE transitions, deduped per task+tab
session.start / session.endengine session lifecycle (C; X start only)
turn.prompt / turn.complete / turn.failed / turn.interruptedone event per turn edge (C, X; interrupted: Kimi-shaped)failure class on failed
tool.pre / tool.post / tool.failedevery tool call (C, X; failed: C); installed into engine config only while some enabled plugin subscribestool.name, tool.id
attention.permission / attention.questionthe engine blocked on a human (C)waiting
context.pre-compact / context.post-compactcontext compaction (C, X)compact.trigger: manual|auto
subagent.start / subagent.stopnested agent lifecycle (C)subagent.type/id

Envelope (KOBE_PLUGIN_EVENT_JSON):

{
  "event": "tool.post",
  "taskId": "",                 // when the event mapped to a task
  "task": { "id", "title", "repo", "branch", "worktreePath", "vendor", "status" },
  "vendor": "claude",            // agent-layer events
  "tabId": "", "sessionId": "",// when known
  "detail": { /* per-event, see table */ },
  "at": 1690000000000
}

The principle: any observable product moment is a candidate event. The catalog grows as subsystems expose their edges (PR status and task status transitions are natural next ones). If your plugin needs a moment that isn't fired yet, ask via kobe feedback or a GitHub issue; the plumbing (ui.reportEvent → plugin sink) makes additions cheap.

Environment contract

Every plugin command gets, on top of the user's environment:

VariableMeaning
KOBE_BIN_PATHexec this to call back into kobe
KOBE_SOCKET_PATHdaemon unix socket, for raw JSON requests
KOBE_HOME_DIRset when kobe runs against a non-default home (keep passing it through)
KOBE_PLUGIN_ID, KOBE_PLUGIN_ROOTwho you are, where your files are
KOBE_PLUGIN_CONFIG_DIRuser-editable config (.env etc.); survives reinstall
KOBE_PLUGIN_STATE_DIRyour durable state; survives reinstall
eventsKOBE_PLUGIN_EVENT, KOBE_PLUGIN_EVENT_JSON, KOBE_PLUGIN_TASK_ID, KOBE_PLUGIN_TASK_TITLE
startupKOBE_PLUGIN_EVENT=startup
actionsKOBE_PLUGIN_ACTION_ID, KOBE_PLUGIN_INVOKE_CWD (where the user invoked, usually "the repo I mean")
panesKOBE_PLUGIN_ENTRYPOINT_ID; cwd is the task worktree

Never write durable state under KOBE_PLUGIN_ROOT. GitHub installs are managed checkouts replaced on reinstall. Settings you declare in [[settings]] arrive as plain vars in your config .env; source it (. "$KOBE_PLUGIN_CONFIG_DIR/.env") or read it yourself.

Calling back into kobe

CLI (recommended, portable): exec $KOBE_BIN_PATH with any command. The high-value verbs live under kobe api: machine-readable list via kobe api schema, human list via kobe api help. Highlights:

"$KOBE_BIN_PATH" api add --repo <dir> --title T --prompt ""   # create task + start engine
"$KOBE_BIN_PATH" api dispatch --task-id ID --prompt ""        # text into a live session
"$KOBE_BIN_PATH" api list                                      # all tasks (JSON)
"$KOBE_BIN_PATH" api notify --title "done"                     # toast in every attached UI
"$KOBE_BIN_PATH" api issue-create --repo <dir> --title ""     # daemon issue tracker
"$KOBE_BIN_PATH" api prompt --title "URL?"                     # host input dialog → {value}|{cancelled}
"$KOBE_BIN_PATH" api read-output --task-id ID                  # structured session reads
"$KOBE_BIN_PATH" plugin pane open you.example.board            # open your own pane

Socket (advanced): newline-delimited JSON frames on KOBE_SOCKET_PATH ({"type":"request","id":"1","name":"task.list","payload":{}}); request names and payloads in packages/kobe-daemon/src/daemon/protocol.ts. Prefer the CLI unless you need push channels.

Interaction surfaces

  • ctrl+e picker: every enabled plugin's panes are listed by title; picking one opens it with your declared placement.
  • User keybindings: users bind chords themselves in ~/.kobe/settings/keybindings.yaml: plugins: { ctrl+b: pane:you.example.board, f6: action:you.example.greet }. Ship the suggestion in your README; kobe ships no default plugin chords.
  • Files pane: [[file_handlers]] claims opens by pattern.
  • Host input dialog: kobe api prompt --title "…" (SDK: promptUser()) pops the TUI's standard input dialog and blocks for the answer: {value} on submit, {cancelled, reason} on esc/timeout. Use it instead of hand-rolling in-pane prompts.
  • Settings → Plugins: enable/disable, declared surfaces, last run, and your [[settings]] editors.
  • CLI: kobe plugin action invoke, kobe plugin pane open, kobe plugin log.

Ground rules

  • Hooks must be fast and silent. Event hooks run on real product moments; do your slow work detached. Exit non-zero only for real failures; output is capped at 8 KB per run in log.jsonl.
  • Never block. Events are observers; there is no veto surface. Blocking tweaks (deny a tool call) belong in engine-native hooks the user installs directly.
  • Trust model: plugins run as the user with their environment; installs preview every command and build step first, but nothing is sandboxed. Keep your repo auditable. That's what gets you installed.
  • Reference implementations: the first-party plugins in Sma1lboy/kobe-plugins (notifications, GitHub/Linear task starters, lazygit pane, Chromium pane, the character-cell video player).

On this page