> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.trysulfur.net/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.trysulfur.net/_mcp/server.

# Manifest reference

Every plugin directory must contain a `plugin.toml`. The manifest has **four required fields** and
one optional field:

```toml title="plugin.toml"
name = "vanilla_cat"          # required — the plugin's unique name
version = "0.1.0"             # required — informational version string
entrypoint = "main.star"      # required — script path, relative to the plugin dir
runtime = "starlark"          # required — "starlark" or "python"
capabilities = ["entities.read", "entities.write", "world.read", "nav"]   # optional
```

## Required fields

| Field        | Type   | Rules                                                                                                                                                                         |
| ------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`       | string | Non-empty. Used for log attribution, unload, and duplicate detection.                                                                                                         |
| `version`    | string | Non-empty.                                                                                                                                                                    |
| `entrypoint` | string | Non-empty. Must be a **relative path inside the plugin directory** — absolute paths and any `..` segment are rejected (path-traversal guard). The path is cleaned before use. |
| `runtime`    | string | `"starlark"` or `"python"`. Anything else is rejected loudly (or skipped with a log in the tolerant operator scan).                                                           |

A manifest missing any required field fails at load with
`manifest <path>: missing required field "<name>"`.

## Capabilities

`capabilities` is the plugin's **least-privilege grant list**. It defaults to empty — a plugin
with no capabilities can subscribe to events, `log()`, `chat()`, and register recipe matchers, but
it cannot read or mutate any entity, world, or navigation state.

The vocabulary is **locked and closed**. The seven valid strings:

| Capability       | Grants                                                                                                                                                             |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `entities.read`  | Reading entity state: position, rotation, health, velocity, type, damage/breeding/fluid flags, `attribute(name)`, the nearest-player-holding scans.                |
| `entities.write` | Mutating an entity: `set_velocity`, `set_attribute`, `set_look`, `set_look_at`, `try_breed`, the eat-animation broadcast (plus `move_to`, which also needs `nav`). |
| `world.read`     | Reading the world: `block_at`, `entities_near`, `nearest_player`, `is_dark`.                                                                                       |
| `world.write`    | Mutating the world: `set_block` (and host seams that write blocks, like the sheep's `eat_grass_block`).                                                            |
| `nav`            | Issuing navigation requests: `move_to`, `nav.path_to`, `nav.stop`, `nav.jump`.                                                                                     |
| `skills.damage`  | Declaring the skill `mechanic("damage", ...)` — enforced **at load**.                                                                                              |
| `skills.effects` | Declaring the skill `mechanic("effect", ...)` — enforced **at load**.                                                                                              |

### Enforcement model

* **Unknown capability strings are rejected at load.** A typo like `"entitys.read"` fails the
  plugin's load with
  `unknown capability "entitys.read" (valid: entities.read, entities.write, world.read, world.write, nav, skills.damage, skills.effects)`
  — a capability can never silently grant nothing.
* **Handle operations check per-op at call time.** A denied call returns a visible Starlark error
  the plugin author sees — `capability denied: this plugin lacks "world.write"` — never a silent
  no-op. See [Handles & capabilities](/plugins/handles-and-capabilities).
* **Skill mechanics check at load.** Because skills are pure data known at load time, a
  `mechanic("damage")` in a plugin whose manifest lacks `skills.damage` is a **load error**
  (fail-closed), not a runtime denial. See [Skills](/plugins/skills).

Internally the grants are a bit-set (`capSet`) stamped onto every handle the plugin receives, so
each check is a single AND at the operation boundary.

### Choosing capabilities: a real example

The bundled `vanilla_pig` plugin reads its own state and the nearest player, and writes its look
and navigation targets — nothing else:

```toml title="plugins/mobs/vanilla_pig/plugin.toml"
name = "vanilla_pig"
version = "0.1.0"
entrypoint = "main.star"
runtime = "starlark"
# Least-privilege caps: a pig READS its own state + the nearest player + WRITES its look/nav.
# It NEVER sets blocks, so NO world.write.
#   entities.read  — read x/y/z (stroll offset base, look target delta)
#   entities.write — set_look_at (the LOOK goals' LookControl.setLookAt analogue)
#   world.read     — nearest_player (LookAtPlayerGoal's getNearestPlayer)
#   nav            — path_to/has_path (WaterAvoidingRandomStrollGoal's navigation.moveTo/isDone)
capabilities = ["entities.read", "entities.write", "world.read", "nav"]
```

An event-only plugin needs no capabilities at all:

```toml title="plugins/gate_events/plugin.toml"
name = "gate_events"
version = "0.1.0"
entrypoint = "main.star"
runtime = "starlark"
# NO capabilities: gate_events only READS the discrete event payload (frozen scalars) and
# reacts via the chat() host builtin. It sets no blocks, spawns nothing, reads no world handle.
capabilities = []
```