{"page":"engine","title":"The engine interface","blurb":"Every noun and verb on $, what's new since 2.1.283, and the calls with surprises in them.","build":"2.1.286","sections":["Every verb is also an event","The nouns","What's new since 2.1.283","What arrived between 2.1.272 and 2.1.283","$.plugin isn't a capability","$.telemetry is for the built-ins","The ops with surprises in them","$ outlives its dispatch","engine.create builds your $","Telling TypeScript about a new noun","A missing noun throws","The names moved once already"],"limit":8000,"section":null,"offset":0,"count":7961,"total":25806,"next_offset":7961,"next_url":"https:\/\/changelogs.core-directive.com\/reference\/mods\/engine.json?offset=7961","markdown":"`$` is the first argument to every hook, and it's the only way your code reaches anything outside itself. There's no `fetch`, no `process` and no `require`. Reading a file, running a command, drawing on the screen and asking the model something all go through `$`. That's on purpose, because a table of functions is something an organisation can take entries out of.\n\nThe cheat sheet says it in one line worth remembering: *`$` is the plugins' capability table, not the agent's.* Taking `$.http` away from your mod doesn't stop Claude using WebFetch. It stops your mod.\n\n## Every verb is also an event\n\nThis is what holds the whole design together. `$.fs.read(path)` doesn't just call a function, it raises a dispatch called `fs.read` that folds through every plugin hooking it before core does the reading. So another mod can log your file reads, rewrite the path, cache the result or refuse. The [events page](\/reference\/mods\/events) lists every operation that works this way.\n\nYour own calls never come back to you, since a hook never sees the dispatches it raised. Everyone else's hooks do see them, the mods beneath you as well as the ones above. Anthropic's reference page says a hook on a call only intercepts calls from the mods that run after it. At 2.1.287 a mod running after another saw that mod's calls, and the run is on the [lifecycle page](\/reference\/mods\/lifecycle).\n\n## The nouns\n\nThere are 21 nouns and 80 verbs in the 2.1.286 declarations. Quoted glosses are the cheat sheet's wording.\n\n| Noun | Verbs | What it's for |\n|---|---|---|\n| `$.tool` | `call` `list` `check` `register` | \"run a tool through the hooks and permissions\", \"tools the model has now\", \"give the model a new tool\" |\n| `$.command` | `run` `list` `register` | \"run \/command as if typed\" |\n| `$.prompt` | `submit` `read` `fill` `suggest` `compose` | \"queue a prompt as this plugin\", read the prompt box, \"write the prompt box \u00b7 propose dim text into it\", build the system prompt without sending it (2.1.286) |\n| `$.agent` | `spawn` `list` `register` | \"start a subagent, resolves when it settles\", and define a new agent type |\n| `$.turn` | `abort` | Stop the turn in flight |\n| `$.session` | `id` `cwd` `root` `repo` `model` `surface` `surfaces` `turns` `messages` `usage` `version` `compact` `send` `append` `authorize` | Everything about the session you're in |\n| `$.model` | `complete` `fork` `classify` | `fork` is \"tool-less completion over this transcript, cache-shared\" |\n| `$.ui` | `log` `notice` `toast` `status` `ask` `open` `close` `panes` `scroll` `focus` `copy` `invalidate` `resolve` `blit` | The screen, with [its own page](\/reference\/mods\/ui) |\n| `$.fs` | `read` `write` `list` `stat` `exists` `ancestors` | `exists` \"never rejects\" |\n| `$.settings` | `read` | The resolved settings, by source |\n| `$.config` | `set` `list` | The whole `\/config` menu, the engine's rows and every enabled plugin's `<plugin>.<field>` rows, and a plugin reads its own fields from `options` ([more on the config page](\/reference\/mods\/config#config-is-the-config-menu)) |\n| `$.env` | `get` `set` | \"one variable by LITERAL name\" |\n| `$.store` | `get` `set` `delete` `keys` | Persistent key\/value storage that only you can see |\n| `$.state` | `get` `set` | Shared values with a version, that panes redraw from |\n| `$.http` | `fetch` | The network, and the only way to it |\n| `$.process` | `run` `spawn` | \"argv on the host, no shell\", and `spawn` streams the output |\n| `$.clock` | `now` `sleep` `after` `every` | Time, which tests can swap out |\n| `$.audio` | `play` `speak` | Sound |\n| `$.telemetry` | `log` `mark` | Analytics, and only the plugins built into Claude Code can call it (2.1.284, more below) |\n| `$.mcp` | `call` `connect` | An MCP server, directly, and connecting one your own manifest lists (2.1.285) |\n| `$.plugin` | `name` `root` | Plain data, see below |\n\n`$.env.get` only takes a literal name, and the same scan that enforces `$.noun.verb(...)` checks it. You can't read the environment by a computed key, which closes off the obvious way to exfiltrate it. `$.state` works the same way: `plugin` and `key` have to be literals in the source.\n\n## What's new since 2.1.283\n\nFive verbs and one noun arrived between 2.1.283 and 2.1.286, going by the declarations: `$.telemetry.log`, `$.telemetry.mark`, `$.mcp.connect`, `$.session.append` and `$.prompt.compose`. A handful of older calls grew new fields as well. I ran everything here at 2.1.286 from a `--plugin-dir` plugin, except where it says I didn't.\n\n- **`$.telemetry`** has `log` and `mark`, and while the built-in telemetry plugin is loaded an installed plugin can't call either of them. It gets its own section below.\n- **`$.mcp.connect(server)`** connects one of the MCP servers your own plugin's manifest lists, and it never rejects for a refusal. On a name my manifest didn't list it resolved to `{ \"isConnected\": false, \"reason\": \"unlisted\", \"message\": \"This plugin's manifest lists no MCP server named \\\"nope\\\".\" }`. The declarations list five more reasons: `unapproved`, `disabled`, `policy`, `auth` and `failed`. A success is `{ isConnected: true, server }`, and `server` is the name `$.mcp.call` takes, usually `plugin:<plugin>:<server>`. I haven't connected a real server, so that shape is only the declarations' word.\n- **`$.session.append({ message, agentId })`** puts a row into the conversation. It takes two kinds of row. `type: \"user\"` is one the model reads and the person doesn't see as typed, and mine came back stored with `role: \"user\"` and `isMeta: true`. `type: \"system\"` is a notice, which the declarations say the model never reads, and mine was stored with `name: \"informational\"`. Content is text blocks only, and a notice takes exactly one. The declarations say to leave `agentId` out for the main conversation, or give a running subagent's id.\n- **`$.model.complete(request, { signal })`** takes an `AbortSignal`. With one that was already aborted it resolved `{ isAnswered: false, reason: \"aborted\" }` in 2 ms with zero usage, and the declarations say no request is made at all. Aborting 300 ms in resolved `aborted` at 327 ms, also with zero usage reported.\n- **`asUser: true` on `$.prompt.submit`** sends your text to the model bare. Hooks still see the origin as `{ kind: \"plugin\", name, asUser: true }`, and the declarations say the transcript still names your plugin. It works in `-p`, where the submitted prompt ran as its own turn before the `-p` prompt's, and only the `-p` prompt's answer was printed. The exact wording on both sides is further down.\n- **`$.process.run` results carry `isStdoutTruncated` and `isStderrTruncated`.** Each stream keeps 4 MiB. Reading 5,000,000 bytes of `\/dev\/zero` gave me a stdout 4,194,304 long and `isStdoutTruncated: true`, and a small run said `false` for both.\n- **A `command.run` answer can carry `exitCode`.** It's what the process exits with when that command was the whole prompt of a headless run. A hook answering `{ text: \"exiting with 3\", exitCode: 3 }` made `claude -p \"\/modsaexit\"` exit 3. The declarations say it's a whole number from 0 to 255 and an interactive session ignores it.\n- **`$.fs.list` entries carry `mtimeMs`** (`$.fs.stat` already did). A file's came back as `1790803880576.4146`, fraction and all, so round it before you use it as a key. A directory's list entry was `0`, and the declarations say a link or anything else that isn't a file is `0` too. For a directory's real time, the declarations point you at `$.fs.stat`.\n- **`decorations` color ranges of the prompt box.** A `prompt.edit` answer and a `$.prompt.fill` can both carry them, each one `{ start, end }` plus any of `color`, `backgroundColor`, `dimColor`, `bold`, `italic`, `underline` and `strikethrough`. The declarations say ranges are clamped, later entries win, and a fill's decorations are gone at the draft's next change unless a `prompt.edit` hook answers new ones. I haven't drawn one yet, so that's all from the declarations.\n","previous_page":"lifecycle","next_page":"events","url":"https:\/\/changelogs.core-directive.com\/reference\/mods\/engine","index_url":"https:\/\/changelogs.core-directive.com\/reference\/mods.json"}