Skip to content

Extensions ​

An extension packages capabilities that would otherwise live under bot/: tools, skills, instructions, connections, hooks, channels, schedules, subagents, artifacts, and workspace seeds. One mount file adds them under a namespace without copying their source into your project.

Extensions and Cursor plugins execute as trusted host code. A namespace prevents name collisions; it does not sandbox the package or limit its side effects. Review the package before mounting it.

Mount an extension ​

Install the package and add one file under bot/extensions/. The filename becomes the namespace, so this mount contributes names such as ci__failed_jobs and ci__triage.

ts
// bot/extensions/ci.ts
import ci from "@acme/ci-triage";

export default ci({
  org: "acme",
  token: () => process.env.BUILDKITE_TOKEN ?? "",
});

Pass token as a function so a tool can call it. The function reads BUILDKITE_TOKEN when the tool calls it.

The extension validates its configuration while the project loads. Missing or mistyped settings fail agent-sdk validate before a turn can call the contributed code.

Extension instructions are appended to the root agent's instructions. The root agent still needs its own bot/instructions.md.

Inspect a mount ​

Run discovery after mounting or upgrading an extension:

text
agent-sdk validate --dir .
agent-sdk info --dir . --json

info lists each mount and its namespaced tools, skills, connections, hooks, channels, schedules, subagents, and artifact kinds. The same data appears under extensions in GET /v1/info.

A mount can also widen what a hosted deployment reaches. info shows the egress hosts and secret names each mount adds, and agent-sdk deploy names every extension that adds egress before it deploys.

Names inside the extension stay short. A tool authored as tools/failed_jobs.ts appears to the mounting agent as <namespace>__failed_jobs. The __ separator is reserved, so root contributions cannot impersonate a mounted one.

Adjust a mounted extension ​

Mount as a directory when you need to replace or remove one contribution. Keep the package mount in extension.ts, then overlay the extension's short name beneath it.

text
bot/extensions/ci/
  extension.ts
  tools/
    job_log.ts
    retry_job.ts
ts
// bot/extensions/ci/tools/job_log.ts
import { defineTool } from "@grok/agent-sdk/tools";
import {
  jobLogInput,
  readRedactedLog,
} from "../../../lib/ci.js";

export default defineTool({
  description: "Read a redacted CI log.",
  inputSchema: jobLogInput,
  execute: readRedactedLog,
});
ts
// bot/extensions/ci/tools/retry_job.ts
import { disableTool } from "@grok/agent-sdk/extensions";

export default disableTool();

The first file replaces ci__job_log; the second removes ci__retry_job. Use the matching disable* helper for another slot, or generic disable() when the file location already identifies it. Validation fails when an overlay names a contribution the package does not provide.

Mount a Cursor plugin ​

cursorPlugin() adapts a vendored Cursor or Claude plugin into the same namespace model. Skills, commands, agents, and MCP servers become Agent SDK skills, subagents, and connections.

ts
// bot/extensions/notion.ts
import { cursorPlugin } from "@grok/agent-sdk/extensions";

export default cursorPlugin({
  path: "plugins/notion-workspace",
  variables: {
    NOTION_TOKEN: "${NOTION_TOKEN}",
  },
});

path is relative to the project root. variables fills placeholders declared by the plugin's MCP configuration. Pass a ${NAME} template. The connection reads NAME when it opens. An unresolved placeholder fails validation instead of mounting a partial server.

Plugin rules and hooks are not mounted. Overlay a generated mcp-connections/<server>.ts file to replace or disable one plugin server. OAuth-backed plugin servers use Host MCP OAuth; transport and tool-filter details live in MCP connections.

Extension contents ​

Ships under the namespaceIgnored
Instructions, tools, skills, MCP and host connectionsagent.ts, storage, OpenTelemetry
Hooks, channels, schedules, and subagentsPlayground code, custom sandbox backends
Artifact kinds and workspace seed filesNested extensions

agent-sdk validate reports unsupported paths.

Build an extension ​

Lay out the package like the shareable part of bot/. The root extension.ts supplies a description and validates any settings the consumer must provide.

text
@acme/ci-triage/
  package.json
  extension.ts
  instructions.md
  tools/
    failed_jobs.ts
    job_log.ts
  skills/
    triage.md
  hooks/
    audit.ts
  lib/
    buildkite.ts
ts
// extension.ts
import { defineExtension } from "@grok/agent-sdk/extensions";
import { z } from "zod";

export default defineExtension({
  root: import.meta.dirname,
  description: "Investigate failing Buildkite jobs.",
  config: z.object({
    org: z.string(),
    token: z.function().args().returns(z.string()),
    logTailLines: z.number().int().positive().default(200),
  }),
  hosting: () => ({
    egressDomains: ["api.buildkite.com"],
    secretNames: ["BUILDKITE_TOKEN"],
  }),
});

Modules inside the package import this definition and read extension.config, which is already validated and has defaults applied. Publish the built entry point with its slot directories and prose assets beside it.

hosting names the hosts the package calls and the secrets it reads from the environment. It receives the validated settings, and the mounting agent's hosting.egressDomains and hosting.secretNames gain whatever it returns, so a hosted deploy reaches Buildkite without the consumer listing api.buildkite.com. List BUILDKITE_TOKEN in secretNames. Call extension.config.token() inside a tool.

The Project layout owns each contribution's authoring contract. Keep extension instructions and tool descriptions namespace-neutral because the consumer chooses the final prefix.