TypeClawTypeClaw
Reference

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.

FieldRequiredNotes
configSchemanoZod schema for the plugin's config block; parsed result is handed to plugin as ctx.config. Omit for unknown.
permissionsnoevery permission this plugin both declares and consumes; read before plugin runs
ownerWildcardExclusionsnopermission strings the owner wildcard sentinel MUST NOT auto-expand to (used by security to keep high-tier bypasses off owner)
commandsnodeclared by-value so the host-stage CLI can dispatch them without booting plugin runtime state
pluginyesasync 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 | EitherCommand
surfaceRunsContext surface
'container'inside the agent runtimeprompt, subagent, exec, origin, permissions
'host'on the user's machinestreams + agentDir (host path), logger, signal
'either'whichever stage was invokedintersection 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 (spawnedByRole is 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)

On this page