Extensions
Read this when you want to add tools, slash commands, system-prompt sections, event gates, or tool colors to Ink. Skills (markdown packages) → skills.md; MCP (external servers) → mcp.md.
What an extension is
A plain ESM module (.mjs, or .js under a module-typed package) whose
default export is a factory receiving one thin api object. Ink loads them
from three places, in this order:
-e <path>/--extension <path>flags (repeatable; file or directory; loads regardless of the trust gate)<project>/.ink/extensions/— behind the trust gate~/.ink/extensions/
--no-extensions skips both discovery directories (explicit -e paths
still load).
// .ink/extensions/hello.mjs — an extension is a plain ESM module.
/** @param {import("../../src/extensions/types.js").ExtensionApi} api */
export default function (api) {
api.registerTool({ /* …an Ink Tool — name, description, parameters, execute… */ });
api.registerCommand({ /* …a /slash command, listed in /help… */ });
api.registerContext("hello", "…a system-prompt section, appended after AGENTS.md…");
api.on("tool_call", (event) => {
// may veto: return { block: true, reason: "…what to do instead…" }
});
}
Three read-only facts ride along: cwd (absolute working directory Ink
started in), version (Ink version string), and origin
("cli" | "project" | "global" — where the extension was discovered).
The api surface
registerTool(tool)— add an LLM-callable tool (same shape as the built-in tools: name, description, typeboxparameters,execute).registerCommand(command)— add a REPL slash command (tagged[source]in/help).registerContext(id, text)— append a static section to the system prompt, after AGENTS.md context.on(event, handler)— subscribe totool_call,tool_end,message_end,run_start,run_end.setStatus(key, text)— one line in the TUI footer (undefinedclears; keys namespaced per extension; the host owns styling). Works from handlers, timers, and command callbacks; safe no-op in print mode. If you create timers,unref()them — a leaked referenced timer blocks process exit.registerToolColor(names, color)— color tool names in the TUI call header.names: one name, several, or"*"(fallback).color: a standard-16 token (black…bright*),"none"(bold-only), or an absolute token —#rrggbboransi256:N(the portable choice). Exact names beat"*"; the first registration of a name wins within the tier. No colors ship by default.suggestToolColor(names, color)— the same API one tier weaker: the tool's own extension suggests its default look; any user registration outranks any suggestion (user exact > user"*"> suggested exact > suggested"*"). Use it in tool-providing extensions;registerToolColorbelongs in themes.confirm(message, detail?, options?)— ask the human a yes/no question. Resolves exactlytrueon explicit approval;falsecovers declines, empty answers, and hosts without an interactive prompt;"timeout"whenoptions.timeoutMsexpires unanswered (compare with=== true, never a truthy check).options.sessionKeyenables "don't ask again this session" memory;options.warnSpanshighlights character ranges in the detail;options.previewrenders a command-style request.
Events
tool_call— after argument validation, before execution. Handlers may return{ block: true, reason: "…" }; the block decision becomes the tool result the model sees (reason included), so the run adapts instead of dying. Subagent calls pass through the same gate withsubagent: trueplus theagentprofile name — a gate can hold children to stricter rules.tool_end/message_end— observers: results and finalized assistant messages.run_start— fires once when a top-level run begins.run_enddoes NOT fire if the run crashes (provider throw) — the crashed run firesrun_errorinstead: treatrun_error(or the nextrun_start) as the close of an open run. Subagent runs emit neither.run_error— fires exactly once when a top-level run crashes (provider throw, or a failed overflow-recovery/compaction): the pairedrun_endnever arrives, and the payload is the crashmessage(first line, capped). Mutually exclusive withrun_end; aborted runs getrun_end, not this. Subagent crashes surface as tool results and never fire it.
Failure behavior
A bad extension never kills Ink: load failures, registration conflicts, and
handler throws each become one ink: teaching line. A throwing tool_call
handler fails safe — the call is blocked.
Security
Extensions are code and run with your full permissions — the same posture as
the agent itself. Check .ink/extensions/ in repositories you didn't write,
or run with --no-extensions.
Bundled examples
examples/extensions/ ships case studies:
notes.mjs— the API tourguardian.mjs— a config-driven permission gate: wildcard/regex rules that deny or ask before a call runs, file-scopedwrite/editrules; config at~/.ink/guardian.json, audited to~/.ink/guardian.lognotify.mjs— a macOS completion notification with soundtask-timer.mjs— a live per-run timer in the TUI footer (run_start/run_end+setStatus)tool-colors.mjs— a name-color theme for the call header (built-ins Claude-orange,taskbright cyan)web-search/— a bundled multi-file search tool
Copy one into ~/.ink/extensions/ and edit to taste.