Reference · §06

Plugins

Extend parc with WebAssembly plugins. Plugins are .wasm binaries packaged with TOML manifests, installed into <vault>/plugins/. They can hook into the fragment lifecycle, validate fragments, render custom output, and add new top-level CLI subcommands.

Plugins require the wasm-plugins cargo feature:

cargo install --path parc-cli --features wasm-plugins

Plugin manifest types and the management subcommands work without the feature — only runtime loading and execution require it. The default builds carry zero wasmtime overhead.

# Plugin commands

parc plugin list                              # list installed plugins
parc plugin info <name>                       # show plugin details
parc plugin install <path> [--manifest file]  # install a plugin
parc plugin remove <name> [--force]           # remove a plugin

install accepts either a directory containing plugin.toml and a .wasm file or a path directly to the manifest with --manifest.

# Manifest format

# .parc/plugins/my-plugin/plugin.toml
[plugin]
name = "my-plugin"
version = "0.1.0"
description = "Does something useful"
author = "alice"
wasm = "my-plugin.wasm"

[capabilities]
read_fragments = true
write_fragments = false
hooks = ["post-create", "pre-update"]
validate = ["todo"]
render = ["note"]
extend_cli = ["my-command"]

[config]
# Optional plugin-specific defaults — overridden by <vault>/config.yml#plugins.my-plugin
default_setting = "value"

# Capability sandbox

Plugins run in a wasmtime sandbox. They can only do what their manifest declares. parc verifies the requested capabilities at install time and again at load time.

Capability Effect
read_fragments Plugin can call parc_host::fragment_get, fragment_list, fragment_search
write_fragments Plugin can call fragment_create, fragment_update, fragment_delete
hooks = [...] Plugin is invoked for these lifecycle events
validate = [...] Plugin's validate(type, fragment) is called for these types after schema validation
render = [...] Plugin's render(type, fragment) is called when displaying these types
extend_cli = [...] Plugin adds these names as top-level CLI subcommands

Plugins have no filesystem access beyond what parc gives them through the parc_host namespace, no network, no environment variables, no spawning processes.

# Lifecycle hooks

Hook When Can mutate?
pre-create Before a new fragment is written Yes (return modified fragment)
post-create After a new fragment is written No (read-only)
pre-update Before an edit lands Yes
post-update After an edit lands No
pre-delete Before a soft-delete Can abort by returning an error
post-delete After a soft-delete No

A plugin returning an error from a pre-* hook aborts the operation and propagates the error to the user.

# Extending the CLI

A plugin that declares extend_cli = ["weekly-review"] exposes itself as parc weekly-review. parc dispatches the subcommand and any arguments to the plugin's cli_dispatch(args: Vec<String>) function.

parc weekly-review                  # dispatched to the plugin
parc weekly-review --since monday   # arguments are forwarded as-is

Plugin output is written to stdout via parc_host::print (or print_json); errors via parc_host::error. parc takes care of TTY detection and colour stripping.

# Compiling a plugin

Plugins are standard Rust crates compiled to wasm32-unknown-unknown. parc ships a parc-plugin-sdk crate that re-exports the host bindings and helper macros — see the SDK documentation for the full API surface.

# Cargo.toml
[lib]
crate-type = ["cdylib"]

[dependencies]
parc-plugin-sdk = "0.1"
use parc_plugin_sdk::*;

#[parc_plugin]
fn validate(ty: &str, fragment: &Fragment) -> ValidateResult {
    if ty == "todo" && fragment.title.len() > 80 {
        ValidateResult::error("todo titles must be 80 characters or less")
    } else {
        ValidateResult::ok()
    }
}

Build with cargo build --target wasm32-unknown-unknown --release and install the resulting .wasm plus a plugin.toml with parc plugin install ./target/wasm32-unknown-unknown/release/.

# Configuration

Per-plugin settings live under the plugins key in the vault's config.yml:

plugins:
  my-plugin:
    api_url: https://example.invalid
    threshold: 5

parc passes the matching subtree to the plugin's init(config: serde_json::Value) callback when the plugin is loaded. Updates to config.yml only take effect the next time the plugin is loaded — parc reindex reloads plugins as a side effect.

# When plugins make sense

Plugins are the right tool for:

  • Custom validation that can't be expressed in a YAML schema
  • Custom output rendering — e.g. PlantUML / Mermaid → ASCII art for terminal display
  • Wiring parc into another tool's lifecycle — e.g. push new todos to an external tracker
  • Adding domain-specific subcommands that share parc's vault, schema, and indexing infrastructure

For one-off scripts, the JSON-RPC server is usually a better fit — no Rust required, no .wasm build pipeline, and the same surface area.