> For the complete documentation index, see [llms.txt](https://docs.kouch.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kouch.dev/docs/plugin_authoring.md).

# Writing a Kouch plugin

A plugin adds a feature to an emulator session: Quick Menu actions, reactions when a game starts or stops, or the emulator's own hotkeys sent at the right moment. Plugins are small **WebAssembly** modules. They run in Kouch's sandbox with no filesystem, network, clock or environment of their own. They can only call the `kouch` host functions below, and only the ones the user approved (ADR 0011).

A working example lives in `examples/plugins/save-slots/`: two Quick Menu actions that send quick-save/quick-load hotkeys.

## Package

A plugin is a folder:

```
my-plugin/
  plugin.json
  plugin.wasm
```

```json
{
  "id": "my-plugin",
  "name": "My plugin",
  "version": "1.0.0",
  "api": 1,
  "entry": "plugin.wasm",
  "description": "One sentence shown in Settings.",
  "capabilities": ["quick_menu", "hotkeys", "toasts"],
  "profiles": []
}
```

* **`id`:** 1–64 characters, drawn from `a-z 0-9 - _ .`.
* **`entry`:** a relative `.wasm` path inside the folder. The module can be at most 8 MB.
* **`profiles`:** the emulator profile ids the plugin applies to. Leave it empty to apply to every emulator.
* **Unknown fields are rejected.**

**Installing and enabling:**

* A plugin is installed from a Workshop item (tag `plugin`), from a `.kod` with `plugins/<id>/`, or from a folder.
* Every plugin starts **disabled**. Enabling it approves exactly the capabilities listed.
* If a later version asks for more capabilities, it is disabled until the user approves it again.

## Capabilities

| Capability        | Allows                                                                                          |
| ----------------- | ----------------------------------------------------------------------------------------------- |
| `quick_menu`      | `menu_action`: add rows to the Quick Menu while a game runs                                     |
| `session_events`  | (reserved) session start/stop are always delivered; this will gate finer events                 |
| `prelaunch`       | (reserved) options on the pre-launch screen                                                     |
| `hotkeys`         | `hotkey`: send the emulator a key chord                                                         |
| `toasts`          | `toast`: show a short message                                                                   |
| `settings`        | `setting_get` / `setting_set`: up to 64 small string settings, kept across sessions             |
| `app_actions`     | ABI 2, app plugins: `app_action`, rows on the plugin's page in Settings › Plugins               |
| `library_read`    | ABI 2: `library_list`, the game list (titles, systems, art, game information; never file paths) |
| `steam_shortcuts` | ABI 2, app plugins: `steam_user`, `steam_shortcuts_read`, `steam_stage` (see below)             |
| `files_saves`     | `file_read` / `file_write` under `saves/`, the emulator's saves folder                          |
| `files_storage`   | `file_read` / `file_write` under `storage/`, the emulator's storage folder                      |

* **Files:** file paths are relative (`saves/slots/1.txt`). A path with `..`, a drive, or a `bios`/`system` component is refused. The files limit is 16 MB per call. Kouch never gives a plugin access to BIOS, firmware or keys.
* **Refused calls:** a call without its capability returns `-2` and is reported to the user. It is never carried out.
* **Size limits** (`crates/kouch-plugins/src/host.rs`):
  * any string a plugin passes (a log line, toast, hotkey, menu label) is capped at 64 KiB;
  * each key in a `hotkey` chord is 1–16 characters, ASCII letters and digits only;
  * `steam_stage` takes at most 50,000 grid files per call.

## ABI (version 1)

Data crosses the boundary as UTF-8 JSON or text in the module's linear memory.

**The module exports:**

| Export           | Signature                     | Purpose                                                      |
| ---------------- | ----------------------------- | ------------------------------------------------------------ |
| `memory`         | memory                        | linear memory Kouch writes events into                       |
| `kouch_api`      | `() -> i32`                   | must return `1`                                              |
| `kouch_alloc`    | `(len: i32) -> i32`           | returns a pointer to `len` writable bytes for the next event |
| `kouch_on_event` | `(ptr: i32, len: i32) -> i32` | handles one event; return `0`                                |

**It imports, from module `kouch`** (`ptr/len` pairs point into its own memory):

| Import        | Signature                               | Notes                                                                                  |
| ------------- | --------------------------------------- | -------------------------------------------------------------------------------------- |
| `log`         | `(ptr, len)`                            | goes to Kouch's log                                                                    |
| `toast`       | `(ptr, len) -> i32`                     | text                                                                                   |
| `hotkey`      | `(ptr, len) -> i32`                     | JSON array of 1–4 key names, e.g. `["shift","f1"]`                                     |
| `menu_action` | `(ptr, len) -> i32`                     | JSON `{"id":"save","label":"Quick save"}`; the label is at most 80 characters          |
| `setting_get` | `(kptr, klen, out_ptr, out_cap) -> i32` | writes the value; returns its length, `-1` if unset, or `-3` if `out_cap` is too small |
| `setting_set` | `(kptr, klen, vptr, vlen) -> i32`       | value at most 4 KB                                                                     |
| `file_read`   | `(pptr, plen, out_ptr, out_cap) -> i32` | returns the bytes read                                                                 |
| `file_write`  | `(pptr, plen, dptr, dlen) -> i32`       | atomic replace; returns the bytes written                                              |

**Return codes:** `>= 0` means success, `-1` invalid or not found, `-2` capability denied, `-3` too large.

**Events** are delivered as JSON with a `type` field:

```json
{"type":"init","profile_id":"emu"}
{"type":"session_start","profile_id":"emu","game_title":"…","game_path":"…"}
{"type":"session_stop","profile_id":"emu"}
{"type":"menu_action","id":"save"}
```

* **Registering menu actions:** do it during `init` or `session_start`. They exist until the session ends.
* **Budget per event:** each event runs with a fixed instruction budget, and memory is capped at 64 MB. A plugin that loops forever traps (the event fails), but the plugin stays loaded for the next one.

## ABI (version 2)

Version 2 (ADR 0016) keeps everything in version 1 and adds **app plugins** and three capabilities. A version-1 plugin keeps working unchanged.

* **`plugin.json`:** `"api": 2` and `"scope": "app"` (the default is `"session"`). An app plugin is loaded when Kouch starts, while it is enabled, and applies to no profiles. `kouch_api` returns `2`.
* **Capabilities:** `app_actions` and `steam_shortcuts` need `"scope": "app"`; `library_read` needs API 2.
* **Events:** `{"type":"init"}` (no `profile_id` for an app plugin) and `{"type":"app_action","id":"sync"}`.
* **Out buffers:** every version-2 import that returns data answers `out_cap == 0` with the length it needs and writes nothing. Call it once to size, allocate, then call it again.

| Import                 | Signature                   | Notes                                                                                                                                                                                                       |
| ---------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `app_action`           | `(ptr, len) -> i32`         | JSON `{"id":"sync","label":"Add games to Steam","description":"…"}`; id 1–64, label ≤ 80, description ≤ 200 and optional. Register during `init`.                                                           |
| `library_list`         | `(out_ptr, out_cap) -> i32` | JSON `[{"id","title","system","system_name","art":{"tall","wide","hero","logo","icon"},"info"}]`. `id` is the game's stable id; each art value is an opaque art path or `null`. Missing games are left out. |
| `steam_user`           | `(out_ptr, out_cap) -> i32` | JSON `{"account_id","steam_id","name","steam_dir","kouch_exe","kouch_start_dir"}` for the most recent Steam user; `-1` when there is no Steam or user.                                                      |
| `steam_shortcuts_read` | `(out_ptr, out_cap) -> i32` | that user's binary `shortcuts.vdf`; `0` when there is none                                                                                                                                                  |
| `steam_stage`          | `(ptr, len) -> i32`         | JSON `{"vdf":"<base64>","grid":[{"name":"<appid>p.png","src":"<art path>"}],"summary":{…}}`, at most 24 MB; `0` = waiting for the user, `-1` = refused (the reason goes to Kouch's log)                     |

**What `steam_stage` accepts.** The plugin never writes a file itself. Kouch checks the proposed file and asks the user:

* **Only Kouch's shortcuts may change.** A shortcut is Kouch's when its `Exe` is `kouch_exe` and its launch options contain `--launch=`. Every other entry must come back unchanged and in the same order.
* **Grid files** must be named `<appid>`, `<appid>p`, `<appid>_hero`, `<appid>_logo` or `<appid>_icon`, with `.png` or `.jpg`. They may only be for Kouch's shortcuts, and their `src` must be an art path `library_list` returned. Kouch converts other image formats itself.
* **Art for removed shortcuts** is deleted by Kouch.

**After confirming.** Steam closes, Kouch backs up the old file, writes the new one and the art, and starts Steam again. When Kouch was started by Steam, it closes first and reopens through Steam. In Deck Game Mode the change is refused.

## Building

Any language that compiles to `wasm32-unknown-unknown` works: Rust, C, Zig, AssemblyScript and others. Don't use WASI; none of it is provided.

For the WAT example:

```sh
cargo run -p kouch-lab -- wat2wasm examples/plugins/save-slots/plugin.wat examples/plugins/save-slots/plugin.wasm
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kouch.dev/docs/plugin_authoring.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
