A plugin is a directory with a grid-plugin.json and an entry point that exports activate(context). It runs in its own process, fenced by what its manifest declares, and reaches Grid Console two ways: transition hooks, which can refuse a card's move, and extension points, which are lists that core reads. Writing and sideloading your own is free on every tier, including Free.
A hook is a function. It runs when a card crosses an arrow and answers with a verdict. An extension point is a registry: a plugin contributes a payload once, at activation, and the subsystem that owns the point reads the list. Hooks are named in the manifest's hooks array and points in its points array. The loader refuses a name outside either vocabulary, and the host refuses a registration the manifest did not declare.
A third manifest block, capabilities, states facts rather than code: isolation and errors, read straight off the manifest by the session engine. See manifest, permissions and trust. It follows its own rule about names. A hook or a point outside the vocabulary is a load error; an unrecognised key inside capabilities is logged once and ignored, so a manifest declaring a capability only a newer Grid reads still loads here.
"use strict";
module.exports = {
activate(context) {
context.hooks.on("before:deliver", (ctx) => {
const body = ctx.card.body ?? ctx.body ?? "";
if (!/^## Ticket\b/m.test(body)) {
ctx.block({
reason: "No ticket reference on the card",
fix: "Add a \"## Ticket\" section naming the issue this card resolves.",
});
return;
}
ctx.pass();
});
},
};
With "hooks": ["before:deliver"] in the manifest, that is a complete plugin. The manifest itself is in manifest, permissions and trust.
Every stage change goes through one place in the engine, whether you dragged the card, pressed approve, or the agent wrote status: into the card file. Hooks register on arrow endpoints.
| HOOK | FIRES |
|---|---|
after:<stage> |
When a card leaves that stage. |
before:<stage> |
When a card is about to enter that stage. |
on:attention |
When a card asks for its human, outside any arrow. |
<stage> is one of inbox, prepare, doing, review, deliver, verify and closed. A move from Doing to Review runs after:doing and then before:review, in registration order, and stops at the first block.
| VERDICT | EFFECT |
|---|---|
ctx.pass() |
The move goes through. Calling nothing means the same. |
ctx.block({ reason, fix }) |
The move stops. The reason lands on the card for the board to show; fix is an optional prompt handed to the card's own agent. |
ctx.amend({ sections }) |
The move goes through after ## <heading> sections are appended to the card body. Needs cardWrite in the manifest, or the host logs the refusal and passes. |
The context carries the card as the contract describes it and nothing more: card.slug, card.title, card.state, card.project, card.thread, the markdown body and the parsed frontmatter, plus from, to, reason, observed, hook and plugin. The host reads the body and frontmatter for you, so a stage contract needs no filesystem permission. Read the body as ctx.card.body ?? ctx.body: which of the two arrives depends on how the host loaded you.
Three rules keep a hook from wedging the pipeline. A hook that throws is logged and treated as a pass. A hook that has not answered after 5 seconds is skipped and the move proceeds. And a move the agent already wrote to disk is observed instead of executed: the hooks still run, but a block on an observed move is recorded as an advisory on the card rather than a revert, because the write has already happened.
Settings › SDLC draws a MUST BE TRUE TO PASS list on every arrow. Each check is the workflow plugin's own wording, and it gets a tick only when a hook is really registered on that arrow, attributed to the plugin that registered it. Today one arrow has a gate: Doing to Review, where grid-sdlc-default holds a card to its Review contract. A manifest that declares a hook and never registers it, as that plugin does for before:deliver, gets no tick.
Sixteen point names are accepted by the loader. Five have a reader in core today, five are claimed by a bundled descriptor plugin while the behaviour lives in core, and six are vocabulary only. That count is about the POINT mechanism specifically. For agent.provider it is no longer the whole story: since plugin API 0.2 a manifest can also declare permissions.spawn, a signed permission rather than a point, which is what lets a provider plugin actually start its own process instead of only supplying wording for one core already knows how to run.
| POINT | WHAT A CONTRIBUTION IS | READ BY |
|---|---|---|
agent.provider |
The session provider: id, name, binary and six stage prompts. | The session runtime, at spawn, for the wording. Where the manifest also declares permissions.spawn, the spawn engine, the transcript adapter and the hook installer read that block to start the program, tail its transcript and install its hooks. See Adding a provider. |
sdlc.workflow |
The transition table: from, to, trigger, command, checks, owning plugin. | Settings › SDLC. |
card.section |
The sections a stage requires. | Settings › SDLC's contract chips. |
editor.contextMenu |
A row for the code view's right-click menu, under FROM PLUGINS, with a when of always, file or selection. |
The code view. |
keymap.command |
A command for the keymap table, unbound until you give it a key. | The keymap and the command palette. |
theme.register |
Theme ids and labels. | The Plugins page lists them. The token blocks stay in the stylesheet. |
deliver.target |
An id, a name and the Deliver action string. | Descriptor only. Delivery policy is core. |
file.explain |
An ownership stub. | Descriptor only. Core writes no file explanations. |
usage.reporter |
An ownership stub. | Descriptor only. Cost accounting is core. |
report.redactor |
An ownership stub. | Descriptor only. Redaction is core. |
stage.transition |
A label. Hooks do the work. | Nothing. |
llm.provider |
Nothing yet. | Nothing. The pickers read the installed CLI. |
panel.slot |
Nothing yet. | Nothing. |
mcp.server |
Nothing yet. | Nothing. |
settings.page |
Nothing yet. | Nothing. No plugin owns a Settings page. |
prompt.file |
Nothing yet. | Nothing. Stage prompts are edited on the SDLC arrow. |
Arrives with the public beta. Readers for the remaining vocabulary points: panels in the session pane and the code view, MCP servers exposed by a plugin, provider-supplied models in the pickers and Settings pages. Until then a manifest may declare them and nothing will fire.
Grid keeps what you must be able to trust, and everything with an opinion in it is meant to ship as a plugin. The alpha is part way there: the bundled plugins own the words and the tables, and several behaviours they describe still run in core.
| CORE | WHY |
|---|---|
| Cards and stage state | The unit of work and its history. Every stage change goes through one function. |
| Worktrees and rewind | Isolation and undo have to hold when a plugin misbehaves. |
| The code index | What powers callers, dependencies and the file map. |
| Attention and the glow | Where to look is the product. |
| Licence and audit | Checked in core, so a plugin cannot grant itself a tier. |
| Action history | Every command an agent ran, recorded before a plugin sees it. |
| Cost accounting | Derived from the provider's transcripts. grid-usage is a descriptor. |
| The phone companion | A surface of the app itself, with no plugin behind it. |
| The seven stages | The state names are fixed. A workflow can leave Deliver out for a project; it cannot add or rename a stage. See workflows. |
The nine bundled plugins are listed with what each really owns in bundled plugins. grid-explain is the one descriptor whose contribution does something today: a menu row, a keymap entry and a command behind them. copilot-provider goes further still, since its manifest is what actually starts its session; see Adding a provider.
Every plugin, bundled ones included, activates in a child process of the daemon rather than inside the daemon itself. The process is started with Node's permission flags derived from the manifest: --permission, --allow-fs-read for the plugin's own directory and every declared filesystem root, --allow-fs-write for the declared roots, and --allow-child-process only when shell is true. Network is fenced inside the process: http, https, net, tls and fetch are wrapped before the plugin is loaded and refuse any host not covered by permissions.network, where a declared domain covers its subdomains. cardWrite, points, hooks and commands are checked on the host's side of the boundary, where a compromised child has no vote.
Plugins with identical grants share one process, so the bundled set, which declares nothing, costs one locked-down process. Each row on the Plugins page says how well it is fenced: full means the flags were applied, partial means this Node runtime refused the flags and only the boundary holds, and none means the in-process escape hatch (GRID_PLUGIN_INPROC=1) is on. A process that crashes is restarted once; a second crash within 30 seconds leaves its plugins down with the reason on the row.
This enforces declared rights without claiming to be a hostile-code sandbox. The network fence holds an honest plugin to what it declared and catches a buggy one; it does not stop code that has decided to get out. Native addons are refused at pack and add time, and node_modules never travels, so bundle your dependencies.
A plugin declares its settings in grid-plugin.json. Grid renders the controls itself, so a plugin does not have to be loaded, or even enabled, for its settings to be visible or changed. Values live in <workspace>/.grid/plugin-settings.json and reach the plugin as context.settings, a frozen snapshot taken at activation. Saving a value disposes and re-activates that one plugin, so activate() runs again with the new object.
"configuration": {
"model": {
"type": "enum",
"title": "Summary model",
"default": "haiku",
"options": ["fable", "opus", "sonnet", "haiku"]
},
"inlineNotes": { "type": "bool", "title": "Inline notes", "default": true },
"prompts.build": { "type": "prompt", "title": "Build prompt", "default": "Settings › SDLC" }
}
| TYPE | KIND |
|---|---|
bool, string, number, enum, string[], path |
A value. Stored, validated against the declared type and option list, and rejected with a reason when it does not fit. An enum needs options. |
page, prompt, file |
A link. Somewhere to go, with display text and nothing stored. A prompt row opens the SDLC arrow that edits it; a file row prints the path the provider reads. |
| Anything else | A readout. The row renders with the reason there is no control, and the plugin still loads. |
scope accepts workspace or project. Only workspace is honoured: this build stores values and enablement per workspace, and the row says so rather than pretending. There is no secret type. Do not put a key in a setting.
The six stage prompts are the case that matters most. The providers declare them as prompt links, and you edit the text on the arrow in Settings › SDLC. What you type there is an override, and Restore default deletes it. There is no diff view against the shipped text.
A contribution to a UI point is static data, so it can name what it does but cannot be what it does. The other half is a command: declare "commands": ["<pluginId>:<name>"] in the manifest, register it with context.commands.register(name, fn), and the UI reaches it over POST /api/plugins/<id>/invoke. Arguments in and the answer out are plain JSON, and a command that has not answered after 15 seconds is reported as such rather than swallowed. The prefix is mandatory so a plugin command can never collide with one of Grid's own in the keymap. grid-explain:describe-selection is the shipped example.
@gridconsole/plugin in the plugins repository ships the type definitions for everything above and four test helpers, so a hook or an activate() runs under node --test without the engine.
| HELPER | DOES |
|---|---|
defineHook(fn) |
Identity with a type, so your editor knows the ctx shape. |
fakeCard(overrides) |
A card shaped like the engine's. |
fakeCtx({ from, to, body, ... }) |
A hook context whose pass, block and amend record their verdict on ctx.verdict. |
fakePluginContext(manifest, values) |
A plugin context that records contributions, hook registrations and commands, and enforces the declared-in-manifest rule the host does. |
const { fakeCtx } = require("@gridconsole/plugin");
const ctx = fakeCtx({ from: "review", to: "deliver", body: "## Summary\n..." });
await hook(ctx);
assert.strictEqual(ctx.verdict.kind, "block");
| COMMAND | DOES |
|---|---|
grid plugin ls [--trust] |
The loaded roster. --trust adds the trust word, the signer, whether the signature still holds, and the enforcement level. |
grid plugin install <dir> |
Records a directory in the workspace registry with its content hash pinned. grid plugin add is the same verb. The daemon reads it at the next grid restart. |
grid plugin install <file.gridplugin> |
Verifies the artefact, unpacks it under Grid's own plugin directory and loads it now. Prints the declared permissions first. |
grid plugin dev <dir> |
Records a directory unpinned, for the one you are editing. It always loads current content. |
grid plugin pack <dir> [--key <pem>] [--out <file>] |
Builds <id>-<version>.gridplugin and validates the manifest with the loader's own reader. Signs when you pass a key; unsigned installs as unverified. |
grid plugin keygen |
Prints a fresh Ed25519 keypair and writes nothing. Store the private half yourself. |
grid plugin trust [<fingerprint>] [--revoke <fingerprint>] |
The publisher keys this workspace believes. |
A pinned plugin whose files no longer hash to the pin refuses to load and reads TAMPERED until you grid plugin add it again. A .gridplugin is one JSON file: the manifest, every file with its own digest, the content hash, a manifest digest and, when signed, the public key, an Ed25519 signature over all of those and a timestamp. The key that makes a plugin official ships inside the app. What the three trust states mean is in manifest, permissions and trust.
Settings › Plugins lists the bundled plugins, then anything installed, then the catalogue.
| CONTROL | DOES |
|---|---|
| ON / OFF | Disposes or re-activates the plugin live, and stops its process when nothing is left in it. Refused on a tampered plugin. |
| The settings panel | One open at a time. A save reports what it refused. |
| Trust chip | VERIFIED, UNVERIFIED, UNKNOWN (a valid signature from a key you have not decided about), BAD SIGNATURE or NO SIGNATURE. Official is silent. |
| UNSTABLE chip | With the manifest's note under it. |
may: line |
The declared rights, and a note when enforcement is partial. |
| Trust publisher | On an UNKNOWN row. Plugins signed by that key become verified. Trusted keys can be revoked in the same pane. |
| Allow unverified plugins | Off by default. While it is on, a banner stays at the top of the page. |
| Browse registry, Install from file | See installing a plugin. |
MCP servers and skills stay where the CLI keeps them. The provider's settings rows point at the config file and the skills folder, and claude mcp remains the way to add a server, because adding one means handling its keys, and Grid never holds those.
Building a plugin? The hooks, the manifest and the review process get discussed in the open, and the maintainers answer there. Join the Discord