Custom Plugins

Extend Aura with your own commands, hooks, and tools.

Overview

The integrations shipped in Aura (GitHub, GitLab, Linear, Jira, Slack, Discord) cover ~90% of typical stacks. For the rest — an internal code-review tool, a bespoke ticket system, a compliance pipeline, a custom orchestrator — Aura exposes a plugin system.

A plugin can:

  • Add new CLI subcommands (e.g. aura my-org deploy-gate).
  • Register hook handlers that run on Aura events (intent logged, prove failed, zone blocked, etc.).
  • Expose new MCP tools to AI agents, namespaced under your plugin.
  • Integrate external systems by subscribing to the event stream and calling the HTTP API.
  • Read and write to plugin-private storage in .aura/plugins/<name>/.

Plugins are local to a repo or installed globally. They are sandboxed: they cannot modify Aura's core DB directly — all mutations flow through the public API.

Setup

Scaffold a plugin

aura plugin new my-plugin --lang rust
# or --lang typescript | python | go

This generates:

my-plugin/
├── aura-plugin.toml     # manifest
├── src/
│   └── main.rs          # entry point
└── README.md

Install a plugin

From a local directory:

aura plugin install ./my-plugin

From a registry:

aura plugin install aura-community/linear-advanced

From a git URL:

aura plugin install https://github.com/acme/aura-deploy-gate

Installed plugins land in ~/.aura/plugins/ (global) or .aura/plugins/ (per-repo). Per-repo overrides global.

List and remove

aura plugin list
aura plugin remove my-plugin

The Manifest

aura-plugin.toml declares everything Aura needs to load the plugin.

[plugin]
name = "deploy-gate"
version = "0.1.0"
description = "Block intent logging unless a deploy window is open."
authors = ["ashiq@example.com"]
license = "MIT"

[plugin.runtime]
kind = "rust"                           # rust | wasm | node | python | binary
entrypoint = "target/release/deploy-gate"

[plugin.api]
min_aura_version = "0.14.0"

[[plugin.commands]]
name = "deploy-gate"
description = "Configure and inspect the deploy-gate plugin."

[[plugin.hooks]]
event = "intent.before_log"
handler = "on_intent_before_log"

[[plugin.hooks]]
event = "prove.failed"
handler = "on_prove_failed"

[[plugin.mcp_tools]]
name = "deploy_gate_status"
description = "Returns whether a deploy window is open."
schema = "schema/status.json"

[plugin.permissions]
network = ["https://deploygate.internal.acme.com"]
storage = "plugin_private"              # plugin_private | repo_readonly | repo_readwrite
read_events = ["intent.*", "prove.*"]
call_api = ["intents:read", "messages:write"]

The permissions block is enforced by the Aura runtime. A plugin cannot, for example, post to a domain it didn't declare.

Hook Points

Plugins subscribe to events by name. The full catalog:

| Event | Fires | |-------|-------| | intent.before_log | Before an intent is written. Handler can veto. | | intent.after_log | After an intent is written. | | intent.mismatch | Hook detects intent-vs-diff divergence. | | prove.started | aura prove begins. | | prove.passed / prove.failed | Prove completes. | | pr_review.finding | PR review surfaces a finding. | | pr_review.done | PR review finishes. | | zone.before_claim | Before a zone is claimed. Handler can veto. | | zone.blocked | Edit blocked by a zone. | | live.push / live.pull | Mothership sync events. | | msg.received | Team or sentinel message. | | session.start / session.end | Session lifecycle. | | commit.pre / commit.post | Git hook events relayed through Aura. | | daemon.start / daemon.stop | Daemon lifecycle. |

Handlers receive the event envelope and can return:

  • Allow — default, continue.
  • Deny(reason) — vetoable events only. Aura aborts the operation and surfaces reason to the user.
  • Mutate(patch) — on events that support patching (e.g. intent.before_log can add ticket references).

Examples

Deny intent without a ticket from an internal system

A plugin that rejects intents unless they cite an OPS- ticket that exists in the company's internal tracker:

use aura_plugin::{plugin, Event, Decision, Context};

#[plugin::hook("intent.before_log")]
async fn check_ticket(ctx: &Context, evt: Event) -> Decision {
    let summary = evt.data["summary"].as_str().unwrap_or("");
    let re = regex::Regex::new(r"OPS-(\d+)").unwrap();

    let Some(m) = re.captures(summary) else {
        return Decision::Deny("Intent must reference an OPS-### ticket.".into());
    };

    let ticket = &m[0];
    let exists = ctx.http()
        .get(format!("https://ops.internal.acme.com/api/tickets/{ticket}"))
        .send().await
        .map(|r| r.status().is_success())
        .unwrap_or(false);

    if exists { Decision::Allow }
    else { Decision::Deny(format!("Ticket {ticket} not found.")) }
}

Post Prove failures to an internal compliance system

import { Plugin, on } from "@aura/plugin";

export default new Plugin("compliance-sink", {
  on: {
    "prove.failed": async (ctx, evt) => {
      await ctx.http.post("https://compliance.acme.com/api/events", {
        source: "aura",
        kind: "prove_failed",
        repo: evt.repo,
        branch: evt.branch,
        goal: evt.data.goal,
        commit: evt.data.commit,
      });
    },
  },
});

Expose a new MCP tool

Plugins can extend the MCP surface. Tools are namespaced under the plugin:

from aura_plugin import Plugin, mcp_tool

plugin = Plugin("deploy-gate")

@plugin.mcp_tool(
    name="deploy_gate_status",
    description="Returns whether a deploy window is currently open.",
)
async def status(ctx, args):
    open = await ctx.http.get_json("https://deploygate.internal.acme.com/state")
    return {"window_open": open["is_open"], "closes_at": open["closes_at"]}

AI agents see this as plugin_deploygate_deploy_gate_status in their MCP tool list.

Custom CLI subcommand

A manifest-declared command dispatches to the plugin binary:

aura deploy-gate status

The plugin receives the args via stdin as JSON and returns output on stdout. See the scaffolded example.

Distribution

The plugin registry

plugins.aura.build hosts the community registry. Publishing requires:

  1. aura plugin publish from the plugin repo.
  2. A signed release — plugins must be signed with a key registered to the publisher account.
  3. Metadata review for the first publish of a given name.

Installed plugins from the registry are verified against the publisher signature every time the daemon loads them.

Private distribution

For internal plugins, skip the public registry:

aura plugin install https://git.internal.acme.com/platform/aura-deploy-gate.git

Or pack and distribute a tarball:

aura plugin pack
# produces my-plugin-0.1.0.tar.gz
aura plugin install ./my-plugin-0.1.0.tar.gz

For air-gapped environments, place the tarball in ~/.aura/plugins/ and run aura plugin reload.

Community Plugins

A non-exhaustive list of plugins maintained by the community or Aura contributors:

| Plugin | What it does | |--------|--------------| | aura-asana | Asana ticket bridge — parallel to built-in Linear/Jira. | | aura-shortcut | Shortcut (formerly Clubhouse) integration. | | aura-pagerduty | Fires PagerDuty incidents on prove failures on main. | | aura-datadog | Emits intent and prove metrics as Datadog events. | | aura-notion | Mirrors intents into a Notion database. | | aura-teams | Microsoft Teams webhook destination. | | aura-bazel | Scoped prove runs for Bazel build graphs. | | aura-monorepo-scope | Intent/prove scoping for Turborepo and Nx. |

Browse and submit at plugins.aura.build.

Troubleshooting

Plugin not loading. Check aura doctor — it reports manifest errors and permission violations. Logs live in ~/.aura/logs/plugins/<name>.log.

permission denied: network. The plugin tried to call a domain not in plugin.permissions.network. Add it to the manifest and reinstall.

Handler slow, blocking intent log. Vetoable hooks (intent.before_log, zone.before_claim) have a default 2-second timeout. Raise with timeout_ms = 5000 on the hook entry, or do the work async and return Allow immediately.

Signature verification failed. The publisher rotated keys. Re-install the latest version — Aura caches the new public key automatically.

Plugin crashes crash the daemon. They shouldn't — plugins run in isolated processes for all runtimes except binary. If the daemon dies, the plugin is binary and linked into the wrong ABI. Switch to rust, wasm, node, or python.

Local plugin dev loop. Use aura plugin dev ./my-plugin to hot-reload on file change without re-installing.

Runtime Details

Plugins run in language-specific runtimes managed by the daemon.

Rust plugins compile to native binaries and run as child processes. Aura communicates with them over a JSON-RPC protocol on stdin/stdout. They are the fastest and most memory-efficient option.

WASM plugins run inside a Wasmtime sandbox embedded in the daemon. They have the strongest isolation — no filesystem access beyond what the host explicitly grants, no arbitrary syscalls — and are the recommended choice for distribution via the public registry.

Node plugins run under a pinned Node.js runtime shipped with Aura (currently Node 20 LTS). No external Node install is required.

Python plugins run under a pinned CPython 3.12 interpreter. Third-party packages are declared in pyproject.toml and installed into a plugin-private virtualenv on install.

Binary plugins are opaque executables. They are supported for legacy compatibility but are not sandboxed, so the registry does not accept them.

Plugin Lifecycle

  1. Install. aura plugin install verifies the manifest, checks signatures for registry plugins, provisions the runtime, and writes to .aura/plugins/<name>/.
  2. Load. On daemon start (or hot-reload), each plugin is loaded, its hooks registered, and its MCP tools added to the tool table.
  3. Invoke. Events matching a hook, or MCP tool calls to a plugin-provided tool, are routed to the plugin via JSON-RPC.
  4. Unload. On daemon stop or aura plugin remove, the plugin is given 5 seconds to flush state, then terminated.
  5. Upgrade. aura plugin upgrade pulls a newer version, verifies it, and swaps atomically; in-flight calls complete on the old version.

Observability

Every plugin invocation is logged with latency, input size, and output size. Surface them via:

aura plugin stats          # recent invocations, p50/p95/p99 latency
aura plugin logs my-plugin # tail the plugin's stderr

High-latency plugins block whatever they hook. Prove-failed hooks that take 10 seconds delay user feedback by 10 seconds. Keep hook handlers under 500ms wherever possible; offload heavy work to async background tasks.

Testing Plugins

The aura-plugin crate (Rust) and @aura/plugin package (TypeScript) include test harnesses that let you drive hooks synthetically:

#[tokio::test]
async fn test_ticket_check_rejects_missing() {
    let harness = aura_plugin::test::Harness::new();
    let evt = harness.synth_event("intent.before_log", json!({
        "summary": "just a cleanup"
    }));
    let decision = check_ticket(&harness.ctx(), evt).await;
    assert!(matches!(decision, Decision::Deny(_)));
}

The harness mocks the HTTP client, the Aura API, and the event envelope. Real integration tests run under aura plugin dev with a disposable daemon.

See Also