---
name: BB Plugin Authoring
slug: bb-plugin-authoring
category: AI Engineering
description: BB Plugin Authoring covers writing, building, and installing bb plugins, from scaffolding a new plugin to extending bb with CLI commands, agent tools, background services, settings, panels, mention providers, and other surfaces. Use it whenever the task is to create a bb plugin, extend bb itself, or add a bb surface via a plugin.
github: "https://github.com/get-bb/bb/tree/main/apps/server/src/services/skills/builtin-skills/bb-plugin-authoring"
language: TypeScript
stars: 2274
forks: 253
install: "npx degit https://github.com/get-bb/bb/tree/main/apps/server/src/services/skills/builtin-skills/bb-plugin-authoring ~/.claude/skills/bb-plugin-authoring"
installs_to: ~/.claude/skills/bb-plugin-authoring
source_path: apps/server/src/services/skills/builtin-skills/bb-plugin-authoring/SKILL.md
collection_size: 14
category_size: 2451
collection_url: "https://dirskills.com/collections/get-bb/bb"
added: 2026-08-18T06:58:22.825Z
last_synced: 2026-08-18T06:58:22.825Z
canonical_url: "https://dirskills.com/skills/bb-plugin-authoring"
---

# BB Plugin Authoring

BB Plugin Authoring covers writing, building, and installing bb plugins, from scaffolding a new plugin to extending bb with CLI commands, agent tools, background services, settings, panels, mention providers, and other surfaces. Use it whenever the task is to create a bb plugin, extend bb itself, or add a bb surface via a plugin.

**Install:**

```bash
npx degit https://github.com/get-bb/bb/tree/main/apps/server/src/services/skills/builtin-skills/bb-plugin-authoring ~/.claude/skills/bb-plugin-authoring
```

## README

# Authoring bb plugins

A bb plugin is a TypeScript package running in-process inside the bb server.
Its backend entry default-exports a factory that receives the full plugin API
(`bb`); an optional frontend entry registers React UI inside the bb app; an
optional host entry is bundled and runs as a supervised Node worker on targeted
enrolled hosts. Plugins are full-trust code in every runtime.

Plugins are on by default. Builtin plugins ship with bb; a few sit behind
their own product gates. `bb plugin list` shows each plugin's status.

## Quickstart

```
bb plugin new hello            # scaffolds ./bb-plugin-hello (add --app for a frontend entry)
cd bb-plugin-hello
bb plugin install .            # registers the directory in place (--yes to skip the prompt)
bb plugin dev                  # rebuild app/host bundles + reload on every save
```

The manifest is `package.json`:

```json
{
  "name": "bb-plugin-hello",
  "version": "0.1.0",
  "type": "module",
  "engines": { "bb": ">=0.9", "bbPluginSdk": ">=0.4.3" },
  "bb": {
    "name": "Hello",
    "description": "A friendly example plugin.",
    "branding": { "icon": "Zap" },
    "server": "./server.ts",
    "app": "./app.tsx",
    "host": "./host.ts",
    "skills": ["skills"]
  }
}
```

- `bb.server` (required) — backend entry. Path installs load it as
  TypeScript directly (no build step); `bb plugin build` also emits a
  self-contained `dist/server.js` + `server.meta.json` that git/npm installs
  prefer when its SDK major matches, so consumers never need npm or
  node_modules. `bb.app` (optional) — frontend entry compiled by
  `bb plugin build` into `dist/app.js` + `app.css` + `app.meta.json`; path
  and git installs build it automatically at install time. Git installs also
  run `npm install --omit=dev` first (so a git plugin may use third-party
  packages) and keep node_modules, since bundling cannot inline data files read
  at runtime. So every package your source imports that bb does not shim
  belongs in `dependencies`: a build-required package left in
  `devDependencies` makes the plugin uninstallable from git, and unbuildable
  after any install that omits dev deps — including the packaged CLI's own,
  which runs npm under `NODE_ENV=production`. `devDependencies` is for types
  and tooling only.
- `bb.host` (optional, singular) — full-trust Node 22 ESM entry bundled into
  `dist/host.js` + source map + `host.meta.json`. Its owning server entry calls
  it through typed host RPC. The daemon downloads it lazily, verifies its
  digest, and reuses one worker per plugin generation. Pure JavaScript
  dependencies are bundled; host code may use Node APIs such as
  `child_process`, `fs`, and `fetch`.
  Installing or updating a git plugin needs `npm` on PATH; checking for
  updates does not, because a check reads the manifest and never builds. Path
  installs build from dependencies you have already installed.
- Building yourself (CI, or verifying a build without a running bb): add
  `bb-app` to `devDependencies` and set `"build": "bb plugin build"`.
  `bb plugin build` needs no server, and depending on `bb-app@X` builds
  against exactly that release's shim configuration. bb downloads its build
  toolchain on first use, so cache `<dataDir>/plugins/toolchain-*` in CI.
- `bb.skills` (optional) — relocates the auto-imported skills directories
  (default `skills/`; `[]` opts out). Every `skills/<name>/SKILL.md` is
  injected into agent threads as the plugin skills tier.
- `bb.themes` (optional) — contributes palettes to Settings → Appearance and
  `bb theme list`. Each entry is
  `{ id, name, description?, css: "./themes/name.css", codeTheme? }`;
  `codeTheme` is `{ dark?, light? }` where each side is a bundled Shiki /
  Pierre name or a plugin-relative VS Code theme `.json` file. bb namespaces
  its selectable id as `plugin:<plugin-id>:<id>`. Only loaded plugins
  contribute.
- `bb.name` and `bb.description` (required) — non-empty human-facing plugin
  identity. The top-level package `name` remains the package identity and
  source of the plugin id.
- `bb.branding` (required) — declare `bb.branding.icon` as either the plugin's
  canonical BB icon name, such as `Zap`, or a plugin-relative compact SVG path
  such as `./assets/icon.svg`. BB validates and hash-serves path-shaped SVGs,
  then renders them as CSS masks so their shape inherits the surrounding text
  color; SVG colors are ignored. BB reuses this icon on roomy surfaces when no
  logo override is declared. Add `logo.light` only for
  intentionally different rich/full-size identity artwork; optional
  `logo.dark` is preferred in dark mode. Logo paths are explicit
  plugin-relative `.svg`, `.png`, or `.webp` files: nulls, empty strings,
  missing/escaping files, unsupported extensions, and a dark logo without a
  light logo fail the manifest. There is no root logo auto-detection. Logo-only
  manifests remain supported for compatibility, so at least an icon or light
  logo is required. BB uses a declared logo where space permits, such as roomy
  Settings rows and cards.
  Compact sidebar, menu, action, mention, and panel-title surfaces prefer the
  plugin-owned icon asset, then a named manifest icon, then a contribution's
  local `icon` hint, then Zap. Branding changes are picked up on
  `bb plugin reload`. Named inline icons use `currentColor`; compact SVG assets
  should contain only the intended transparent glyph shape. Do not duplicate
  the same artwork across `icon` and `logo`; reserve logos for intentionally
  different branded artwork and provide a dark variant when needed.
- `engines.bb` — optional semver range checked against the bb app version.
- `engines.bbPluginSdk` — optional semver range for the plugin SDK surface
  (currently `0.4.3`; the scaffold writes `">=0.4.3"`). bb reads it as a floor,
  not a ceiling: a later SDK in the same major still loads the plugin, so a
  caret range keeps working after the SDK moves forward. Absent means a legacy
  manifest. Managed (`git:`/`npm:`) installs **refuse** a plugin that needs a
  newer SDK than the host provides, or one pinned to a different major; path
  installs surface it as `incompatible` at load.
  Compatible updates (`bb plugin outdated` / `bb plugin update`) only select
  candidates that satisfy these ranges; newer incompatible releases are
  reported as blocked rather than applied. Dev builds (bb `0.0.0`) skip
  enforcing `engines.bb` and annotate that on check results.
- **Manual updates:** `bb plugin outdated` checks tracking sources and
  `bb plugin update` applies compatible candidates (reinstall of an already
  installed managed plugin is refused). A failed activation **rolls back** to
  the previous state snapshot and records the failure for the user. Keep
  `engines.*` honest and ship load-safe factories so an update never strands
  users.
- `bb plugin build` stamps authoritative metadata into every declared
  artifact's `dist/*.meta.json`: `sdkMajor`, `sdkVersion`,
  `artifactFormatVersion` (currently `1`), `pluginId`, `pluginVersion`, and
  `builtWith: { bbVersion, pluginSdkVersion }`. Managed installs reject
  artifacts whose `pluginId`/`pluginVersion` disagree with the package
  manifest, or whose SDK major does not match the host.
- Default to `bb-plugin-hello` for the package name. Scoped names such as
  `@acme/bb-plugin-hello` are also supported. The plugin id is the final
  package-name component minus the `bb-plugin-` prefix, so both forms use
  `hello`; it namespaces routes, storage, settings, and CLI commands. Builtin
  ids such as
  `automations`, `connect`, `custom-instructions`, `inline-vis`, and `secrets`
  cannot use a non-`builtin:` source — use `builtin:<name>` instead.

Backend API imports normally stay type-only;
the root runtime exports are `defineRpcContract`, supplied by BB for shared
schema contracts, and the numeric `PLUGIN_CLI_OUTPUT_MAX_BYTES` ceiling:
`import { defineRpcContract, type BbPluginApi } from
"@get-bb/plugin-sdk"`. Validator imports such as Zod are normal plugin runtime
dependencies (and are bundled by `bb plugin build`).

On-disk state per plugin: `<dataDir>/plugins/<id>/data.db` (its SQLite),
`secrets/` (secret settings + HTTP token), `logs/plugin.log` (JSONL,
rotated at 5MB). Settings edits never auto-reload — `bb plugin reload <id>`
after configuring.

## Looking up the exact API

This skill is a guide, not the contract. For an exact signature or a symbol it
does not cover:

1. **`bb plugin types`**, run in the plugin directory (or given its path),
   syncs that plugin's SDK surface to the running bb — no server needed. For a
   plugin that depends on the npm package it repins the exact
   `@get-bb/plugin-sdk` devDependency to this bb's SDK version (run
   `npm install` after); for an older plugin that still vendors `types/*.d.ts`
   it rewrites those declarations. Either way a cloned or older plugin can be
   thousands of lines behind. `--check` reports a mismatch without writing;
   `bb plugin build` and `bb plugin dev` keep things in step too.
2. **Read the bundled declarations** — the authoritative surface, ~13,000
   lines of readable declarations with doc comments:
   - plugins scaffolded by a current bb depend on the npm package, so after
     `npm install` read
     `node_modules/@get-bb/plugin-sdk/bundled-types/bb-plugin-sdk.d.ts`
     (`bb-plugin-sdk-app.d.ts` for frontend symbols and
     `bb-plugin-sdk-host.d.ts` for the host entry);
   - plugins scaffolded before that still carry the root declaration in
     `types/bb-plugin-sdk.d.ts` (plus `types/bb-plugin-sdk-app.d.ts` for an
     app), which the plugin's `tsconfig.json` maps
     `@get-bb/plugin-sdk` onto. Read whichever the plugin in front of you has.
     That layout still works for existing entries, but migrate before adding
     `bb.host` so the `/host` and `/testing/host` subpaths are present; `bb
plugin migrate` converts such a plugin to the npm package (it prints the plan
     and asks first, and needs `--yes` when stdin is not a terminal). Never
     migrate a plugin the user did not ask you to migrate.
3. **`git clone --depth 1 https://github.com/get-bb/bb`** for host behavior or
   a reference implementation: `packages/plugin-sdk/src/`,
   `apps/server/src/services/plugins/`, `plugins/`.

Never answer an API question from a built bundle — `dist/*.js` and the bb app's
own JavaScript are minified. If you are grepping minified JavaScript, go back
to step 1.

## Distributing a plugin

Users can install third-party plugins directly from a local path, npm package,
or Git repository:

```sh
bb plugin install ./bb-plugin-notes
bb plugin install npm:bb-plugin-notes@^1.0.0
bb plugin install https://github.com/acme/bb-plugin-notes
bb plugin install git:https://github.com/acme/bb-plugin-notes.git@main
bb plugin install git:https://github.com/acme/bb-plugin-notes.git@^1.2.0
```

A bare HTTP(S) repository URL tracks its default branch. Use the `git:` form
with an explicit branch, tag, or commit when that tracking intent matters.

### Releasing a git plugin with semver tags

Tag each release `vX.Y.Z` and users can install a range instead of a ref:
bb reads the repository's tags, installs the highest release the range allows,
and `bb plugin update` moves them to later releases in the same range.
Prereleases stay out unless the range names one. Give each plugin of a
multi-plugin repository its own tag prefix — `notes/v1.2.3` — and users add
`--tag-prefix notes/`.

bb records the tag it installed together with the commit that tag pointed at,
and refuses the plugin if that tag is ever moved to another commit. Publish a
fix as a new version rather than retagging.

### Several plugins in one repository

Keep each plugin in its own directory with its own `package.json`, then index
the directories in a `.bb/plugins.json` collection manifest at the repository
root:

```json
{
  "$schema": "https://getbb.app/schemas/plugins.schema.json",
  "schemaVersion": 1,
  "name": "acme-plugins",
  "plugins": [
    { "name": "notes", "source": "./plugins/notes" },
    { "name": "status", "source": "./plugins/status" }
  ]
}
```

Each `source` is a repository-relative directory that starts with `./`. The
file is an index only — it never overrides a plugin's identity, branding,
entry points, or engine ranges. Users install one plugin at a time:

```sh
bb plugin install git:https://github.com/acme/bb-plugins.git@main --plugin notes
bb plugin install git:https://github.com/acme/bb-plugins.git@main --subdirectory plugins/notes
bb plugin install path:. --plugin notes
```

`--subdirectory` works without a collection manifest; `--plugin` resolves an
entry name from it. If the repository is not itself a plugin, an install with
neither flag fails and lists the entry names.

### Publishing your own marketplace

A marketplace is one `marketplace.json` file. It lists plugins with their
store branding and their npm or git source; it never hosts plugin code, and
installing an entry runs the same install pipeline a direct install runs.

```json
{
  "$schema": "https://getbb.app/schemas/marketplace.schema.json",
  "schemaVersion": 1,
  "name": "acme-plugins",
  "displayName": "Acme Plugins",
  "description": "Plugins the Acme team maintains.",
  "plugins": [
    {
      "id": "notes",
      "displayName": "Notes",
      "description": "Keep notes beside a thread.",
      "icon": { "url": "./icons/notes.svg" },
      "tags": ["notes", "interface"],
      "author": { "name": "Acme", "github": "acme", "url": "https://acme.dev" },
      "engines": { "bb": ">=0.0.34" },
      "source": {
        "git": {
          "url": "https://github.com/acme/bb-plugins.git",
          "subdir": "plugins/notes",
          "range": "^1.0.0",
          "tagPrefix": "notes/"
        }
      }
    }
  ]
}
```

The schema is strict: an unknown field rejects the whole document, and the
last catalog bb validated keeps serving. `name` is the marketplace's identity
and must be unique on the user's machine; `bb-community` is reserved. `engines`
may narrow a plugin manifest's ranges and never widen them. Icons are `.svg`,
`.png`, or `.webp`, either an absolute https URL or a path relative to the
manifest — bb fetches and validates them server-side and serves them from its
own origin.

Host it three ways, and users add whichever fits:

```sh
bb marketplace add https://plugins.acme.dev/marketplace.json
bb marketplace add git:github.com/acme/bb-marketplace@main
bb marketplace add path:/work/acme-marketplace
```

An https marketplace is re-read with a conditional request; a git one is
cloned into a throwaway checkout each refresh, with `marketplace.json` and any
relative icons read from the repository root. Prefer git tag ranges over
pinned refs so a release reaches users without a catalog change. Before
installing from a marketplace that is not `bb-community`, bb resolves and shows
the true source — including the exact release tag and commit a range lands
on — so keep your listed URL, subdirectory, and range honest.

BB's own official plugins are separate: inclusion in the `bb-community`
marketplace is a BB release decision, not part of the plugin authoring
workflow, and the bundled official plugins ship inside the app itself and
install from that local copy with no network fetch.

## The backend factory

```ts
import type { BbPluginApi } from "@get-bb/plugin-sdk";

export default async function plugin(bb: BbPluginApi) {
  // Register surfaces here. Load-safe: settings, storage, http, rpc,
  // realtime, background, cli, agents, ui, events, status, onDispose.
  // bb.sdk works here in the real server, but prefer it in handlers/services
  // (bind-gated — see below).
}
```

The factory runs at load/reload/enable (time-boxed 30s). A throwing initial
factory puts the plugin in `error` status with the message as the detail; a
throwing reload candidate leaves the prior registration set running and
reports the reload failure in its detail. `bb.pluginId` is the plugin's own id.

Keyed registrations must be unique within one factory execution: duplicate
settings, routes, rpc methods, services, schedules, CLI registrations, tools,
instruction providers or mention providers are rejected.
Listeners are different: `bb.events.on`, settings `onChange`, and `onDispose`
are additive, so registering multiple listeners is supported.

### bb.log

`bb.log.debug|info|warn|error(message: string)` — goes to the server log
(prefixed `[plugin:<id>]`) and to the per-plugin JSONL file behind
`bb plugin logs <id> [-n N] [-f]`.

### bb.settings

`bb.settings.define(descriptors)` declares plain-data descriptors (rendered
in Extensions → Plugins and editable via `bb plugin config <id> set <key>
<value>`). Four descriptor types:

```ts
const settings = bb.settings.define({
  apiKey: { type: "string", label: "API key", secret: true }, // 0600 file, never in db or frontend
  teamKey: { type: "string", label: "Team", default: "" },
  mode: {
    type: "select",
    label: "Mode",
    options: ["fast", "slow"],
    default: "fast",
  },
  verbose: { type: "boolean", label: "Verbose", default: false },
  project: { type: "project", label: "Project" }, // project picker, stores a proj_* id
});
const { apiKey, teamKey } = await settings.get(); // load-safe; re-read inside handlers for freshness
settings.onChange((next, prev) => {
  /* fires after a settings save */
});
```

Typing rule: a descriptor **with** `default` yields a non-optional value
from `get()`; without one the value is `string | boolean | undefined` — so
give non-secrets defaults and handle missing secrets explicitly.

### bb.storage

- `bb.storage.kv` — namespaced JSON key-value rows in bb.db:
  `get<T>(key)`, `set(key, value)`, `delete(key)`, `list(prefix?)`. Values
  are capped at **256KB each** — kv is for cursors, links, and small state;
  caches and datasets go in the plugin database.
- `bb.storage.database()` — the plugin's own better-sqlite3 database at
  `<dataDir>/plugins/<id>/data.db` (WAL, busy_timeout 5000). Handles are
  host-tracked and closed on reload; a closed handle throws.
- `bb.storage.migrate(db, statements)` — statement index = migration id;
  unapplied statements run in one transaction. **Append-only**: never
  reorder or edit shipped statements, only push new ones.

```ts
const db = bb.storage.database();
bb.storage.migrate(db, [
  `CREATE TABLE IF NOT EXISTS issues (id TEXT PRIMARY KEY, title TEXT NOT NULL)`,
]);
```

### bb.server

Read-only facts about the running server. `bb.server.loopbackBaseUrl` is the
server's own loopback base URL (e.g. `http://127.0.0.1:38886`), which serves
the SPA + `/api` + `/ws` — for plugins that proxy or relay traffic back to
the server itself (the builtin connect plugin's tunnel is the canonical
user). **Bind-gated** like `bb.sdk`: reading it before the server is
listening throws, so prefer reading it from handlers, services, and timers.

### bb.hosts

For a plugin with a singular `bb.host` entry, define one runtime contract
shared by the server and host modules:

```ts
// contract.ts
import {
  defineRpcContract,
  type ExperimentalHostSignals,
} from "@get-bb/plugin-sdk";
import { z } from "zod";

export const hostContract = defineRpcContract({
  setEnabled: {
    input: z.object({ enabled: z.boolean() }).strict(),
    output: z.object({ enabled: z.boolean() }).strict(),
  },
});

export const hostSignals = {
  changed: {
    payload: z.object({ reason: z.string() }).strict(),
  },
} satisfies ExperimentalHostSignals;
```

The host entry default-exports its implementation:

```ts
// host.ts
import { experimental_defineHostEntry } from "@get-bb/plugin-sdk/host";
import { hostContract, hostSignals } from "./contract.js";

export default experimental_defineHostEntry({
  contract: hostContract,
  experimental_signals: hostSignals,
  handlers: {
    setEnabled: async ({ enabled }, context) => {
      await setEnabled(enabled, context.signal);
      await context.experimental_emitSignal("changed", {
        reason: "setting-appl
