ext.yml Manifest Reference
ext.yml is an extension container's manifest, placed at the root of the container directory. It declares the container's metadata and, in the contributes section, lists the capabilities the container provides to the system. This is the authoritative reference for all fields.
To understand the overall model first, read the Extension System Overview. When writing a manifest, validate it with
clacky ext verify— it reports errors field by field with locations.
Top-Level Fields
id: weather-panel # keep aligned with the container directory name
name: Weather Panel # display name
description: Sidebar weather panel
version: "0.1.0" # semantic version; used as-is on publish
author: Your Name # credit, shown on the New Session card
origin: self # source nature: self / marketplace / enterprise
contributes: # contributions (see below)
...
| Field | Required | Description |
|---|---|---|
id |
No | Manifest metadata; keep it equal to the container directory name. The loader uses the directory name, not this field, as the container id and cross-layer override key (local > installed > builtin). |
name |
No (local loading) | Display name; the local loader falls back to the container id. Set it explicitly for distribution. |
title |
No | Optional short title (used by some UIs). |
description |
No | One-line summary. |
version |
Required for publishing | Optional locally; when present, use three numeric segments such as "0.1.0". Publishing requires a version greater than the latest release, without automatic increments. |
author |
No | Credit, shown on the New Session card. |
origin |
No | Source metadata: self (locally authored, default) / marketplace / enterprise. Setting it does not encrypt files or create a runtime sandbox. |
homepage |
No | Homepage / repo URL. |
license |
No | License identifier, e.g. MIT. |
license_required |
No | License-requirement metadata. The local container loader does not enforce a license check from this flag; it is not an access-control switch. |
public |
No | Container opt-in for API routes that bypass access-key authentication; also requires public_endpoint in the handler. Not marketplace visibility. See HTTP API Extensions. |
keywords |
No | Array of strings; keywords for marketplace search. |
config |
No | Extension-owned configuration mapping, exposed to API handlers as config. This is not a schema that automatically generates a settings UI. |
contributes |
Required for publishing | Contribution mapping; an empty container can be resolved locally, but publishing requires at least one contribution. |
The verifier also accepts top-level name_zh, display_name, display_name_zh, description_zh, and emoji as metadata. Acceptance is not a promise that every host UI reads each alias. For agent cards and panel tabs, use the unit's documented title/description fields. Do not infer new runtime behavior from an accepted metadata key.
The contributes Section
contributes may hold any combination of the following eight keys. Detailed usage is in each linked doc; here we only list fields.
panels — Web UI Panels
contributes:
panels:
- id: weather # panel id, unique within this container
title: Weather # tab / title text
title_zh: 天气 # Chinese title (optional)
view: panels/weather/view.js # panel script path (relative to container root)
attach: ["*"] # visibility: array of agent ids, or ["*"] for all
order: 100 # ordering when multiple panels share a slot (default 100)
| Field | Description |
|---|---|
id |
Panel id, unique within its container. An agent in the same container uses panels: [id]; an agent in another container uses panels: [container-id/panel-id]. |
title / title_zh |
Title text (EN / ZH). |
description / description_zh |
Panel description (optional). |
view |
Panel JS script path, relative to container root. |
attach |
Visibility: array of agent-id strings, or ["*"] for all. Omit to let referencing agents' panels: decide mounting. |
order |
Ordering number, default 100. |
entry_points |
Optional. Array of { unit_id, slot } recording which unit mounts into which slot. Written by tooling (e.g. Extension Studio) as metadata; the loader does not consume it. |
See Web UI Extensions.
api — HTTP Backend Endpoint
A single backend can be written inline:
contributes:
api: api/handler.rb # single handler, mounted at /api/ext/<id>/
handler.rb defines a Clacky::ApiExtension subclass declaring endpoints with a routing DSL. See HTTP API Extensions.
skills — AI Skills
contributes:
skills:
- id: triage # skill id
dir: skills/triage/ # directory holding SKILL.md (defaults to skills/<id>/)
protected: false # protection metadata; does not encrypt the skill
protected defaults to true for origin: marketplace, false otherwise. Setting it does not encrypt a plain file. The extension skill loader skips units with protected: true or a SKILL.md.enc payload; those are not loaded through the normal plaintext extension-skill path.
agents — Assistants
contributes:
agents:
- id: designer
title: Designer
title_zh: 设计师
description: A demo agent that owns the canvas panel.
description_zh: 拥有画布面板的演示 agent。
order: 100
prompt: agents/designer.md # system prompt for personality / role
avatar: agents/designer.png # avatar (optional)
panels: [canvas] # which panels to mount (referenced by id)
skills: [layout-tips] # which skills to bind (referenced by id)
tools: [hello] # which extension tools to inject (referenced by id)
disabled_skills: [triage] # which skills to disable (referenced by id, blacklist)
hidden: false # true hides it from the new-session picker
panels: references a panel by bare id within this container, or by container-id/panel-id across containers. tools: can only reference tools contributed by this container's contributes.tools.
skills: declares skill references; it is not a runtime allowlist. The verifier checks these references against contributed skill ids, but listing a skill does not grant access or exclude other skills. Runtime access depends on the skill being loaded/enabled, its own agent: scope, and the agent's disabled_skills: blacklist (matched against the skill identifier). See How to Use a Skill for skill metadata and Agent Configuration for general configuration.
hidden: true only hides an agent from the new-session picker; it does not disable the profile or prevent explicit use by id.
channels — IM Channel Adapters
contributes:
channels:
- id: slack
platform: slack # platform identifier (optional)
adapter: channels/slack.rb # adapter script path
patches — Runtime Patches
contributes:
patches:
- target: "Clacky::Tools::Terminal#execute" # method to prepend onto
file: patches/audit.rb # patch file path
fingerprint: "a1b2c3..." # optional: source fingerprint of the target
on_mismatch: disable # on fingerprint mismatch: disable / warn
Omitting fingerprint = trust the patch and require it directly; providing one lets the loader act via on_mismatch when upstream source drifts. See Runtime Patches.
hooks — Tool-Call Interception
contributes:
hooks:
- event: before_tool_use # event name (see table below)
file: hooks/audit.rb # callback script path
Valid events: before_tool_use, after_tool_use, on_tool_error, on_start, on_complete, on_iteration, session_rollback. See Declarative Shell Hooks.
tools — Extension Tools
Add a brand-new tool to the AI's model schema (a new API, a new system-level operation). tools/<id>.rb defines a Clacky::Tools::<Camelized id> subclass — the file name IS the class-name mapping (tools/hello.rb → Clacky::Tools::Hello), so no extra registration code is needed:
contributes:
tools:
- id: hello
file: tools/hello.rb # defines Clacky::Tools::Hello < Clacky::Tools::Base
| Field | Description |
|---|---|
id |
Tool id; agents reference it via tools: [id]. |
file |
Ruby file path, relative to container root. The file name maps to the class name: tools/<id>.rb → Clacky::Tools::<Camelized id>. |
A tool is injected only into agents that declare it — an agent mounts the tool into its schema only when its agents: entry lists tools: [hello]. A broken tool file is logged and skipped at startup; it never blocks the agent.
A minimal class for tools/weather.rb (replace the demonstration result with the real operation):
module Clacky
module Tools
class Weather < Base
self.tool_name = "weather"
self.tool_description = "Get current weather for a city."
self.tool_category = "general"
self.tool_parameters = {
type: "object",
properties: { city: { type: "string", description: "City name" } },
required: %w[city]
}
def execute(city:, **)
{ temperature: 22, city: city }
end
end
end
end
execute is an instance method accepting keyword arguments; allow host-supplied keywords such as working_dir. Runtime registration loads tools/<id>.rb, so keep file, id, and class mapping aligned rather than relying on an arbitrary file alias. An opted-in extension tool with the same tool name replaces the previously registered tool, including a built-in: choose a unique name unless overriding is deliberate. Prefer a skill when existing tools already compose the capability.
A tool instance receives the host agent at registration (via the agent accessor), giving it access to the Ruby-layer public API — this is what sets a tool apart from a Skill:
| Method | Purpose |
|---|---|
agent.skill_loader |
Inspect installed skills |
agent.fork_subagent(model:, forbidden_tools:, system_prompt_suffix:) |
Fork a subagent inheriting the parent conversation |
agent.fan_out_labeled(jobs, max_concurrency:, timeout:) |
Run a batch of [{ label:, run: }] in parallel, UI handled for you |
agent.final_reply(subagent) |
Get a subagent's final reply text |
agent.absorb_subagent_cost(result, notify_ui: false) |
Merge a subagent's spend into the parent (thread-safe) |
fan_out_labeled returns an array strictly aligned to the input order, each entry carrying ok? / value / error / duration. One failing job never drags down its siblings, so check ok? per entry when aggregating. The Web UI folds each job into its own card; the CLI collapses them into a single shared progress line.
Three hard rules when orchestrating parallel subagents: ① fork_subagent must complete serially on the calling thread (it deep-copies the parent config and history, so concurrent forks race) — only the blocking run belongs in the run: lambda; ② add the parallel tool itself to the subagents' forbidden_tools, or it will recursively spawn threads; ③ never call execute_skill_with_subagent from a thread — that's the serial path, which writes the parent's history and opens a UI phase, and is not thread-safe. Don't spawn your own Thread.new for subagents either: you'd lose the UI phase attribution and the task epoch (the marker used to discard events after a task is interrupted).
Full Example
A "Slack suite" contributing five capabilities from one container:
id: slack-suite
name: Slack Suite
description: Slack integration + support agent + inbox panel
version: "0.1.0"
author: Your Name
origin: self
contributes:
channels:
- id: slack
adapter: channels/slack.rb
agents:
- id: support
title: Support
prompt: agents/support.md
panels: [inbox]
skills: [triage]
panels:
- id: inbox
view: panels/inbox/view.js
attach: [support] # only appears in the support agent's sessions
skills:
- id: triage
dir: skills/triage/
api: api/handler.rb
Validate with clacky ext verify: it checks known manifest keys, supported structural constraints and reference integrity (for example, whether referenced panels and view/prompt files exist), reporting locatable issues. It is not a complete schema/type checker or a runtime test; it does not execute contribution scripts. See Extension System Overview for the validation boundary.