Adding plugins
Give an agent a hook, a slash command, a skill and an MCP server with ai-sdk-harness-plugins, then let it pick single items from a catalog.
This guide starts from the agent of Getting started, Claude Code in a Docker Sandbox, and gives it a plugin, then a catalog of items. Every part of a plugin is in the package's documentation.
Install the package
pnpm add ai-sdk-harness-plugins zodWrite a plugin
A guard that keeps .env files out of the conversation, with the script it runs, a slash command, and the skill the command relies on:
import { definePlugin } from 'ai-sdk-harness-plugins';
export const team = definePlugin({
name: 'team',
description: 'How our team works.',
// ${PLUGIN_ROOT} is replaced by the sandbox directory the plugin's files are written to.
hooks: [{ event: 'PreToolUse', matcher: 'Read|Bash', command: '"${PLUGIN_ROOT}/guard.sh"' }],
files: [
{
path: 'guard.sh',
executable: true,
content: `#!/bin/sh
# The tool call, as JSON on the standard input.
if grep -Eq '\\.env($|[^A-Za-z])'; then
echo '.env files hold secrets, and stay out of the conversation.' >&2
exit 2
fi`,
},
],
commands: [
{
name: 'explain',
description: 'Explain a file or a directory',
prompt: 'Explain $1 to a newcomer, following the code-tour skill.',
},
],
skills: [
{
name: 'code-tour',
description: 'How to explain code. Use it whenever you explain code.',
content: 'Say what the code is for, name the pieces that matter, show one excerpt.',
},
],
});Give it to the agent
import { expandCommand, withPlugins } from 'ai-sdk-harness-plugins';
import { team } from './plugins';
const agent = new HarnessAgent(withPlugins({ harness: createClaudeCode() }, [team]));
const session = await agent.createSession({ sandboxSession });
// `/explain src` is expanded here, on your server.
const message = '/explain src';
const prompt = expandCommand(message, [team])?.prompt ?? message;
const { text } = await agent.generate({ session, prompt });Ask the agent to read .env: Claude Code runs the hook's script before the read, the script exits with 2, and the agent is told why instead of reading the file:

Connect an MCP server
An MCP server is connected by your server: its tools reach the agent, its token does not. The plugin only holds a reference to the token, which resolveSecret resolves on your server:
const docs = definePlugin({
name: 'docs',
description: 'Library documentation.',
mcpServers: {
context7: {
type: 'http',
url: 'https://mcp.context7.com/mcp',
headers: { Authorization: { secret: 'CONTEXT7_TOKEN' } },
},
},
});
const { plugins, close } = await resolvePlugins([team, docs], {
resolveSecret: (name) => secretManager.get(name),
});
const agent = new HarnessAgent(withPlugins({ harness: createClaudeCode() }, plugins));
Let each agent pick items
Rather than whole plugins, offer single items — tools, skills, commands, hooks, subagents, MCP servers —, written in code or as JSON, and let each agent pick its own; an item brings what it requires:
const catalog = createCatalog({
plugins: [team, docs],
items: [
{
kind: 'tool',
name: 'npm-latest',
type: 'http',
description: 'The latest version of an npm package.',
// What the model reads to call it: {name} of the url is one of its required properties.
inputSchema: {
type: 'object',
properties: { name: { type: 'string', description: 'The package name' } },
required: ['name'],
},
url: 'https://registry.npmjs.org/{name}/latest',
},
{
kind: 'command',
name: 'npm',
description: 'Size a package up',
prompt: 'Look $1 up with the npm-latest tool…',
requires: ['tool:npm-latest'],
},
],
});
const { plugins } = await catalog.resolve(['plugin:team', 'command:npm'], { resolveSecret });
Next steps
- Every part of a plugin and every option: the package's documentation.
- The Next.js example, with a marketplace of plugins and items, and a prompt that completes their commands and skills.