Plugins and stages
Where plugins live, what they can touch, and why the container is the trust boundary
A TypeClaw plugin is a TypeScript module loaded into the container-stage Bun process. Not a subprocess. Not an IPC peer. Not a sandboxed worker. The same process that runs the agent loop also runs your plugin's code, calls your plugin's tools, and reads your plugin's config block.
This is unusual enough that it's worth saying why.
Why plugins are in-process
The alternative — plugins as subprocesses, plugins as WASM modules, plugins as plugin-server RPC clients — buys isolation at the cost of speed and ergonomics. Every tool call becomes a serialization boundary. Every shared piece of state (the message stream, the permission service, the session store) needs a wire protocol. Every plugin needs its own deployment surface.
TypeClaw already has an isolation boundary: the container. Plugins can't escape it. They can't reach the host filesystem (no docker.sock mount). They can't change which other plugins are loaded. They can't rewrite the Dockerfile. The trust boundary is the container, and the container is already sandboxed.
Pulling the boundary down to the plugin level would buy almost no extra security — a malicious plugin in-process can already do everything a malicious plugin via RPC could do, because they're both running with the same container's capabilities. The RPC version would just be slower, harder to debug, and impossible to hot-reload.
What plugins receive
The plugin(ctx) factory is an async callback that gets a PluginContext carrying:
ctx.config— the parsed config block for this plugin (validated by yourconfigSchema). Thectxobject is frozen, but the config object itself isn't, so treat it as read-only. Boot-only fields are snapshots; reloadable fields update ontypeclaw reload.ctx.permissions— the live permission service. Plugins query it (ctx.permissions.has(origin, perm)) rather than implementing their own checks.ctx.spawnSubagent(name, payload, options)— for plugins that contribute subagents and want to invoke them; provenance is resolved from the spawning origin.ctx.agentDir— absolute path to the agent folder (mounted at/agentin the container).ctx.name/ctx.version/ctx.logger/ctx.github— the plugin's derived name, its package version, a namespaced logger, and GitHub token resolution.
Everything plugins talk to flows through this object. There are no module-global imports of agent state, no singletons reaching into the runtime — the surface is the ctx.
What plugins can contribute
A plugin's factory returns a PluginExports object with optional fields:
tools— Zod-typed functions the agent can callskills/skillsDirs— markdown procedures the agent loads on demand, inline or from directoriessubagents— focused worker sessions with their own system prompt and payload schemacronJobs— scheduled prompts, exec commands, or in-process handlers contributed at boothooks— session- and tool-lifecycle observers (see below)doctorChecks—typeclaw doctorchecks, optionally with a--fixapply step
Plus, declared on the definePlugin envelope itself (not inside the factory):
permissions— every permission this plugin both checks and contributes. Lifted out of the factory because the runtime reads it before invoking the factory, to expand the owner wildcard and validate user-declaredpermissions[]against the union of all plugin permissions.commands— host- or container-stage CLI subcommands, declared by-value so the host CLI can dispatch them without booting plugin runtime state.
The full signatures are at /reference/plugin-api.
Hooks intercept the agent's own tools
Reactive work runs through hooks, not a message-stream subscription. The session hooks (session.start / end / idle / prompt / turn.start / turn.end) observe the agent's lifecycle; the bundled memory plugin debounces off session.idle to spawn its logger.
The tool hooks are the important ones for security. tool.before and tool.after fire for both plugin-contributed tools and the agent's own built-in tools — read, bash, edit, write, grep, find, ls. A tool.before returning { block: true, reason } aborts the call before it runs, including a bash shell command. This is exactly how the bundled security plugin enforces its policies (secret exfil, SSRF, git remote taint, prompt injection) — by inspecting the agent's own bash invocations at tool.before. So "plugins can't drive the agent" does not mean plugins can't intercept it: a plugin cannot issue tool calls, but it can gate every tool call the agent makes.
What plugins cannot do
Plugins can't drive the agent. They contribute tools the agent might call; they don't issue tool calls themselves. If you need scheduled work, contribute a cron. If you need reactive work, install a hook. The agent's autonomy is the agent's; plugins are how you give it more capabilities, not how you put it on rails.
Plugins also can't change other plugins' contributions, mutate the Dockerfile, or open new mounts. These are container-stage facts captured at boot, owned by the host-stage launcher, and not exposed to plugin code.
For the practical "add your first plugin" walkthrough see Add a plugin.