一键导入
write-zot-extension
Help the user create a new zot extension (slash command, LLM tool, or guard) in any language.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Help the user create a new zot extension (slash command, LLM tool, or guard) in any language.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Help the user create, install, or package zot themes, including theme-only extensions.
Run a thorough self-review pass on the most recent change.
Diagnose and minimally fix a failing test, then re-run.
| name | write-zot-extension |
| description | Help the user create a new zot extension (slash command, LLM tool, or guard) in any language. |
Use this skill when the user asks for help building a zot extension — a new slash command, a new tool the LLM can call, an audit hook, or a permission gate. Skim this whole skill first, then collaborate with the user on the specific extension they want.
A zot extension is an external executable that zot launches as a subprocess and talks to over its stdin/stdout in newline-delimited JSON. It can be written in any language that can read/write JSON lines from stdio: Go, TypeScript (via tsx), Python, Rust, shell with jq, anything. Crash isolation is automatic; one bad extension never takes down zot.
Three things an extension can do (any combination):
Slash commands — register /foo so the user can run it from
the input. The handler returns a "prompt" (submitted to the
agent), an "insert" (text dropped into the editor), a "display"
(one-shot styled note in the chat), or a "noop".
Tools — register tools the LLM itself calls. Schema is
JSON Schema; zot routes the model's tool_call to the
extension's tool_result. Same lifecycle as built-in tools
(read/write/edit/bash/skill).
Lifecycle hooks — subscribe to events (session_start, turn_start, tool_call, turn_end, assistant_message) for telemetry / audit / custom UI, or intercept tool calls before execution to refuse dangerous patterns.
Each extension lives in its own directory:
~/Library/Application Support/zot/extensions/<name>/
├── extension.json # manifest (required)
└── <executable> # whatever exec points at
Or project-local: <project>/.zot/extensions/<name>/. Project-local
wins on name conflict.
For ad-hoc use during development, skip the install step entirely
and run zot --ext PATH (repeatable: -e PATH -e PATH).
{
"name": "weather",
"version": "1.0.0",
"exec": "./weather",
"args": [],
"language": "go",
"description": "current weather lookups for any city",
"enabled": true
}
Field rules:
name (required, unique) — id zot uses internally; matches the
hello frame. Slash commands & tools live in the same name space
as built-ins; conflicts are silently shadowed by built-ins.exec (required) — the executable path. Resolution:
./ or ../: relative to the manifest's directory$PATH (e.g. node,
python3, npx, tsx)args — extra argv passed to exec (e.g. ["index.js"])language — informational only ("go", "typescript",
"python", etc.)enabled — defaults to true; set false to keep installed but skipNewline-delimited JSON in both directions. Top-level type is the
discriminator. Optional id correlates command/tool requests with
their responses.
The very first frame the extension sends is hello:
{"type":"hello","name":"weather","version":"1.0.0",
"capabilities":["commands","tools"]}
Capabilities are advisory; current values are commands, tools,
events. Send all that apply.
zot replies with hello_ack:
{"type":"hello_ack","protocol_version":1,"zot_version":"0.0.x",
"provider":"anthropic","model":"claude-opus-4-7","cwd":"/path/to/project"}
The canonical startup order is hello, wait for hello_ack, send
registration frames in any order, then send a single ready sentinel
so zot can finalize the agent's tool registry:
{"type":"register_command","name":"weather","description":"current weather"}
{"type":"register_tool","name":"weather","description":"Get current weather for a city.",
"schema":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}
{"type":"subscribe","events":["tool_call"],"intercept":["tool_call"]}
{"type":"ready"}
If you don't send ready, zot's idle watchdog auto-treats you as
ready after 250ms of no frames, but always send it explicitly when
you can. Newer extensions on faster hosts shave that 250ms off.
Use hello_ack.cwd for the user's project directory. The extension
process itself runs from the extension directory, so do not use
os.Getwd() or process.cwd() when you need the project path.
zot → extension:
{"type":"command_invoked","id":"abc","name":"weather","args":"berlin"}
{"type":"tool_call","id":"def","name":"weather","args":{"city":"Berlin"}}
{"type":"event","event":"turn_start","step":1}
{"type":"event_intercept","id":"ghi","event":"tool_call",
"tool_name":"bash","tool_args":{"command":"rm -rf /tmp/foo"}}
{"type":"shutdown"}
extension → zot (replies + spontaneous notifications):
{"type":"command_response","id":"abc","action":"prompt",
"prompt":"Show today's weather for Berlin in one line."}
{"type":"tool_result","id":"def","content":[{"type":"text","text":"Berlin: 16°C, fog"}]}
{"type":"event_intercept_response","id":"ghi","block":true,
"reason":"refused: command matches the danger pattern \"rm -rf\""}
{"type":"notify","level":"info","message":"refreshed cache"}
{"type":"clear_notes"}
{"type":"shutdown_ack"}
notify notes are one-shot: they clear when the user sends their next
prompt (and on esc / /clear). Send clear_notes to retract every
note this extension pushed earlier (e.g. a transient approval prompt)
without waiting for the next turn; other extensions' notes are kept.
command_response.action values:
"prompt" — submit prompt as a fresh user message"insert" — drop insert into the editor at the cursor"display" — append display to chat as a one-shot note (no
model call, not in transcript)"noop" — handled internally; zot doesn't change the UItool_result.content[] blocks: {"type":"text","text":"..."} or
{"type":"image","mime_type":"image/png","data":"<base64>"}.
Per-tool timeout: 60s. Per-intercept timeout: 5s. Missing the intercept timeout is treated as "allow" so an unresponsive guard never stalls the agent.
hello; do not send notify, logs, or registration
frames before that handshake starts. Use stderr for logs / debug
output (zot captures stderr to $ZOT_HOME/logs/ext-<name>.log).\n.package main
import (
"encoding/json"
"github.com/patriceckhart/zot/packages/agent/ext"
)
func main() {
e := ext.New("weather", "1.0.0")
e.Command("weather", "current weather for a city",
func(args string) ext.Response {
return ext.Prompt("Tell me the weather for " + args)
})
e.Tool("weather", "Get current weather for a city.",
json.RawMessage(`{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}`),
func(args json.RawMessage) ext.ToolResult {
var in struct{ City string `json:"city"` }
if err := json.Unmarshal(args, &in); err != nil {
return ext.TextErrorResult("invalid args")
}
return ext.TextResult(in.City + ": sunny, 21°C (fake)")
})
// Optional: register project-specific commands after hello_ack.
e.OnHello(func(host ext.HostInfo) {
if host.CWD != "" {
e.Command("cwd", "show the current project directory", func(args string) ext.Response {
return ext.Display(host.CWD)
})
}
})
if err := e.Run(); err != nil {
e.Logf("fatal: %v", err)
}
}
Build: go build -o weather .
OnHello is optional. Use it when configuration or registrations need
host metadata such as HostInfo.CWD, Provider, Model, ZotVersion,
ExtensionDir, or DataDir. The SDK sends hello, waits for
hello_ack, runs OnHello, announces registrations, then sends ready.
extension.json:
{"name":"weather","version":"1.0.0","exec":"./weather","language":"go","enabled":true}
Run via tsx, which executes .ts files without a build step.
{"name":"scratchpad","version":"1.0.0","exec":"tsx","args":["index.ts"],"language":"typescript","enabled":true}
// index.ts (excerpt; see examples/extensions/scratchpad/index.ts for the full version)
import { createInterface } from "node:readline";
import { stderr, stdin, stdout } from "node:process";
function send(o: object) { stdout.write(JSON.stringify(o) + "\n"); }
function log(s: string) { stderr.write(`[scratchpad] ${s}\n`); }
send({ type: "hello", name: "scratchpad", version: "1.0.0",
capabilities: ["commands", "tools"] });
const rl = createInterface({ input: stdin, crlfDelay: Infinity });
rl.on("line", (line) => {
const f = JSON.parse(line);
if (f.type === "hello_ack") {
// f.cwd is the user's project directory.
send({ type: "register_command", name: "note", description: "append a note" });
send({ type: "register_tool", name: "read_notes",
description: "Read the user's scratchpad notes.",
schema: { type: "object", properties: {} } });
send({ type: "ready" });
} else if (f.type === "command_invoked" && f.name === "note") {
send({ type: "command_response", id: f.id, action: "display",
display: `noted: ${f.args}` });
} else if (f.type === "tool_call" && f.name === "read_notes") {
send({ type: "tool_result", id: f.id,
content: [{ type: "text", text: "(notes go here)" }] });
} else if (f.type === "shutdown") {
send({ type: "shutdown_ack" });
rl.close();
}
});
tsx install: npm install -g tsx. Without global tsx, fall back
to "exec":"npx","args":["--yes","tsx","index.ts"] (slower
startup; npx checks the registry every launch).
{"name":"hello-py","version":"1.0.0","exec":"./hello.py","language":"python","enabled":true}
#!/usr/bin/env python3
import json, sys
def emit(o): sys.stdout.write(json.dumps(o) + "\n"); sys.stdout.flush()
emit({"type": "hello", "name": "hello-py", "version": "1.0.0", "capabilities": ["commands"]})
for line in sys.stdin:
msg = json.loads(line)
if msg["type"] == "hello_ack":
# msg["cwd"] is the user's project directory.
emit({"type": "register_command", "name": "hellopy", "description": "say hi (python)"})
emit({"type": "ready"})
elif msg["type"] == "command_invoked":
emit({"type": "command_response", "id": msg["id"],
"action": "prompt", "prompt": "Say hi briefly."})
elif msg["type"] == "shutdown":
emit({"type": "shutdown_ack"})
break
chmod +x hello.py.
zot ext install ./weather # copy into $ZOT_HOME/extensions/
zot --ext ./weather # run from disk for one zot session (no install)
zot --ext . # cwd is the extension dir
zot ext list # show installed extensions
zot ext logs weather # cat the extension's stderr
zot ext logs weather -f # tail it
zot ext disable weather # keep installed but skip on launch
zot ext enable weather
zot ext remove weather
For TS / Python extensions, no build step is needed — edit the source in place and relaunch zot.
For Go, run go build -o <name> . in the extension directory after
edits, then zot ext install (which copies the manifest + binary)
or zot --ext . to test from the working tree.
The extension is just a process. Drive it directly with shell pipes to see exactly what's happening on the wire:
{
printf '%s\n' '{"type":"hello_ack","protocol_version":1,"zot_version":"x","provider":"a","model":"o","cwd":"/tmp"}'
sleep 0.2
printf '%s\n' '{"type":"command_invoked","id":"1","name":"weather","args":"Berlin"}'
sleep 0.5
printf '%s\n' '{"type":"shutdown"}'
} | ./weather
Compare what comes out of stdout to the expected wire format. If a
frame doesn't match what zot expects, it's discarded silently and
logged to ext-<name>.log.
command onlytoolevent + interceptzot --ext <path> for testing without committing to an
install.zot ext install <path>.Don't try to write a full SDK or framework on top of the protocol unless the user asked for one — the wire format is small enough that a 30-line raw script is the right answer for most extensions.