---
title: "Build a Trusted Runtime Extension"
source: https://docs.autohand.ai/tutorials/extensions/build-runtime-extension
---

# Build a trusted runtime extension

Create a real Autohand Code extension that users run with /deploy. Along the way you will add a stateful Ink menu, status and help content, a keyboard shortcut, a CLI flag, a lifecycle hook, a provider, and permission policy.

Intermediate · About 35 minutes

$extension-builder create a runtime showcase with /deploy, an Ink deployment menu, ctrl+k, a provider, and a permission policy

## What you will learn

-   Package compiled JavaScript behind `contributes.runtime`.
-   Register commands, custom Ink UI, line segments, shortcuts, and flags.
-   Connect session hooks, an `extension:` provider, and permission policy.
-   Make an informed `--trust` decision and prove the complete lifecycle.
-   Use the installed extension every day without invoking `$extension-builder`.

## Before you start

-   **Autohand Code:** use a build that includes trusted runtime Extension API v1.
-   **A safe workspace:** run the walkthrough in a disposable Git repository.
-   **Source review:** runtime extensions execute inside the Autohand process and are not sandboxed.

**`--trust` grants code execution.** A trusted entrypoint has the same operating-system access as Autohand. Permission contributions govern actions routed through Autohand; they do not restrict arbitrary extension code.

## Step 1: Scaffold the package

Create a package with a compiled runtime directory. This tutorial uses JavaScript directly so there is no build step.

``` bash
mkdir -p autohand.runtime-showcase/dist
cd autohand.runtime-showcase
touch README.md autohand.extension.json dist/extension.mjs
```

TypeScript authors can keep `src/extension.ts`, import the public `ExtensionRuntimeAPI` type from `autohand-cli`, and compile to `dist/extension.mjs`. Autohand does not transpile TypeScript or install package dependencies.

## Step 2: Declare the runtime

Save this manifest as `autohand.extension.json`:

``` json
{
  "$schema": "https://raw.githubusercontent.com/autohandai/code-extensions/main/schema/autohand.extension.schema.json",
  "schemaVersion": 1,
  "extensionApi": 1,
  "id": "autohand.runtime-showcase",
  "name": "Runtime Showcase",
  "version": "1.0.0",
  "description": "Demonstrate trusted runtime Extension API v1 capabilities.",
  "license": "Apache-2.0",
  "contributes": {
    "runtime": ["dist/extension.mjs"]
  }
}
```

Runtime paths must remain inside the package and end in `.js`, `.mjs`, or `.cjs`. Validation reads the file but never imports it.

## Step 3: Build the command and Ink view

Start `dist/extension.mjs` with a stateful view and a command that opens it:

``` js
export async function activate(api) {
  const { React, Ink } = api.ui;

  function DeploymentView({ close, workspaceRoot, environment }) {
    const choices = ['Plan deployment', 'Validate release', 'Cancel'];
    const [selected, setSelected] = React.useState(0);

    Ink.useInput((_input, key) => {
      if (key.upArrow) {
        setSelected(current => (current - 1 + choices.length) % choices.length);
      } else if (key.downArrow) {
        setSelected(current => (current + 1) % choices.length);
      } else if (key.return) {
        const choice = choices[selected];
        close(choice === 'Cancel'
          ? 'Deployment cancelled.'
          : `${choice} selected for ${environment}.`);
      }
    });

    return React.createElement(
      Ink.Box,
      { flexDirection: 'column', marginTop: 1 },
      React.createElement(Ink.Text, { color: 'green' }, 'Trusted runtime extension active'),
      React.createElement(Ink.Text, null, `Target: ${environment}`),
      React.createElement(Ink.Text, { dimColor: true }, `Workspace: ${workspaceRoot}`),
      React.createElement(Ink.Text, { dimColor: true }, 'Use arrows and Enter. Escape closes.'),
      ...choices.map((choice, index) => React.createElement(
        Ink.Text,
        { key: choice, color: selected === index ? 'cyan' : undefined },
        `${selected === index ? '❯' : ' '} ${choice}`,
      )),
    );
  }

  api.ui.registerView({
    id: 'autohand.runtime-showcase.deploy',
    title: 'Deployment console',
    component: DeploymentView,
  });

  api.commands.register({
    command: '/deploy',
    description: 'Open the extension deployment console',
    execute(context) {
      const environment = context.args[0]
        || context.cli.getOption('deployEnvironment')
        || 'staging';
      return context.ui.open('autohand.runtime-showcase.deploy', { environment });
    },
  });
}
```

Use the React 19 and Ink 7 instances exposed by `api.ui`. Bundling another React or Ink copy can break hooks. Autohand owns modal pause/resume, Escape, Ctrl+C, and terminal cleanup.

## Step 4: Add line content, a shortcut, and a flag

Add these registrations inside `activate`, before its closing brace:

``` js
api.ui.setStatusLine({
  segments: [
    { id: 'runtime-showcase-status', text: 'extensions:ready', color: 'success' },
  ],
});

api.ui.setHelpLine({
  segments: [
    { id: 'runtime-showcase-help', text: 'ctrl+k deploy', color: 'accent' },
  ],
});

api.keybindings.register({
  key: 'ctrl+k',
  command: '/deploy',
  when: 'input-empty',
});

api.cli.registerFlag({
  flags: '--deploy-environment <name>',
  description: 'Default deployment environment',
  defaultValue: 'staging',
});
```

Commands, flags, shortcuts, providers, views, and segment IDs cannot collide with built-ins or another extension. A collision rejects the complete activation instead of leaving partial state.

## Step 5: Register a lifecycle hook

Use the existing hook contract to add context when a session starts:

``` js
api.hooks.on('session-start', () => ({
  additionalContext: 'The runtime showcase is active. Use /deploy for its deployment console.',
}));
```

Runtime hooks share deterministic ordering and the normal hook response contract. Disabling or removing the extension removes its hook immediately.

## Step 6: Add an extension provider

Providers use the reserved `extension:` namespace. Add this deterministic local provider inside `activate`:

``` js
api.providers.register({
  name: 'extension:showcase',
  displayName: 'Showcase Provider',
  create(config) {
    let model = config.model;
    return {
      getName: () => 'extension:showcase',
      async complete(request) {
        const lastMessage = request.messages.at(-1);
        const content = typeof lastMessage?.content === 'string'
          ? lastMessage.content
          : 'an Autohand request';
        return {
          id: `showcase-${Date.now()}`,
          created: Math.floor(Date.now() / 1000),
          content: `Showcase provider (${model}) received: ${content}`,
          finishReason: 'stop',
          raw: { provider: 'extension:showcase', model },
        };
      },
      listModels: async () => ['showcase-local'],
      isAvailable: async () => true,
      setModel: nextModel => { model = nextModel; },
      getModel: () => model,
    };
  },
});
```

A production provider can read provider-owned settings from `extensionProviders`. Keep credentials in user configuration or environment variables, never in the extension package.

## Step 7: Contribute permission policy

Add one narrow allow and one explicit deny:

``` js
api.permissions.registerPolicy({
  allowList: ['run_command:git status --short'],
  denyList: ['run_command:npm publish'],
});
```

Permission policy applies only to actions routed through Autohand. The immutable security blacklist is checked first and cannot be overridden by an extension, unrestricted mode, or user configuration. An extension cannot replace the session permission mode or decision cache.

## Step 8: Validate, review, and trust the package

Return to the package parent, validate without execution, review the entrypoint, then make the trust decision explicitly:

``` bash
autohand extensions validate ./autohand.runtime-showcase
sed -n '1,240p' ./autohand.runtime-showcase/dist/extension.mjs
autohand extensions install ./autohand.runtime-showcase --trust
autohand extensions show autohand.runtime-showcase
autohand extensions doctor
```

**Validation is safe to automate.** It checks the manifest, paths, runtime file type, and package limits without importing the runtime. Only trusted installation allows activation.

## Step 9: Use the extension in Autohand

Start Autohand with the extension flag:

``` bash
autohand --deploy-environment quality-assurance
```

Confirm `extensions:ready` in the status line and `ctrl+k deploy` in the help line. Then run the daily command:

``` text
/deploy production
```

Move through the deployment menu with the arrow keys and press Enter. With an empty composer, press `ctrl+k` to reopen it using `quality-assurance` from the CLI flag. Press Escape to close it safely.

**Daily usage is direct.** Users run `/deploy` or the registered shortcut. `$extension-builder` is an authoring skill, not the runtime command.

## Step 10: Configure the provider and prove policy

Select the provider in `~/.autohand/config.json`:

``` json
{
  "provider": "extension:showcase",
  "extensionProviders": {
    "extension:showcase": {
      "model": "showcase-local"
    }
  }
}
```

Start a fresh session and send a harmless prompt to confirm the provider response. Then ask Autohand to run `git status --short` and confirm the exact allow entry. Ask it to run `npm publish` and confirm denial occurs before execution.

## Step 11: Test disable, enable, and removal

Lifecycle operations must remove every runtime registration cleanly:

``` text
/extensions disable autohand.runtime-showcase
/deploy
# Expected: Command /deploy is not supported.

/extensions enable autohand.runtime-showcase
/deploy staging

/extensions doctor
/extensions remove autohand.runtime-showcase --yes
```

The top-level commands are equivalent when Autohand is not running:

``` bash
autohand extensions disable autohand.runtime-showcase
autohand extensions enable autohand.runtime-showcase
autohand extensions remove autohand.runtime-showcase --yes
```

After disable or removal, the slash command, view, line segments, shortcut, flag, hook, provider, and permission policy must no longer be active.

## What you learned

-   Declared a compiled runtime entrypoint without executing code during validation.
-   Built a slash command and stateful Ink interface that preserve terminal behavior.
-   Registered line content, a shortcut, flag, hook, provider, and permission policy.
-   Installed with explicit trust and used the extension directly with `/deploy`.
-   Proved that disable and removal clean up every registration.

### Try next

$extension-builder adapt this runtime showcase into a release extension with /release, a version picker, and a provider-backed changelog summary

## Related tutorials

[

### Using skills and slash commands

Understand the daily invocation model for skills and commands.

Beginner · 10 min](/tutorials/using-skills-and-commands.html)[

### Extension authoring reference

Review the complete API, validation, trust, compatibility, and publishing contracts.

Reference](https://github.com/autohandai/code-cli/blob/main/docs/extension-authoring.md)