Plugins and hooks

Extend Xal with trusted in-process plugins that can register tools, providers, UIs, commands, and lifecycle hooks.

Loading plugins

The top-level plugins array tells Xal what to load. It does not install or download anything. Every referenced plugin must already exist and be resolvable when Xal starts.

Each entry supports one of these forms:

{
  "plugins": ["/absolute/path/to/my-plugin"]
}

The referenced directory must contain a plugin.ts whose default export has a name, a synchronous register function, and optionally asynchronous bootstrap and shutdown functions. Relative plugin paths are not resolved from the project directory, even when declared in project configuration.

Plugin registration is transactional. If importing, validating, or registering a plugin fails, Xal records a plugin registration failure and keeps none of that plugin's contributions.

Lifecycle

ctx.runtime exposes the app name and version, app home and cache paths, coordinated credential loading and saving, and transient secret protection. Provider plugins should use this runtime instead of reading or rewriting Xal's shared credential file directly.

Plugins can contribute slash commands with ctx.registerCommand. Commands known synchronously belong in register; commands discovered from files or services may be added during bootstrap, before interactive input is released.

When the UI or CLI exits, Xal aborts ctx.signal so in-progress bootstrap work can stop, waits for bootstrap to settle, and then runs shutdown in reverse plugin order. Plugins that own child processes or network connections close them there. A dynamically discovered tool can be removed with ctx.unregisterTool(tool) using the same tool object that was registered.

Hooks

Plugins register trusted lifecycle hooks with ctx.registerHook. Hooks run in built-in and configured-plugin order. Multiple hooks for the same event run sequentially, and a replacement from one hook becomes the input to the next.

HandlerInputAllowed result
promptModel-facing prompt text and image countReplace the text or reject the prompt
beforeToolTool name, call ID, and JSON argumentsReplace the arguments or block the call
afterToolTool details and its model-facing outputReplace the output
turnEndFinal output and token usage when availableNo result; use it for lifecycle automation or observability

Every handler also receives a context containing an abort signal and the session ID, kind, working directory, provider, model, and permission mode. Prompt changes affect what the model sees while the TUI keeps the user's original text. Tool argument changes happen before scheduling and permission evaluation, so Xal authorizes and records the effective action. Post-tool hooks also run for failed or interrupted executions, but not for calls blocked before execution.

Hook failures stop prompt, pre-tool, and turn-completion processing. A post-tool failure becomes a failed tool result that warns the model the tool may already have changed state. Hook inputs and code run inside Xal's process, so only load hooks you trust. Returned text and arguments pass through secret redaction before they reach the model, session storage, or TUI.

Hook example

This plugin marks prompts and read results, and blocks an exact git push command:

export default {
  name: "visual-hooks",
  register(ctx) {
    ctx.registerHook({
      name: "marker",
      prompt(input) {
        return { type: "replace", text: `${input.text}\n\nInclude the exact marker HOOKS_ACTIVE in the answer.` }
      },
      beforeTool(input) {
        if (input.tool !== "bash" || input.args.command !== "git push") return
        return { type: "block", reason: "Publishing is disabled by the visual hook." }
      },
      afterTool(input) {
        if (input.tool !== "read") return
        return { type: "replace", output: `[visual-hooks]\n${input.output}` }
      },
    })
  },
}

Put the file at plugin.ts inside a plugin directory and add that directory's absolute path to plugins. In the TUI, /hooks lists every registered hook and the events it handles. Each completed primary-session hook invocation appears in the transcript with its action and elapsed time; task-agent hook invocations appear in that agent's job output.

Plugin configuration

A custom plugin receives the object under pluginConfig whose key matches its exported plugin name:

{
  "pluginConfig": {
    "example-plugin": {
      "enabled": true
    }
  }
}

Built-in plugin options are documented with their features in TUI, Integrations, and Providers and models.