zot extensions
zot can be extended with custom slash commands by running an external
program as a subprocess and exchanging newline-delimited JSON over
its stdin/stdout. Extensions can be written in any language that
can read and write JSON lines from stdio — Go, TypeScript, Python,
Rust, shell with jq, anything.
Three phases shipped so far:
- Phase 1: slash commands + chat notifications.
- Phase 2: tools the LLM can call.
- Phase 3: lifecycle event subscriptions + tool-call interception
for guardrail extensions.
Quick start
The simplest extension is a script that prints a hello frame, reads
commands, and prints responses. Here's the whole thing in Python,
no SDK required:
#!/usr/bin/env python3
# $ZOT_HOME/extensions/hello-py/hello.py
import json, sys, threading
def emit(obj):
sys.stdout.write(json.dumps(obj) + "\n")
sys.stdout.flush()
emit({"type":"hello","name":"hello-py","version":"1.0.0","capabilities":["commands"]})
emit({"type":"register_command","name":"hellopy","description":"say hi (python)"})
for line in sys.stdin:
msg = json.loads(line)
if msg["type"] == "command_invoked":
emit({"type":"command_response","id":msg["id"],"action":"prompt",
"prompt": "Greet me very briefly. Add one emoji."})
elif msg["type"] == "shutdown":
emit({"type":"shutdown_ack"})
break
Drop it in a directory with this extension.json:
{
"name": "hello-py",
"version": "1.0.0",
"exec": "./hello.py",
"language": "python",
"enabled": true
}
chmod +x hello.py, install:
zot ext install ./hello-py
Restart zot, type /hellopy, the agent greets you. Done.
Built-in extensions
zot ships with no extensions installed by default. A fresh zot install (or go install) gives you a clean agent. Extensions are entirely opt-in: you install (or --ext for one run) only the ones you want.
The examples/extensions/ directory in the repo is reference code, not a default install set. To use any of those:
# go-based examples need a build first
cd path/to/zot/examples/extensions/hello && go build -o hello .
# install (copies to $ZOT_HOME/extensions/hello/)
zot ext install path/to/zot/examples/extensions/hello
# or load straight from the repo for one zot session
zot --ext path/to/zot/examples/extensions/hello
Nothing is auto-installed and nothing reaches out to the network without your explicit action.
Layout & discovery
zot scans two directories on startup, in this order:
- Project-local:
./.zot/extensions/<name>/extension.json
- Global:
$ZOT_HOME/extensions/<name>/extension.json
A project-local extension with the same name wins over a global one.
On macOS $ZOT_HOME defaults to ~/Library/Application Support/zot/;
on Linux it's $XDG_STATE_HOME/zot or ~/.local/state/zot.
Each extension owns its own subdirectory. The extension.json
manifest tells zot how to launch it:
{
"name": "weather",
"version": "1.0.0",
"exec": "./weather",
"args": ["--mode", "daemon"],
"language": "go",
"description": "current weather for any city",
"enabled": true
}
| field |
meaning |
name |
required. how zot identifies the extension; must match what's sent in the hello frame. |
version |
optional. shown in zot ext list. |
exec |
required. path to the executable (relative to the manifest). |
args |
optional. extra argv passed to exec. |
language |
optional. informational only (go, python, typescript, ...). |
description |
optional. shown in zot ext list. |
enabled |
optional, defaults to true. set to false to disable without removing. |
Lifecycle
- Discovery: zot reads every
extension.json in the search dirs.
- Spawn: enabled extensions are launched as subprocesses. stderr
redirects to
$ZOT_HOME/logs/ext-<name>.log (one file per
extension, append-mode).
- Hello handshake: the extension sends a
hello frame; zot
replies with hello_ack containing the protocol version and the
active provider/model/cwd.
- Registration: the extension sends
register_command frames.
First-come-first-served: a name already taken by a built-in or by
a previously-loaded extension is silently shadowed (logged in the
extension's own log file).
- Runtime: zot dispatches
command_invoked frames when the
user runs a registered command; the extension responds with
command_response. Extensions can also push notify frames at
any time.
- Shutdown: when zot exits, it sends
shutdown and waits up to
2s for the extension to send shutdown_ack. Holdouts are
SIGTERM'd, then SIGKILL'd.
A crashing extension does not bring down zot. The slash command it
owned simply stops working until the extension is fixed and zot is
restarted.
All frames are one JSON object per line. Top-level type is the
discriminator. Optional id correlates request frames with their
responses.
Extension → host
hello (required, first frame)
{"type":"hello","name":"weather","version":"1.0.0",
"capabilities":["commands","tools"]}
register_command
{"type":"register_command","name":"weather",
"description":"current weather for a city"}
Registers a tool the LLM can call. schema is a JSON Schema object
describing the tool's args (the same shape Anthropic and OpenAI accept).
{"type":"register_tool","name":"weather",
"description":"Get the current weather for a city.",
"schema":{
"type":"object",
"properties":{"city":{"type":"string"}},
"required":["city"]
}}
Tool names live in the same namespace as built-in tools (read,
write, edit, bash, skill). Conflicts are silently shadowed by
the built-in.
ready
Sentinel telling zot "all initial registrations are flushed". Send it
right after your last register_* frame so the host can build the
agent's tool registry without racing the registration window.
{"type":"ready"}
Reply to a tool_call from the host. content[] is a list of
message blocks; each block is {"type":"text","text":"..."} or
{"type":"image","mime_type":"image/png","data":"<base64>"}. Set
is_error: true to mark the call as failed.
{"type":"tool_result","id":"...",
"content":[{"type":"text","text":"Berlin: 16°C, fog"}]}
subscribe
Declares which lifecycle events the extension wants to observe and
which it wants to intercept. Send once after hello, before ready.
{"type":"subscribe",
"events":["session_start","turn_start","tool_call","turn_end","assistant_message"],
"intercept":["tool_call"]}
Recognised event names: session_start, turn_start, turn_end,
tool_call, assistant_message. Only tool_call is interceptable
in this version; other names listed under intercept are ignored.
event_intercept_response
Reply to an event_intercept from the host. block: true refuses
the action; reason is shown to the model as the tool error text.
Missing the response within 5s is treated as "allow" (i.e. an
unresponsive extension never stalls the agent).
{"type":"event_intercept_response","id":"...",
"block":true,"reason":"refused: matches danger pattern \"rm -rf\""}
command_response (reply to command_invoked)
{"type":"command_response","id":"...","action":"prompt",
"prompt":"Show today's weather for Berlin in one line."}
action is one of:
"prompt" — submits prompt as a fresh user message; the agent
runs a turn against it.
"insert" — inserts insert into the editor at the cursor without
submitting.
"display" — appends display to the chat as a one-shot styled
note. No model call, nothing written to the transcript.
"noop" — the extension handled it itself (e.g. it pushed
notify frames or kicked off background work). zot doesn't change
the UI in response.
If error is non-empty, zot renders it as a red status line
regardless of action.
notify (one-way, any time)
{"type":"notify","level":"info",
"message":"refreshed cache (12 entries)"}
level is one of info, success, warn, error. The note shows
up below the transcript with the extension's name in brackets.
shutdown_ack
Sent in response to shutdown. Extension should exit promptly after.
Host → extension
hello_ack
{"type":"hello_ack","protocol_version":1,
"zot_version":"0.0.7","provider":"anthropic",
"model":"claude-opus-4-7","cwd":"/Users/pat/Developer/zot"}
Sent immediately after hello. The extension can use these fields to
decide which commands to register (e.g. only register a Python tool
on macOS, only register a model-specific shortcut for opus, etc.).
command_invoked
{"type":"command_invoked","id":"...",
"name":"weather","args":"berlin"}
args is everything the user typed after the command name, trimmed.
Sent when the LLM invokes a tool the extension registered. args is
the parsed JSON object the model produced; the extension is
responsible for validating/coercing it.
{"type":"tool_call","id":"...","name":"weather",
"args":{"city":"Berlin"}}
Reply with tool_result within the host's tool timeout (default 60s).
Missing the timeout surfaces an error to the model and the call is
marked as failed.
event
Lifecycle notification for events the extension subscribed to via
subscribe. One-way — no response expected.
{"type":"event","event":"turn_start","step":1}
{"type":"event","event":"tool_call",
"tool_id":"...","tool_name":"read","tool_args":{"path":"foo.go"}}
{"type":"event","event":"turn_end","stop":"end_turn"}
event_intercept
Sent when zot wants to give the extension a chance to block a
lifecycle event before it happens. Same payload shape as event.
Reply with event_intercept_response within 5s; missing the deadline
is treated as "allow".
Only event: "tool_call" is sent in this version.
{"type":"event_intercept","id":"...","event":"tool_call",
"tool_id":"...","tool_name":"bash",
"tool_args":{"command":"rm -rf /tmp/foo"}}
shutdown
Sent during graceful zot exit (or /reload-ext once that lands).
Reply with shutdown_ack and then exit.
Managing extensions from the CLI
zot ext list list installed extensions and their state
zot ext install <path|git-url> copy / clone into $ZOT_HOME/extensions/
zot ext remove <name> delete an extension directory
zot ext enable <name> re-enable a disabled extension
zot ext disable <name> disable without removing
zot ext logs <name> [-f] cat / tail the extension's stderr
zot ext install <path> does a recursive copy; <git-url> does a
shallow clone. Both validate that the destination contains an
extension.json and roll back if not.
Loading an extension for one run
For iteration on a working copy, skip the install + reload cycle
and load straight from disk for one zot session:
zot --ext ./my-extension # short form: -e ./my-extension
zot --ext ./a -e ./b # repeatable
--ext paths take precedence over installed extensions of the same
name, so you can shadow an installed copy with a work-in-progress
version without uninstalling first. Nothing is copied or persisted;
the extension dies with zot like any other subprocess.
SDKs
Writing the wire protocol by hand is fine for one-off scripts, but
for anything bigger the SDKs handle the boilerplate.
Go — pkg/zotext
package main
import (
"encoding/json"
"github.com/patriceckhart/zot/pkg/zotext"
)
func main() {
ext := zotext.New("hello", "1.0.0")
// Slash command
ext.Command("hello", "say hi", func(args string) zotext.Response {
return zotext.Prompt("Greet me in one short sentence.")
})
// LLM-callable tool
ext.Tool("weather", "Current weather for a city.",
json.RawMessage(`{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}`),
func(args json.RawMessage) zotext.ToolResult {
var in struct{ City string `json:"city"` }
json.Unmarshal(args, &in)
return zotext.TextResult(in.City + ": sunny")
})
ext.Run()
}
Build with go build -o hello ., drop the binary + an extension.json
into $ZOT_HOME/extensions/hello/.
See:
examples/extensions/hello/ — slash commands
examples/extensions/clock/ — slash commands in plain Node, no SDK
examples/extensions/weather/ — LLM-callable tool
examples/extensions/guard/ — event subscriptions + tool-call
interception (refuses dangerous bash patterns)
TypeScript / Python
These SDKs aren't in the main repo yet; the wire format is small
enough that a ~30 line raw script gets you started in either
language. See the Quick start Python example for the
shape. SDK packages will land in follow-up commits.
Security
Extensions run with the user's full filesystem and network
permissions. Treat installing an extension the same as installing
any other binary on your machine.
zot ext install <git-url> clones from any URL you give it. There's
no sandbox in v1; if you need isolation, install only extensions you
trust or run zot under your platform's sandboxing tool (bwrap /
sandbox-exec / AppContainer).
Roadmap
Phase 1 (shipped):
Phase 2 (shipped):
Phase 3 (shipped):
Future (no firm timeline):
source
1# zot extensions
2
3zot can be extended with custom slash commands by running an external
4program as a subprocess and exchanging newline-delimited JSON over
5its stdin/stdout. Extensions can be written in **any language** that
6can read and write JSON lines from stdio — Go, TypeScript, Python,
7Rust, shell with `jq`, anything.
8
9Three phases shipped so far:
10
11- **Phase 1**: slash commands + chat notifications.
12- **Phase 2**: tools the LLM can call.
13- **Phase 3**: lifecycle event subscriptions + tool-call interception
14 for guardrail extensions.
15
16## Quick start
17
18The simplest extension is a script that prints a hello frame, reads
19commands, and prints responses. Here's the whole thing in **Python**,
20no SDK required:
21
22```python
23#!/usr/bin/env python3
24# $ZOT_HOME/extensions/hello-py/hello.py
25import json, sys, threading
26
27def emit(obj):
28 sys.stdout.write(json.dumps(obj) + "\n")
29 sys.stdout.flush()
30
31emit({"type":"hello","name":"hello-py","version":"1.0.0","capabilities":["commands"]})
32emit({"type":"register_command","name":"hellopy","description":"say hi (python)"})
33
34for line in sys.stdin:
35 msg = json.loads(line)
36 if msg["type"] == "command_invoked":
37 emit({"type":"command_response","id":msg["id"],"action":"prompt",
38 "prompt": "Greet me very briefly. Add one emoji."})
39 elif msg["type"] == "shutdown":
40 emit({"type":"shutdown_ack"})
41 break
42```
43
44Drop it in a directory with this `extension.json`:
45
46```json
47{
48 "name": "hello-py",
49 "version": "1.0.0",
50 "exec": "./hello.py",
51 "language": "python",
52 "enabled": true
53}
54```
55
56`chmod +x hello.py`, install:
57
58```bash
59zot ext install ./hello-py
60```
61
62Restart `zot`, type `/hellopy`, the agent greets you. Done.
63
64## Built-in extensions
65
66**zot ships with no extensions installed by default.** A fresh `zot install` (or `go install`) gives you a clean agent. Extensions are entirely opt-in: you install (or `--ext` for one run) only the ones you want.
67
68The `examples/extensions/` directory in the repo is reference code, not a default install set. To use any of those:
69
70```bash
71# go-based examples need a build first
72cd path/to/zot/examples/extensions/hello && go build -o hello .
73
74# install (copies to $ZOT_HOME/extensions/hello/)
75zot ext install path/to/zot/examples/extensions/hello
76
77# or load straight from the repo for one zot session
78zot --ext path/to/zot/examples/extensions/hello
79```
80
81Nothing is auto-installed and nothing reaches out to the network without your explicit action.
82
83## Layout & discovery
84
85zot scans two directories on startup, in this order:
86
871. **Project-local**: `./.zot/extensions/<name>/extension.json`
882. **Global**: `$ZOT_HOME/extensions/<name>/extension.json`
89
90A project-local extension with the same name wins over a global one.
91On macOS `$ZOT_HOME` defaults to `~/Library/Application Support/zot/`;
92on Linux it's `$XDG_STATE_HOME/zot` or `~/.local/state/zot`.
93
94Each extension owns its own subdirectory. The `extension.json`
95manifest tells zot how to launch it:
96
97```json
98{
99 "name": "weather",
100 "version": "1.0.0",
101 "exec": "./weather",
102 "args": ["--mode", "daemon"],
103 "language": "go",
104 "description": "current weather for any city",
105 "enabled": true
106}
107```
108
109| field | meaning |
110|---|---|
111| `name` | required. how zot identifies the extension; must match what's sent in the `hello` frame. |
112| `version` | optional. shown in `zot ext list`. |
113| `exec` | required. path to the executable (relative to the manifest). |
114| `args` | optional. extra argv passed to `exec`. |
115| `language` | optional. informational only (`go`, `python`, `typescript`, ...). |
116| `description` | optional. shown in `zot ext list`. |
117| `enabled` | optional, defaults to `true`. set to `false` to disable without removing. |
118
119## Lifecycle
120
1211. **Discovery**: zot reads every `extension.json` in the search dirs.
1222. **Spawn**: enabled extensions are launched as subprocesses. stderr
123 redirects to `$ZOT_HOME/logs/ext-<name>.log` (one file per
124 extension, append-mode).
1253. **Hello handshake**: the extension sends a `hello` frame; zot
126 replies with `hello_ack` containing the protocol version and the
127 active provider/model/cwd.
1284. **Registration**: the extension sends `register_command` frames.
129 First-come-first-served: a name already taken by a built-in or by
130 a previously-loaded extension is silently shadowed (logged in the
131 extension's own log file).
1325. **Runtime**: zot dispatches `command_invoked` frames when the
133 user runs a registered command; the extension responds with
134 `command_response`. Extensions can also push `notify` frames at
135 any time.
1366. **Shutdown**: when zot exits, it sends `shutdown` and waits up to
137 2s for the extension to send `shutdown_ack`. Holdouts are
138 SIGTERM'd, then SIGKILL'd.
139
140A crashing extension does not bring down zot. The slash command it
141owned simply stops working until the extension is fixed and zot is
142restarted.
143
144## Wire format
145
146All frames are one JSON object per line. Top-level `type` is the
147discriminator. Optional `id` correlates request frames with their
148responses.
149
150### Extension → host
151
152#### `hello` (required, first frame)
153
154```json
155{"type":"hello","name":"weather","version":"1.0.0",
156 "capabilities":["commands","tools"]}
157```
158
159#### `register_command`
160
161```json
162{"type":"register_command","name":"weather",
163 "description":"current weather for a city"}
164```
165
166#### `register_tool`
167
168Registers a tool the LLM can call. `schema` is a JSON Schema object
169describing the tool's args (the same shape Anthropic and OpenAI accept).
170
171```json
172{"type":"register_tool","name":"weather",
173 "description":"Get the current weather for a city.",
174 "schema":{
175 "type":"object",
176 "properties":{"city":{"type":"string"}},
177 "required":["city"]
178 }}
179```
180
181Tool names live in the same namespace as built-in tools (`read`,
182`write`, `edit`, `bash`, `skill`). Conflicts are silently shadowed by
183the built-in.
184
185#### `ready`
186
187Sentinel telling zot "all initial registrations are flushed". Send it
188right after your last `register_*` frame so the host can build the
189agent's tool registry without racing the registration window.
190
191```json
192{"type":"ready"}
193```
194
195#### `tool_result`
196
197Reply to a `tool_call` from the host. `content[]` is a list of
198message blocks; each block is `{"type":"text","text":"..."}` or
199`{"type":"image","mime_type":"image/png","data":"<base64>"}`. Set
200`is_error: true` to mark the call as failed.
201
202```json
203{"type":"tool_result","id":"...",
204 "content":[{"type":"text","text":"Berlin: 16°C, fog"}]}
205```
206
207#### `subscribe`
208
209Declares which lifecycle events the extension wants to observe and
210which it wants to intercept. Send once after `hello`, before `ready`.
211
212```json
213{"type":"subscribe",
214 "events":["session_start","turn_start","tool_call","turn_end","assistant_message"],
215 "intercept":["tool_call"]}
216```
217
218Recognised event names: `session_start`, `turn_start`, `turn_end`,
219`tool_call`, `assistant_message`. Only `tool_call` is interceptable
220in this version; other names listed under `intercept` are ignored.
221
222#### `event_intercept_response`
223
224Reply to an `event_intercept` from the host. `block: true` refuses
225the action; `reason` is shown to the model as the tool error text.
226Missing the response within 5s is treated as "allow" (i.e. an
227unresponsive extension never stalls the agent).
228
229```json
230{"type":"event_intercept_response","id":"...",
231 "block":true,"reason":"refused: matches danger pattern \"rm -rf\""}
232```
233
234#### `command_response` (reply to `command_invoked`)
235
236```json
237{"type":"command_response","id":"...","action":"prompt",
238 "prompt":"Show today's weather for Berlin in one line."}
239```
240
241`action` is one of:
242
243- `"prompt"` — submits `prompt` as a fresh user message; the agent
244 runs a turn against it.
245- `"insert"` — inserts `insert` into the editor at the cursor without
246 submitting.
247- `"display"` — appends `display` to the chat as a one-shot styled
248 note. No model call, nothing written to the transcript.
249- `"noop"` — the extension handled it itself (e.g. it pushed
250 `notify` frames or kicked off background work). zot doesn't change
251 the UI in response.
252
253If `error` is non-empty, zot renders it as a red status line
254regardless of `action`.
255
256#### `notify` (one-way, any time)
257
258```json
259{"type":"notify","level":"info",
260 "message":"refreshed cache (12 entries)"}
261```
262
263`level` is one of `info`, `success`, `warn`, `error`. The note shows
264up below the transcript with the extension's name in brackets.
265
266#### `shutdown_ack`
267
268Sent in response to `shutdown`. Extension should exit promptly after.
269
270### Host → extension
271
272#### `hello_ack`
273
274```json
275{"type":"hello_ack","protocol_version":1,
276 "zot_version":"0.0.7","provider":"anthropic",
277 "model":"claude-opus-4-7","cwd":"/Users/pat/Developer/zot"}
278```
279
280Sent immediately after `hello`. The extension can use these fields to
281decide which commands to register (e.g. only register a Python tool
282on macOS, only register a model-specific shortcut for opus, etc.).
283
284#### `command_invoked`
285
286```json
287{"type":"command_invoked","id":"...",
288 "name":"weather","args":"berlin"}
289```
290
291`args` is everything the user typed after the command name, trimmed.
292
293#### `tool_call`
294
295Sent when the LLM invokes a tool the extension registered. `args` is
296the parsed JSON object the model produced; the extension is
297responsible for validating/coercing it.
298
299```json
300{"type":"tool_call","id":"...","name":"weather",
301 "args":{"city":"Berlin"}}
302```
303
304Reply with `tool_result` within the host's tool timeout (default 60s).
305Missing the timeout surfaces an error to the model and the call is
306marked as failed.
307
308#### `event`
309
310Lifecycle notification for events the extension subscribed to via
311`subscribe`. One-way — no response expected.
312
313```json
314{"type":"event","event":"turn_start","step":1}
315{"type":"event","event":"tool_call",
316 "tool_id":"...","tool_name":"read","tool_args":{"path":"foo.go"}}
317{"type":"event","event":"turn_end","stop":"end_turn"}
318```
319
320#### `event_intercept`
321
322Sent when zot wants to give the extension a chance to block a
323lifecycle event before it happens. Same payload shape as `event`.
324Reply with `event_intercept_response` within 5s; missing the deadline
325is treated as "allow".
326
327Only `event: "tool_call"` is sent in this version.
328
329```json
330{"type":"event_intercept","id":"...","event":"tool_call",
331 "tool_id":"...","tool_name":"bash",
332 "tool_args":{"command":"rm -rf /tmp/foo"}}
333```
334
335#### `shutdown`
336
337Sent during graceful zot exit (or `/reload-ext` once that lands).
338Reply with `shutdown_ack` and then exit.
339
340## Managing extensions from the CLI
341
342```
343zot ext list list installed extensions and their state
344zot ext install <path|git-url> copy / clone into $ZOT_HOME/extensions/
345zot ext remove <name> delete an extension directory
346zot ext enable <name> re-enable a disabled extension
347zot ext disable <name> disable without removing
348zot ext logs <name> [-f] cat / tail the extension's stderr
349```
350
351`zot ext install <path>` does a recursive copy; `<git-url>` does a
352shallow clone. Both validate that the destination contains an
353`extension.json` and roll back if not.
354
355## Loading an extension for one run
356
357For iteration on a working copy, skip the install + reload cycle
358and load straight from disk for one zot session:
359
360```
361zot --ext ./my-extension # short form: -e ./my-extension
362zot --ext ./a -e ./b # repeatable
363```
364
365`--ext` paths take precedence over installed extensions of the same
366name, so you can shadow an installed copy with a work-in-progress
367version without uninstalling first. Nothing is copied or persisted;
368the extension dies with zot like any other subprocess.
369
370## SDKs
371
372Writing the wire protocol by hand is fine for one-off scripts, but
373for anything bigger the SDKs handle the boilerplate.
374
375### Go — `pkg/zotext`
376
377```go
378package main
379
380import (
381 "encoding/json"
382 "github.com/patriceckhart/zot/pkg/zotext"
383)
384
385func main() {
386 ext := zotext.New("hello", "1.0.0")
387
388 // Slash command
389 ext.Command("hello", "say hi", func(args string) zotext.Response {
390 return zotext.Prompt("Greet me in one short sentence.")
391 })
392
393 // LLM-callable tool
394 ext.Tool("weather", "Current weather for a city.",
395 json.RawMessage(`{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}`),
396 func(args json.RawMessage) zotext.ToolResult {
397 var in struct{ City string `json:"city"` }
398 json.Unmarshal(args, &in)
399 return zotext.TextResult(in.City + ": sunny")
400 })
401
402 ext.Run()
403}
404```
405
406Build with `go build -o hello .`, drop the binary + an `extension.json`
407into `$ZOT_HOME/extensions/hello/`.
408
409See:
410- `examples/extensions/hello/` — slash commands
411- `examples/extensions/clock/` — slash commands in plain Node, no SDK
412- `examples/extensions/weather/` — LLM-callable tool
413- `examples/extensions/guard/` — event subscriptions + tool-call
414 interception (refuses dangerous bash patterns)
415
416### TypeScript / Python
417
418These SDKs aren't in the main repo yet; the wire format is small
419enough that a `~30 line` raw script gets you started in either
420language. See the [Quick start](#quick-start) Python example for the
421shape. SDK packages will land in follow-up commits.
422
423## Security
424
425Extensions run with **the user's full filesystem and network
426permissions**. Treat installing an extension the same as installing
427any other binary on your machine.
428
429`zot ext install <git-url>` clones from any URL you give it. There's
430no sandbox in v1; if you need isolation, install only extensions you
431trust or run zot under your platform's sandboxing tool (`bwrap` /
432`sandbox-exec` / AppContainer).
433
434## Roadmap
435
436Phase 1 (shipped):
437- [x] subprocess lifecycle + hello handshake
438- [x] `register_command` + `command_invoked`
439- [x] `notify`
440- [x] `zot ext` CLI
441
442Phase 2 (shipped):
443- [x] `register_tool` + `tool_call` + `tool_result`
444- [x] `ready` sentinel for safe agent-registry build timing
445- [x] tool result attribution surfaces extension name in details
446
447Phase 3 (shipped):
448- [x] event subscriptions (`session_start`, `turn_start`, `turn_end`,
449 `tool_call`, `assistant_message`)
450- [x] tool-call interception (block before execution)
451
452Future (no firm timeline):
453- [ ] interception for additional events beyond `tool_call`
454- [ ] modify (not just block) tool args mid-flight
455- [ ] `/reload-ext` slash command (hot-reload without restarting zot)