Plugin API
definePlugin, PluginContext, contribution types
The walkthrough is at Add a plugin. The shape is at /concepts/plugins-and-stages. This page is the contract. The source of truth is src/plugin/types.ts and src/plugin/define.ts.
definePlugin
import { definePlugin } from 'typeclaw/plugin'
export default definePlugin({
configSchema, // optional: z.ZodType<TConfig>
permissions, // optional: readonly string[]
ownerWildcardExclusions, // optional: readonly string[]
commands, // optional: Record<string, PluginCommand>
plugin: async (ctx) => {
return {} // PluginExports
},
})The plugin's name is not declared here — the loader derives it from the entry. For a local/path plugin (./packages/foo) the name is the resolved path's basename (minus a .ts/.js/… extension); for an npm plugin it's package.json#name run through derivePluginNameFromPackage (drops a @scope/ and a typeclaw-plugin- prefix). That derived name scopes the config block (typeclaw.json#<name>) and namespaces permissions.
| Field | Required | Notes |
|---|---|---|
configSchema | no | Zod schema for the plugin's config block; parsed result is handed to plugin as ctx.config. Omit for unknown. |
permissions | no | every permission this plugin both declares and consumes; read before plugin runs |
ownerWildcardExclusions | no | permission strings the owner wildcard sentinel MUST NOT auto-expand to (used by security to keep high-tier bypasses off owner) |
commands | no | declared by-value so the host-stage CLI can dispatch them without booting plugin runtime state |
plugin | yes | async factory; receives the live PluginContext, returns the plugin's PluginExports |
PluginContext
type PluginContext<TConfig = never> = {
readonly name: string // derived plugin name
readonly version: string | undefined
readonly agentDir: string // absolute; '/agent' in container
readonly config: TConfig // parsed per configSchema (the ctx is frozen; the config object itself is not)
readonly logger: PluginLogger // .info / .warn / .error
readonly permissions: PermissionService // .has(origin, perm) etc.
readonly github: PluginGithubServices // .resolveTokenForRepo
spawnSubagent: (name: string, payload?: unknown, options?: SpawnSubagentOptions) => Promise<void>
}
type SpawnSubagentOptions = {
parentSessionId?: string
spawnedByOrigin?: SessionOrigin
}spawnSubagent resolves spawnedByRole from spawnedByOrigin via the PermissionService, so the spawning session's role is inherited rather than forged. Hook handlers that own an origin (e.g. session.idle, session.turn.end) should pass parentSessionId and spawnedByOrigin: event.origin.
PluginExports
Returned from the plugin(ctx) factory. All fields optional.
type PluginExports = {
tools?: Record<string, Tool<any>>
subagents?: Record<string, Subagent<any>>
cronJobs?: Record<string, PluginCronJob>
skills?: Record<string, PluginSkill>
skillsDirs?: string[]
hooks?: Hooks
doctorChecks?: Record<string, PluginDoctorCheck>
}Commands are declared on definePlugin (by-value), not returned here.
Tool
type Tool<P = unknown> = {
description: string
parameters: z.ZodType<P>
execute: (args: P, ctx: ToolContext) => Promise<ToolResult>
}
type ToolContext = {
signal: AbortSignal | undefined // fires when the agent cancels the call
sessionId: string
agentDir: string
logger: ToolLogger
}
type ToolResult = {
content: ContentPart[] // { type: 'text'; text } | { type: 'image'; mimeType; data }
details?: unknown
}defineTool(tool) is an identity helper for type inference. Built-in tools are referenced by tagged ref (readTool, bashTool, editTool, writeTool, grepTool, findTool, lsTool, webSearchTool, webFetchTool) — useful when wiring a subagent's tools list.
Subagent
type Subagent<P = unknown> = SubagentShared<P> & {
tools?: BuiltinToolRef[] // built-in tools, by tagged ref
customTools?: Tool<any>[] // plugin-contributed tools
inFlightKey?: (payload: P) => string // per-payload coalescing
}A subagent inherits its system prompt and payload schema from SubagentShared. inFlightKey defaults to the subagent name alone (only one instance runs at a time); override it to allow per-payload concurrency. Memory's memory-logger keys by parentSessionId so different parent sessions run in parallel while duplicate runs against the same session deduplicate. defineSubagent(subagent) is the identity helper.
Cron
cronJobs is a Record<string, PluginCronJob>; the runtime prefixes each key with __plugin_<plugin-name>_ to form the global cron id (no collision with cron.json user jobs or across plugins). PluginCronJob is a discriminated union on kind:
type PluginPromptCronJob = {
schedule: string
kind: 'prompt'
prompt: string
enabled?: boolean
timezone?: string
subagent?: string // invoke a subagent instead of prompting
payload?: unknown
}
type PluginExecCronJob = {
schedule: string
kind: 'exec'
command: string[]
enabled?: boolean
timezone?: string
}
type PluginHandlerCronJob = {
schedule: string
kind: 'handler'
handler: (ctx: CronHandlerContext) => Promise<void>
enabled?: boolean
timezone?: string
}kind: 'handler' is plugin-only — a TypeScript function reference, so it cannot appear in cron.json (the schema gate limits user files to prompt and exec). Use it when a job needs imperative control flow (probe → maybe prompt → write file) in the same plugin as the logic. CronHandlerContext mirrors a command's LLM-call surface (prompt, subagent, exec) plus jobId, name, agentDir, logger, signal, permissions, and a cron-shaped origin that stamps provenance on anything the handler spawns.
Skills
type PluginSkill = {
description: string
content: string
frontmatter?: Record<string, unknown>
}Contribute skills inline via skills: Record<string, PluginSkill>, or point at on-disk skill directories with skillsDirs: string[].
Hooks
type Hooks = {
'session.start'?: (event: SessionStartEvent, ctx: HookContext) => void | Promise<void>
'session.end'?: (event: SessionEndEvent, ctx: HookContext) => void | Promise<void>
'session.idle'?: (event: SessionIdleEvent, ctx: HookContext) => void | Promise<void>
'session.prompt'?: (event: SessionPromptEvent, ctx: HookContext) => void | Promise<void>
'session.turn.start'?: (event: SessionTurnStartEvent, ctx: HookContext) => void | Promise<void>
'session.turn.end'?: (event: SessionTurnEndEvent, ctx: HookContext) => void | Promise<void>
'tool.before'?: (event: ToolBeforeEvent, ctx: HookContext) => ToolBeforeResult | Promise<ToolBeforeResult>
'tool.after'?: (event: ToolAfterEvent, ctx: HookContext) => void | Promise<void>
}
type HookContext = { agentDir: string; pluginName: string; logger: PluginLogger }tool.before is the gate. Return:
type ToolBeforeResult = void | undefined | { block: true; reason: string }Return nothing (or undefined) to allow; return { block: true, reason } to block. The bundled security plugin returns the block variant for its tier-based policies. It fires for plugin-defined tools and TypeClaw-exposed built-in tools (read/bash/edit/write/grep/find/ls).
tool.after is read-only; use it for logging, metrics, and side-channel cleanup.
session.prompt brackets system-prompt assembly — mutate near the end of event.prompt to preserve provider prompt-cache hits; mutations near the start invalidate the cache on every call. session.turn.start / session.turn.end bracket every session.prompt(...) turn (distinct from session.start / session.end, which bracket session lifetime). session.idle carries an idleMs window; plugins wanting delayed reactions install their own setTimeout.
Doctor checks
type PluginDoctorCheck = {
description: string
category?: string
run: (ctx: PluginDoctorContext) => Promise<PluginCheckResult>
}Each check is read-only by default. Declaring fix.apply on the returned PluginCheckResult opts it into typeclaw doctor --fix, where the host serializes plugin fixes, validates each changedPaths entry stays inside the agent folder, and commits the union of all fixes in a single commit.
Commands
Commands are declared by-value on definePlugin#commands so the host-stage CLI can dispatch them without booting plugin runtime state. surface controls where a command may run:
type PluginCommand = ContainerCommand | HostCommand | EitherCommandsurface | Runs | Context surface |
|---|---|---|
'container' | inside the agent runtime | prompt, subagent, exec, origin, permissions |
'host' | on the user's machine | streams + agentDir (host path), logger, signal |
'either' | whichever stage was invoked | intersection of the two |
defineCommand(cmd) infers the context type from surface and types args from a z.ZodObject (v1 constraint: primitive leaves so --help can render --<name>=<type>). Container commands may set isolated: true to spawn a fresh Bun subprocess (~150ms cold-start) for isolation from the agent.
Lifecycle
Plugins load once at container start. They can:
- Expose tools, subagents, cron jobs, skills, hooks, doctor checks, and commands
- Read live config via
ctx.config(boot-only fields are snapshots; reloadable fields are live) - Spawn subagents with provenance (
spawnedByRoleis resolved from the parent origin)
Plugins cannot:
- Change which other plugins are loaded
- Rewrite the Dockerfile or escape the container
- Issue tool calls themselves (they expose tools; the agent decides when to call them)