docs blog library
github

Plugins

Jazz plugins are optional, pre-bundled JavaScript modules that extend the harness. A plugin may contribute any mix of capabilities:

  • tools — model-callable functions that join the agent’s tool set;
  • workspace context — bounded, current context supplied before a model request as the agent reads or changes files;
  • commands — user-invoked /name slash commands;
  • personas — selectable agent personalities;
  • skills — loadable instruction documents;
  • advisory hooks — route.skills (suggest a skill before the first model request), compact.tools (choose which stale tool results to prune during compaction), classify.page (flag a browser page for extra scrutiny), and route.snapshot (put the relevant elements of a browser snapshot first), which only shape context or add scrutiny;
  • a policy hook — classify.command-risk, which can affect whether the active approval policy requires a person to approve one shell command.

Tools and commands run code and are gated accordingly; personas and skills are inert declared data. Advisory hooks cannot authorize a tool, change approval policy, or act on the model’s behalf; the policy hook shapes approval only.

Install a plugin for one agent

Browse the Marketplace for reviewed plugins. For example, the LSP plugin adds code navigation and compiler diagnostics:

jazz plugin add com.jazz.plugins.lsp
jazz plugin inspect com.jazz.plugins.lsp
jazz plugin trust com.jazz.plugins.lsp
jazz plugin enable com.jazz.plugins.lsp --agent <agent-name>

Review the code, declared capabilities, network destinations, and secrets before trusting. Omit --agent only when you want the plugin enabled for every agent, including future agents. Plugins are disabled until you trust and enable them.

Turn plugins on and off from chat

/plugins opens a checklist of installed plugins. A plugin is checked when it runs for the current agent, and the highlighted one shows its version, what it adds (tools, commands, skills, personas), the hooks it handles, its network destinations, the data it sends, and what you have granted it. Change the checks and press Enter; Esc changes nothing. Then choose where the change applies:

  • This conversation: nothing is saved. The change ends when you start or resume another conversation, or quit. Use it to try a plugin, or to silence one for a single task. The checklist tags such a plugin on · temporary or off · temporary.
  • Always: saved for every agent, the same as jazz plugin enable <id> and jazz plugin disable <id> without --agent.

Turning a plugin on asks to trust its code and consent to its declared access the first time, in either scope, exactly as jazz plugin trust and jazz plugin enable do, and asks for any required secret it cannot find. It is refused when another active plugin already handles one of its hooks. Its tools, commands, and skills apply from the next message.

Trust means code execution

Plugins run inside the Jazz process with the authority of the operating-system user. The manifest’s hook, network, data, and secret declarations support review and consent; they are not a sandbox. A plugin can technically read files or environment variables, access the network, block Jazz, or terminate the process without using the host API.

For that reason installation, code trust, data-egress consent, and per-agent enablement are separate steps. Without --yes, first-time trust and new consent are granted only from a local interactive terminal: a headless run cannot show the confirmation prompt. --yes is itself the operator’s consent, so jazz plugin trust --yes and jazz plugin enable --yes work from a script, a pipe, or CI. Chat and unattended surfaces report the required local command instead.

# GitHub is the default source: no author build, pack, or release step.
jazz plugin add owner/repo            # latest default branch
jazz plugin add owner/repo@v1.2.3     # pin a branch, tag, or commit
jazz plugin inspect com.example.router
jazz plugin trust com.example.router
jazz plugin enable com.example.router --agent default
# Or enable for every agent, including agents created later:
jazz plugin enable com.example.router

If a lifecycle command names a plugin that is not installed, Jazz shows an installation command, for example jazz plugin add com.jazz.plugins.lsp. Install it first, then inspect, trust, and enable it; the error does not install or enable anything automatically.

jazz plugin add owner/repo downloads the repository tarball over HTTPS (no local git), extracts it, and hashes the source tree; that hash is the digest you trust. A local directory holding a jazz-plugin.json installs the same way (jazz plugin add ./my-plugin). A curated catalog id, an HTTPS manifest URL, or a locally packed ./release/catalog-entry.json still install as bundled artifacts (see Authoring).

What the digest pins

The digest is only worth trusting if it covers every byte the plugin can load, so add refuses a source that could run code the digest does not cover:

  • The manifest entry must be a plain relative path inside the plugin (no .., no absolute path), and it must be a regular file.
  • The tree may hold only regular files and directories. A symlink, hard link, device, or FIFO anywhere in the tree (or in the GitHub tarball) fails the install. node_modules and .git are skipped by name and are not part of the digest.
  • Jazz reads every import, re-export, require, and literal dynamic import reachable from the entry without running any of it. Each must be a Node or Bun built-in or a file inside the plugin. Imports that reach outside the plugin (../, an absolute path, a tsconfig alias pointing out) are refused, and so are package imports, because packages resolve from node_modules, which the digest does not cover.
  • A bundled artifact may import only Node or Bun built-ins.

The GitHub download stays on api.github.com and codeload.github.com over HTTPS, times out after 60 seconds, and is capped at 32 MB compressed and 128 MB extracted.

Community plugin discovery

Public repositories can opt into Jazz’s metadata-only community directory by adding the exact GitHub topic jazz-plugin and a valid root jazz-plugin.json. Jazz’s scheduled index refresh reads repository metadata, resolves the default branch to a full commit SHA, and fetches only that JSON manifest. It never checks out, builds, imports, or executes community repository code.

Community entries are labeled community-indexed — not reviewed by Jazz. Their declared hooks, network destinations, data classes, secrets, tools, and commands are disclosures supplied by the repository, not independent security findings. Jazz shows the observed commit and emits an install command pinned to it:

jazz plugin add owner/repo@<commit-sha>

The normal local lifecycle still applies: installation stores and hashes the source, then the operator must inspect, trust, grant egress consent, and enable it. Community entries are kept out of the reviewed artifact catalog and cannot be installed by reviewed catalog id.

add stores the source or bytes but never imports them. Jazz imports a module lazily only for a run whose agent has enabled it — per agent, or for all agents — and whose exact code and consent digests are still granted. Before each run it repeats every install check on the installed copy (entry, hash, file types, and import graph) and refuses to load code that no longer matches its trusted digest, including a file swapped for a symlink after you trusted it.

An enabled route.skills plugin ranks the live skills for the turn, and Jazz adds a short, non-authoritative relevance hint for the top skill to the first provider request when it beats the no-skill option. The hint is transient provider context: it never enters durable history, resume state, work state, or telemetry, and the plugin can never load a skill, change tools, or authorize anything. Any error or abstention falls back to deterministic behavior, and routing is skipped for resumes and summarizer runs.

Tool-compaction hook

compact.tools runs before summarization: automatically at the clear rung of the context ladder — once a run passes 50% of its context window, and on every iteration above it — and as a lossless pre-pass when you invoke /compact yourself (which otherwise jumps straight to the summarizer). Below 50% nothing is touched. For each old, large tool result it decides keep / truncate / drop; Jazz applies the decision by replacing content (never removing a message, so assistant/tool pairing stays valid) and only ever sends the result preview, not the whole body. The policy is asymmetric — a result is dropped only on a confident signal, a large uncertain one is truncated to head and tail, and anything else is kept. If the plugin abstains, times out, or is absent, Jazz falls back to its deterministic tool-result clearer. It never touches user or assistant text.

Because this happens quietly mid-run, Jazz surfaces it: the first time a run reclaims space this way it prints a one-line green notice crediting the plugin, and the individual keep / truncate / drop decisions are written to the log (Compaction plugin tool-result decisions) at both the clear rung and /compact. /compact additionally shows the decisions live and names the plugin as it works.

Browser page hooks

Two advisory hooks serve the browser tools.

classify.page runs on each browser_snapshot. It receives the page’s origin (scheme and host, never the path or query), its title, up to 200 interactive elements as role and label pairs, and whether the page has a password or payment card field. It returns a probability for each of four flags: credential-entry, payment, captcha, and agent-directed-instructions. A flag at or above 0.5 adds scrutiny: browser_act asks on every call on that page, even under the high-risk policy or an allowlist, and the approval and the snapshot carry a warning that Jazz words from the flag names alone. Jazz rebuilds the answer from those two fields, so nothing else a plugin returns can reach a decision, and no answer can lower a risk, skip an approval, remove a flag the page’s own structure raised, or clear the untrusted-content marking.

route.snapshot runs on the first window of a snapshot. It receives your latest request, the origin, and the same elements, and returns a probability for each element plus one for none. Up to eight elements at or above 0.1 are listed first under “Likely relevant to your request:”. The whole outline follows unchanged, so a wrong answer costs attention and hides nothing.

Neither hook receives page text. Both time out after two seconds like the other advisory hooks, and any error, invalid answer, or abstention leaves the snapshot exactly as it would be without them. jazz plugin dev cannot run these two hooks yet.

Command-risk policy hook

classify.command-risk is eligible only for execute_command, whose declared risk is unknown because its arguments determine what it can do. The plugin classifies the proposed command as read-only, low-risk, or high-risk; Jazz validates that result and applies the operator’s approval policy. A lower classification can therefore remove an approval prompt. Enabling this hook is explicit consent to that effect.

The plugin is not the enforcement point. It cannot lower another tool’s declared risk, expand the run’s effective tool set, override a command allowlist, change the selected approval tier, or bypass the shell denylist. Jazz sends the hook only the bounded command string: not conversation history, tool results, environment variables, or file contents. Network-backed manifests must disclose that command-text egress and its exact destination before local consent can be granted.

If the hook is absent, abstains, times out, fails validation, exceeds its budget, or becomes unavailable, Jazz falls back to its built-in command classifier. If classification remains unresolved, the command is treated as high-risk. Plugin failure never silently makes an unknown command safer.

Tools

For a complete optional tool plugin, see the generic LSP plugin, which connects configured language servers to semantic code navigation and approved refactors.

A plugin may contribute model-callable tools. Each tool is declared in the manifest — name, description, a JSON Schema for its arguments, a riskLevel (read-only / low-risk / high-risk), and whether calling it sends model-authored content off the machine (egress) — and the module supplies the matching handler. The manifest declaration is the reviewed, consented contract; the module can neither register an undeclared tool nor claim a lower risk than declared.

When a plugin is enabled for an agent, its tools join that agent’s tool set automatically — no separate mention in the agent’s config is needed. Jazz namespaces each tool (plugin_<id>_<tool>) so it never collides, advertises the declared JSON Schema to the model, and validates the model’s arguments against that schema before the handler runs. A read-only tool runs directly; anything else becomes an approval-gated tool, so a person confirms it under the active approval policy exactly like a built-in. A handler that throws, times out, or is unavailable returns an error result to the model rather than crashing the run.

Tool handlers receive the agent’s current working directory and an abort signal. A mutating tool may register both prepare and executePrepared: prepare returns an approval message, optional diff, and JSON-serializable prepared data. Jazz persists that data with the approval, including parked runs. Only the hidden execution tool receives it, after approval. The plugin must revalidate any files or remote state that could have changed while approval was pending. Without those callbacks, Jazz shows a bounded argument preview and runs the ordinary handler after approval.

// jazz-plugin.json
{
  "tools": [
    {
      "name": "reverse_text",
      "description": "Reverse the characters of the given text.",
      "parameters": {
        "type": "object",
        "properties": { "text": { "type": "string" } },
        "required": ["text"],
        "additionalProperties": false,
      },
      "riskLevel": "read-only",
      "egress": false,
    },
  ],
}
// src/index.ts
import type { JazzPluginModule } from "@jazz/plugin-sdk";

const plugin: JazzPluginModule = {
  apiVersion: 1,
  register(api) {
    api.tools.register({
      name: "reverse_text",
      handler: (args) =>
        Promise.resolve({ content: [...String(args["text"] ?? "")].reverse().join("") }),
    });
  },
};

export default plugin;

plugins/example-tool in the repository is a complete, minimal example — it contributes one of each capability below.

Commands

A plugin may contribute user-invoked slash commands. Each is declared in the manifest (name + description); the module registers a handler via api.commands.register. When a plugin is enabled, its commands are registered at chat startup, and again when /plugins turns a plugin on or off, and behave like the built-in dynamic commands: /name autocompletes (with a (plugin) badge), and running it invokes the handler, whose returned message becomes your next turn to the agent. Built-in, skill, and MCP-prompt commands win a name collision, so a plugin cannot shadow /help. A command that returns nothing is a quiet no-op.

Personas

A plugin may contribute personas — pure manifest data (name, description, systemPrompt, optional tone/style), no handler or egress. An enabled plugin’s personas appear alongside built-in and custom ones in the wizard, /switch, and jazz persona list, and an agent’s config.persona may name one. A built-in or custom persona of the same name always wins.

Skills

A plugin may contribute skills — inert instruction documents declared in the manifest (name, description, content). They appear in the skill index the model sees; the body is injected only when the model loads the skill, never automatically. A skill of the same name from any other source takes precedence. A plugin skill is instruction-trust surface (it can steer the model), not a capability grant — it cannot itself act or reach the network.

Lifecycle hooks

A plugin may subscribe to host lifecycle events, declared in the manifest (lifecycleHooks) and registered via api.lifecycle.register. Handlers are fire-and-forget observers: they receive a bounded payload (agent id, conversation id, the run’s working directory cwd, and event-specific data) and cannot change what the host does — a throw or timeout is swallowed and never delays or breaks the run. To influence a decision (routing, tool-result pruning, command risk) a plugin uses an advisory or policy hook instead, not a lifecycle event.

EventFires whendata
session-startA chat session starts—
session-endA chat session ends—
user-promptYou submit a promptprompt
run-completeA turn finishesprompt, summary
run-failedA turn errorserror
awaiting-inputJazz is waiting for you—
tool-startA tool call beginstool, toolCallId
tool-endA tool call succeedstoolCallId, durationMs, summary?
tool-errorA tool call failstoolCallId, durationMs, error
permission-requestA tool needs approvaltool, toolCallId, riskLevel?
permission-deniedAn approval is declinedtool, toolCallId, auto
subagent-startA sub-agent is spawnedagentName?, task?
subagent-stopA sub-agent finishesagentName?, durationMs?
compact-startHistory compaction beginsmessages, tokensBefore
compact-endHistory compaction finishesmessagesBefore/After, tokensBefore/After

data fields are bounded (long strings truncated) and may be absent, so always guard (event.data?.summary). The tool, permission, and sub-agent events are delivered on the interactive streaming path.

The handler’s second argument is a context with an AbortSignal and writeTerminalSequence — use the latter to send a terminal escape sequence (for example an OSC notification) to the user’s terminal. The host writes to the controlling terminal directly, so the sequence reaches it even though Jazz’s fullscreen UI owns stdout — plugin authors don’t have to know that detail.

This is what a terminal-notification plugin rides. A Warp notifier subscribes to run-complete (task finished) and awaiting-input (Jazz is waiting on you) and emits an OSC sequence bound to the terminal tab — the same shape as warpdotdev/claude-code-warp. Off a supporting terminal a plugin can instead shell out to a desktop notifier.

api.lifecycle.register({
  event: "run-complete",
  handler: async (event, { writeTerminalSequence }) => {
    const summary = typeof event.data?.summary === "string" ? event.data.summary : "done";
    // OSC 777 desktop notification, delivered to the user's terminal (e.g. a Warp tab).
    writeTerminalSequence(`\u001b]777;notify;Jazz;${summary}\u0007`);
  },
});

Global vs per-agent

Tools attach to the agent whose run registers them. Personas and skills are treated as globally available once a plugin is enabled anywhere — there is no separate per-agent gating for inert data. A plugin turned on or off with /plugins for this conversation applies to every agent in that conversation.

Lifecycle

jazz plugin list
jazz plugin doctor com.example.router
jazz plugin update owner/repo        # re-fetches a source install; omit the source to reuse the recorded one
jazz plugin rollback com.example.router
jazz plugin disable com.example.router --agent default
jazz plugin remove com.example.router
jazz plugin gc

An update keeps one previous artifact for rollback and returns the plugin to pending trust/consent when code or declared data changes. An update also disables the plugin everywhere, including a jazz plugin enable grant for all agents, so the new code runs only after you enable it again. Disablement prevents new dispatch after the state commit. Since JavaScript modules are process-cached, a daemon or bot restart is required to remove already-loaded code, timers, sockets, or global mutations completely.

Plugin state lives under $JAZZ_HOME/plugins/ (normally ~/.jazz/plugins/). Artifacts are stored by SHA-256. State transitions are cross-process locked and atomically committed.

Secrets

A plugin may ask only for secret names declared in its manifest. Resolution is environment first, then Jazz-owned secure storage. jazz plugin enable prompts for any required secret it cannot already resolve and stores it in secure storage, so first-time setup needs no manual export or separate command. Set or clear a stored value later without putting it in shell history:

jazz plugin secret set com.example.router apiKey
jazz plugin secret status com.example.router apiKey
jazz plugin secret forget com.example.router apiKey

remove deletes Jazz-owned plugin secrets unless --keep-secrets is passed. It cannot delete an environment variable and reports when one remains effective.

Authoring

The Plugin SDK README has a complete minimal plugin, development setup, and an overview of the public TypeScript contracts and host API.

jazz plugin init my-router
cd my-router
bun install
bun test
jazz plugin dev . --hook route.skills --input fixtures/request.json
jazz plugin dev . --hook classify.command-risk --input fixtures/command.json
git init && git add -A && git commit -m "my plugin" && git push   # publish

The command-risk fixture is a JSON object such as { "command": "git status" }. Development probing resolves declared environment-backed secrets from the current shell, while keeping the plugin disposable and out of installed state.

To publish, push the repository to GitHub. There is no build, pack, digest, or release step. Users install it with jazz plugin add owner/repo, and Jazz imports the entry (src/index.ts) directly. Keep the plugin dependency-free: it may import Node/Bun built-ins and its own files, and it imports @jazz/plugin-sdk with import type only (the SDK is types, so nothing is left to load at runtime). Write apiVersion: 1 rather than importing the constant. Install refuses any other import; see What the digest pins.

For a plugin that genuinely needs bundled dependencies, jazz plugin pack . still produces a self-contained release/plugin.mjs, its SHA-256, and a catalog entry, installable as a bundled artifact from a local path or HTTPS manifest URL — an opt-in escape hatch, no longer the default.

The official catalog build runs reviewed, locked source without provider credentials and publishes the generated manifest plus its immutable digest-addressed artifact with the Jazz website. Authors never choose the catalog’s authoritative digest.

machine-readable: /docs/configure/plugins.md · /llms.txt · /llms-full.txt