Mod API
A mod talks to Syntic Code through its standard input and standard output. Each message is a single JSON object on its own line. This page lists every message in both directions, the two API methods a mod can call, and the environment a mod starts with.
The protocol is deliberately small. A mod can draw, choose which events it hears, post a note and read a summary of the session. It has no way to run tools, send text to the model, read the conversation or change settings.
Protocol version
The current protocol version is 1. It is sent in the hello message and in the SYNTIC_MOD_PROTOCOL environment variable. Check it at start-up if you want your mod to refuse to run against a protocol it does not know.
Messages Syntic Code sends
type | Fields | When |
|---|---|---|
hello | protocol, mod, events, ui, granted | Once, always first |
event | event, data | A subscribed event happened. See Events. |
result | id, ok, and value or error | The answer to one of your call messages |
bye | none | The session is ending. Exit within half a second. |
hello
{"type":"hello","protocol":1,"mod":"turn-clock","events":["turn.start","turn.end"],"ui":["status","pane"],"granted":["notes"]}| Field | Type | Meaning |
|---|---|---|
protocol | number | Protocol version (1). |
mod | string | The mod’s name. |
events | string array | Events from the manifest. This is the starting subscription. |
ui | string array | Slots from the manifest that the mod may draw in. |
granted | string array | Permissions the user actually granted. This can be shorter than the manifest’s permissions list. |
Use granted to decide which features to turn on. For example, skip note.post calls when notes is missing, instead of sending calls that will be refused.
result
{"type":"result","id":1,"ok":true,"value":{}}
{"type":"result","id":2,"ok":false,"error":"permission `session.read` not granted"}id is copied from your call, so you can match answers to requests. It can be any JSON value: a number, a string or an object. On success ok is true and value holds the answer. On failure ok is false and error holds a short message.
Messages a mod sends
type | Fields | Effect |
|---|---|---|
draw | slot, optional text, lines, title | Sets or clears one slot. See Screen areas. |
subscribe | events | Replaces the event subscription with a subset of the manifest list. |
call | id, method, optional params | Calls an API method. Always answered with a result. |
draw
| Field | Type | Meaning |
|---|---|---|
slot | string | status, band or pane. Must be listed in the manifest’s ui. |
text | string | The content. For band and pane, newlines split it into rows. |
lines | string array | The content as rows. For band and pane, lines wins when both are sent; for status, text wins and only the first row of lines is used. |
title | string | Pane title. Ignored for other slots. Defaults to the mod name. |
A draw with neither text nor lines clears the slot.
subscribe
{"type":"subscribe","events":["turn.end"]}Names that are not in the manifest’s events are ignored.
call
{"type":"call","id":7,"method":"note.post","params":{"text":"Build finished."}}There are two methods.
note.post
Posts one short line into the conversation view. The line is shown to you only; it is never sent to the model and never becomes part of the conversation history the agent sees.
| Permission | notes |
| Params | text (string, required) |
| Returns | {} |
How the note is handled:
- Newlines are turned into spaces, and the text is cleaned the same way as drawn text (control and direction-override characters removed).
- It is cut to 500 characters.
- It appears as
[mod <name>] <text>. - Each mod may post at most 5 notes in any 60-second window. A sixth note gets an error result:
too many notes; try again in a minute. - If the screen falls far behind, only the newest 50 waiting notes (from all mods) are kept.
Errors:
error | Cause |
|---|---|
permission `notes` not granted | The user did not grant notes. |
`text` is required | params.text is missing or not a string. |
`text` is empty | Nothing was left after cleaning. |
too many notes; try again in a minute | Rate limit reached. |
session.state
Returns a summary of the session.
| Permission | session.read |
| Params | none |
{"type":"result","id":2,"ok":true,"value":{"session_id":"8c1f…","model":"amara-1","permission_mode":"default","turns":4,"busy":false,"cwd":"/home/sam/src/shop"}}| Field | Type | Meaning |
|---|---|---|
session_id | string | The session’s id. |
model | string | The model the session is using. |
permission_mode | string | The current permission mode, with the same values as mode.change. |
turns | number | The session’s turn counter. |
busy | boolean | true while the agent is answering. |
cwd | string | The folder the session was started in. |
The values are refreshed by the interface on every screen tick, so they are current to within a moment.
Errors and refused messages
Some messages are counted as bad lines:
- a line that is not valid JSON, is not UTF-8, or has an unknown
type; - a line longer than 64 KB (it is skipped whole);
- a
drawto a slot the manifest does not list; - a
callto an unknown method, or to a method whose permission was not granted.
Blank lines are ignored. A call that fails for another reason, such as a missing text or the note rate limit, still gets an error result but is not a bad line.
When a mod sends more than 20 bad lines in a session, Syntic Code stops it, clears its slots and shows a warning line in the conversation saying the mod was stopped because it sent too many messages it is not allowed to send.
An unknown method still gets a result with ok: false and the error unknown method `<name>` , so a mod written for a later protocol can detect what is missing.
The mod’s environment
A mod starts with an empty environment plus a short allow-list. Your shell’s other variables, including any tokens or keys, are not passed.
| Variable | Value |
|---|---|
PATH | A cleaned search path: only absolute folders that exist and are not inside the opened project, plus standard system folders. |
HOME, USER, LOGNAME | Copied from your environment, if set. |
LANG, LC_ALL, LC_CTYPE, TZ, TMPDIR | Copied, if set. |
SYSTEMROOT, WINDIR, PATHEXT, TEMP, TMP, USERPROFILE | Copied on Windows, if set. |
SYNTIC_MOD_NAME | The mod’s name. |
SYNTIC_MOD_DIR | The mod’s folder. |
SYNTIC_MOD_DATA | A private folder for the mod’s own files, ~/.syntic/mod-data/<name>/. Created before start, readable only by you on Unix. Kept between sessions. |
SYNTIC_MOD_PROTOCOL | 1 |
Other facts about the process:
- Its working folder is the mod’s own folder, not the project.
- Standard error is discarded.
- On Unix it runs in its own process group, so pressing Ctrl+C in the terminal does not reach it.
- When it is stopped, any processes it started in its group are stopped too.
Next steps
- Test a mod: try the protocol by hand.
- Troubleshoot a mod: what the notices mean.
- mod.json reference: the
permissionsfield.