Extensions
Package reusable eve capabilities and mount them from npm or a monorepo workspace.
Extensions package eve tools, channels, connections, skills, schedules, subagents, instruction fragments, and hooks. An author builds an extension package; each agent that uses it declares the package as a dependency and mounts it. The package can be published to a package registry or kept private inside a monorepo workspace.
Ready-made extensions can also be distributed through an eve integration registry. See Add Integrations to discover and add one with eve add; this page explains how extension packages are authored, mounted, configured, and overridden.
This enables sharing many different capability sets. A browser extension might include several tools for navigating a site. A memory extension could use hooks to capture context and tools to recall it. A self-improving extension could pair hooks with dynamic instructions.
Author: create an extension
Create the package
Start with the extension scaffold:
npx eve@latest extension init my-crmThe command creates the package, installs dependencies, and initializes Git. It includes extension/extension.ts, TypeScript configuration, and the package metadata required to build and publish.
An extension uses the same file conventions as an agent for its contributions:
@acme/crm/
package.json
extension/
extension.ts
tools/search.ts
channels/webhook.ts
connections/api.ts
skills/triage/SKILL.md
schedules/sync.ts
subagents/reviewer/agent.ts
instructions.md
hooks/audit.ts
lib/http.tsEach listed slot accepts the same authored forms as its agent counterpart. Static and dynamic tools, skills, and instructions all work in an extension: extension/instructions.ts is as valid as extension/instructions.md, and extension/tools/ can contain defineDynamic(...).
Names come from paths, so call the tool search, not crm_search; the consumer's mount adds the crm__ prefix. The same prefix applies to channel, schedule, and parent-visible subagent IDs, while channel route paths and schedule cron expressions stay unchanged. Keep shared code in extension/lib/.
The extension root cannot declare agent configuration, a sandbox, or nested extensions. A subagent contributed under extension/subagents/ owns its own agent configuration and sandbox like any other declared subagent.
Add configuration and contributions
The author's extension/extension.ts default-exports a defineExtension handle. Give it a Standard Schema when consumers need to provide settings:
import { defineExtension } from "eve/extension";
import { z } from "zod";
export default defineExtension({
config: z.object({
apiKey: z.string(),
baseUrl: z.string().url().default("https://api.acme.example"),
}),
});Contributions, including schedule handlers, can import that handle to read the validated configuration. Defaults have already been applied:
import { defineTool } from "eve/tools";
import { z } from "zod";
import extension from "../extension";
export default defineTool({
description: "Search the CRM.",
inputSchema: z.object({ query: z.string() }),
async execute({ query }) {
const { apiKey, baseUrl } = extension.config;
return { query, baseUrl, authenticated: apiKey.length > 0 };
},
});If no configuration is needed, export defineExtension() and let consumers re-export it directly. Config schemas must validate synchronously.
defineState is automatically scoped to the extension package, so the same state name does not collide with the consumer or another extension.
Add a subagent
Author a subagent under extension/subagents/<id>/ using the same files as a subagent declared by an agent. Mounting the extension as crm exposes extension/subagents/reviewer/ to the consuming agent node as crm__reviewer. The subagent's own tools, connections, skills, hooks, instructions, sandbox, and nested subagents remain isolated inside its node and keep their path-derived names.
Modules inside the contributed subagent can import the extension handle. For example, a tool under extension/subagents/reviewer/tools/ can read the configuration bound by the consumer's agent/extensions/crm.ts mount.
Build and optionally publish
The scaffold's package.json declares separate source and distribution roots:
{
"name": "my-crm",
"version": "0.0.0",
"type": "module",
"eve": {
"extension": {
"source": "./extension",
"dist": "./dist/extension",
},
},
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs",
},
"./tools": {
"types": "./dist/tools/index.d.ts",
"default": "./dist/tools/index.mjs",
},
},
"scripts": {
"build": "eve extension build",
"prepare": "eve extension build",
"typecheck": "tsc",
},
"dependencies": {
"zod": "^x",
},
"devDependencies": {
"@types/node": "^x",
"eve": "x.y.z",
"typescript": "^x",
},
"peerDependencies": {
"eve": "*",
},
"engines": {
"node": ">=24",
},
}The scaffold omits engines when it creates a workspace package.
Build the package with eve extension build:
eve extension buildeve extension build writes an agent-shaped dist/extension tree, copies skill assets, emits declarations, and records compatibility metadata. It also manages the package exports for the mount factory (@acme/crm) and tool definitions (@acme/crm/tools). Publish dist/; consumers do not need the author's TypeScript source.
The exact eve development pin controls the extension authoring API and build tooling. The wildcard peer lets the consumer provide the runtime copy of eve. At consumption time, eve checks generated metadata, not the npm peer range. Do not add eve to regular dependencies.
Put runtime packages such as zod or an SDK in dependencies. If a dependency cannot be bundled, such as a native addon, tell consumers to add it to build.externalDependencies in agent.ts.
Consumers can now add the built package to an agent. A workspace-only extension uses the same package contract but does not need to be published; see Use an extension in a workspace.
Consumer: install and mount an extension
A mount gives the extension's contributions a namespace. Updating the package updates the mounted extension; nothing is copied into the consumer's agent.
Install the package
Install the extension with the package manager already used by the consumer's agent project. Fresh eve projects use pnpm:
pnpm add @acme/crmMount it
Create a file under agent/extensions/. Its filename becomes the mount namespace. Call the extension's default export when it needs configuration:
import crm from "@acme/crm";
export default crm({ apiKey: process.env.CRM_API_KEY! });Set CRM_API_KEY in the consumer's environment, such as .env.local for local development.
The mount adds crm__ to named contributions: tools/search.ts becomes crm__search, channels/webhook.ts becomes crm__webhook, schedules/sync.ts becomes crm__sync, connections/api.ts becomes crm__api, and subagents/reviewer/ becomes crm__reviewer. Channels keep their declared route paths, and schedules keep their cron expressions.
For an extension with no configuration, mount its default export directly:
export { default } from "@acme/gizmo";The same mount shape works with an npm package, a workspace dependency, or a linked local package.
Use an extension in a workspace
A workspace extension is a regular extension package kept in the same monorepo as its consumers. It is useful when several agents need the same capabilities, or when a private capability should evolve alongside the agents that use it.
For example, a pnpm workspace can keep one extension next to two independently deployable agents:
acme-agents/
├── pnpm-workspace.yaml
├── packages/
│ └── shared-capabilities/
│ ├── package.json
│ └── extension/
│ ├── extension.ts
│ ├── tools/
│ ├── skills/
│ └── hooks/
└── agents/
├── support/
│ ├── package.json
│ └── agent/extensions/shared.ts
└── operations/
├── package.json
└── agent/extensions/shared.tsMake both the extension and agent directories workspace members:
packages:
- "agents/*"
- "packages/*"You can scaffold the extension from a directory already covered by the workspace configuration:
cd packages
npx eve@latest extension init shared-capabilitiesGive the generated package the name consumers will import. Add "private": true if it should never be published:
{
"name": "@acme/shared-capabilities",
"private": true,
"eve": {
"extension": {
"source": "./extension",
"dist": "./dist/extension",
},
},
}Each consuming agent declares its own workspace dependency:
{
"dependencies": {
"@acme/shared-capabilities": "workspace:*",
},
}Then each agent mounts the package:
export { default } from "@acme/shared-capabilities";The mount is intentionally per agent. Each consumer chooses its own mount namespace and, for a configured extension, passes its own configuration. For example, shared.ts contributes shared__search, while mounting the same package as company.ts in another agent contributes company__search.
Develop from source
When eve dev starts a consuming agent, it builds mounted, source-backed extensions found inside the same workspace before compiling the agent. It watches the extension source and relevant package and TypeScript configuration, then rebuilds only the affected extension. If an extension edit fails to build, the previous successful development generation keeps running.
Production eve build expects the extension distribution to exist already. Keep eve extension build in the extension package's build and prepare scripts, as the scaffold does, and run workspace builds in dependency order so extensions build before their consuming agents.
Override a contribution
Use a directory mount to replace or remove an extension contribution. Put the mount declaration in extension.ts and add overrides beside it:
agent/extensions/crm/
extension.ts
tools/search.tsimport crm from "@acme/crm";
export default crm({ apiKey: process.env.CRM_API_KEY! });A same-named consumer channel, tool, connection, skill, schedule, or subagent wins. To adjust an extension tool, import it from the package's ./tools export and define it again:
import { search } from "@acme/crm/tools";
import { defineTool } from "eve/tools";
import { always } from "eve/tools/approval";
export default defineTool({ ...search, approval: always() });To remove an extension tool, use disableTool() in its matching slot:
import { disableTool } from "eve/tools";
export default disableTool();Hooks and instruction fragments are additive, so they cannot be replaced. To replace a dynamic tool, use a dynamic definition in the same slot; dynamic tools win over same-named static tools at runtime. disableTool() removes either kind.
The crm__ prefix is reserved for this directory mount. A consumer cannot override the extension from agent/tools/, agent/connections/, agent/subagents/, or another agent-root slot.
Use an extension tool result in a hook
To retain an extension tool's result type in a consumer hook, import its definition from ./tools and pass it to toolResultFrom:
import { defineHook } from "eve/hooks";
import { toolResultFrom } from "eve/tools";
import { search } from "@acme/crm/tools";
export default defineHook({
events: {
"action.result"(event) {
const match = toolResultFrom(event.data.result, search);
if (match) console.log(match.output);
},
},
});toolResultFrom recognizes the mounted crm__search result from the original definition, not the namespaced string. Publishers should keep tool descriptions distinct so eve can assign each definition an unambiguous identity.
Compatibility
At build time, eve checks the extension's generated capability metadata. If the extension needs an unsupported capability contract, upgrade eve or install a compatible extension release.
What to read next
- Integrations: browse ready-to-install extensions using the Extensions filter
- Tools: static tools, approval, and tool output
- Dynamic capabilities: dynamic tools, skills, and instructions
- Instructions: static and TypeScript instructions
- Skills: package procedures and supporting files
- Connections: integrate external services
- Channels: receive messages and expose routes
- Schedules: run the agent on a cron cadence
- Subagents: delegate to declared specialists
- Hooks: observe agent events