Skip to content

Plugins

Plugins are WASM modules with a PLUGIN.toml manifest that extend AgentZero with new commands. They run inside the same wasmtime sandbox used for WASM skills, with fuel-based time limits and no ambient filesystem or network access.

Skills are the original extension mechanism. They use SKILL.md frontmatter, live in .agentzero/skills/, and support multiple runtimes (instruction-only, WASM, host-supervised). Plugins are a newer, more structured alternative:

FeatureSkillsPlugins
ManifestSKILL.md (Markdown frontmatter)PLUGIN.toml (structured TOML)
Location.agentzero/skills/<name>/.agentzero/plugins/<name>/
Runtimenone, wasm, host_supervisedwasm only
CommandsSingle entrypointMultiple named commands
Host imports_start or mainaz::* host imports (filesystem, clock, logging)

Plugins are the recommended path for all new extensions (see ADR 0016). They have richer host integration, multiple commands per module, MCP bridge support, and external subcommand dispatch.

Installed plugins can be invoked directly as az subcommands:

Terminal window
az brain capture "interesting thought"
az my-plugin do-something

If az <name> doesn’t match a built-in command, it checks for an installed plugin with that name and dispatches to its WASM module.

Terminal window
az plugin list

Shows all plugins installed in .agentzero/plugins/:

NAME VERSION CMDS DESCRIPTION
------------------------------------------------------------
brain 0.1.0 10 Personal LLM wiki — manage a Karpathy-style knowledge vault
Terminal window
# From a local directory
az plugin install /path/to/plugin-directory
# From a GitHub release
az plugin install owner/repo

The source must contain a PLUGIN.toml and at least one .wasm file. GitHub installs download the latest release, verify SHA-256 checksums, and update the lockfile at .agentzero/plugins/plugins.lock.

Terminal window
az plugin info brain

Displays the plugin manifest, available commands, WASM path, and install location.

Every plugin has a PLUGIN.toml in its root directory:

[plugin]
name = "brain"
version = "0.1.0"
description = "Personal LLM wiki — manage a Karpathy-style knowledge vault"
runtime = "wasm"
wasm_path = "brain.wasm"
[[commands]]
name = "init"
description = "Initialize a brain vault"
[[commands]]
name = "today"
description = "Create or show today's daily note"
[[commands]]
name = "capture"
description = "Append a timestamped thought to today's daily note"
FieldRequiredDescription
plugin.nameyesPlugin identifier (used as the directory name)
plugin.versionyesSemantic version
plugin.descriptionyesHuman-readable description
plugin.runtimenoRuntime type (default: wasm)
plugin.wasm_pathnoPath to the WASM module relative to the plugin directory. If omitted, the registry scans for any .wasm file.
plugin.permissionsnoDeclared capabilities: file_read, file_write, shell, network
plugin.keywordsnoKeywords for progressive disclosure — plugin commands auto-injected into LLM tool list when user messages match
commandsnoArray of {name, description} entries declaring available subcommands

Plugins compile from Rust to wasm32-unknown-unknown. A typical plugin has two parts:

  1. A library crate (crates/agentzero-<name>/) containing the business logic with a BrainFs-style trait for filesystem abstraction.
  2. A WASM guest crate (plugins/<name>/) that implements the trait using az::* host imports and exports run(ptr, len) -> i64.
plugins/my-plugin/
Cargo.toml # crate-type = ["cdylib"], targets wasm32-unknown-unknown
PLUGIN.toml # manifest
src/lib.rs # WASM guest: host imports + run() export
[package]
name = "my-plugin-wasm"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[profile.release]
opt-level = "s"
lto = true
strip = true
Terminal window
cargo build --manifest-path plugins/my-plugin/Cargo.toml \
--target wasm32-unknown-unknown --release

Or use the Justfile recipe:

Terminal window
just build-plugin my-plugin
Terminal window
just install-plugin my-plugin

This copies the built .wasm and PLUGIN.toml into .agentzero/plugins/<name>/.

Plugins export two functions:

ExportSignaturePurpose
alloc(size: i32) -> i32Allocate bytes in guest memory for the host to write into
run(ptr: i32, len: i32) -> i64Execute a command; input is a JSON string, return is a packed (ptr << 32 | len) pointing to a JSON response

The host writes a JSON object into guest memory. The action field selects which command to run:

{
"action": "capture",
"root": "/home/user/brain",
"message": "interesting thought about WASM sandboxing"
}

The guest returns a packed pointer to a JSON response:

{
"success": true,
"output": "Captured to wiki/daily/2026-05-11.md",
"error": null
}

On failure:

{
"success": false,
"output": null,
"error": "path traversal detected: ../etc/passwd"
}

Plugins access host capabilities through the az import module. These are the same imports used by dynamically generated WASM tools:

ImportSignatureDescription
az::read_file(ptr, len) -> i64Read a file; returns packed (ptr, len) string or -1 on error
az::write_file(path_ptr, path_len, content_ptr, content_len) -> i32Write a file; returns 0 on success
az::append_file(path_ptr, path_len, content_ptr, content_len) -> i32Append to a file; returns 0 on success
az::list_dir(ptr, len) -> i64List directory entries as JSON array; returns packed string
az::create_dir(ptr, len) -> i32Create directory and parents; returns 0 on success
az::file_exists(ptr, len) -> i32Check existence; 0 = exists, 1 = not found
az::now() -> i64Current time as packed ISO 8601 string
az::log(ptr, len)Write a message to the host log
az::http_request(url_ptr, url_len, method_ptr, method_len, headers_ptr, headers_len, body_ptr, body_len) -> i64Make an HTTP request; returns packed JSON response string. Policy-checked and PII-scanned.
az::run_command(cmd_ptr, cmd_len, args_ptr, args_len) -> i64Execute an allowlisted shell command. Returns packed JSON: {exit_code, stdout, stderr}. Allowed: git, rg, grep, find, wc, sort, head, tail.

All filesystem imports are mediated by the host’s PathValidator, which rejects path traversal (..), null bytes, and access outside the allowed root directory. WASM modules that declare undeclared imports are rejected before execution.

By default, plugins have no network access. To enable outbound HTTP requests, declare a [plugin.network] section in PLUGIN.toml:

[plugin.network]
policy = "allow_egress_filtered"
allowed_hosts = ["api.slack.com", "hooks.slack.com"]
PolicyBehavior
deny (default)All HTTP requests blocked
allow_egressAll outbound URLs allowed
allow_egress_filteredOnly URLs matching allowed_hosts are allowed (includes subdomains)

Every az::http_request call passes through three checks before the request is sent:

  1. Capability — the project’s policy.yml must allow NetworkRequest
  2. URL allowlist — the URL’s host must match the plugin’s declared allowed_hosts
  3. PII scan — the request body is scanned for secrets and PII. If any are detected, the request is blocked.

The response is returned as a JSON string: {"status": 200, "body": "..."}.

Plugin execution follows the same security model as WASM skills:

  • Policy-gated: wasm_execution must be allow or require_approval in .agentzero/policy.yml
  • Path validation: All filesystem paths pass through the PathValidator which blocks traversal attacks and sensitive paths (.ssh, .env, .aws/credentials)
  • Memory capped: 64 MB default
  • Time-limited: 30-second fuel budget
  • No ambient access: No filesystem or network access except through declared host imports
  • Import verification: Modules with undeclared imports are rejected before execution
  • Audit trail: Every plugin execution emits a structured audit event
  • Network controlled: HTTP requests require explicit [plugin.network] declaration, URL allowlist, and PII body scan

Plugin commands are automatically exposed as MCP tools when running az mcp. MCP clients (Claude Code, Cursor, Zed) can discover and invoke plugin commands without any configuration.

Plugin tools appear as plugin_<name>_<command> in the MCP tools list:

plugin_brain_query — [plugin:brain] Search the vault
plugin_brain_capture — [plugin:brain] Capture a thought to today's daily note

Installed plugins are tracked in .agentzero/plugins/plugins.lock:

{
"brain": {
"version": "0.1.0",
"source": "github:auser/brain-plugin",
"checksum": "sha256:a1b2c3..."
}
}

Use verify_integrity() to check WASM checksums against the lockfile.