Agent event protocol reference
The wire protocol coding-agent hooks use to report run state to Mytty. Transport, provider adapters, the mytty-agent-hook helper, and the per-provider installers are all implemented. Source: Sources/MyTTYCore/AgentEvent.swift, Sources/MyTTYCore/AgentHookBridge.swift. For which provider emits which event kind, see Agent providers.
Environment variables
Each Mytty terminal surface receives three environment variables:
MYTTY_EVENT_SOCKET
MYTTY_SURFACE_ID
MYTTY_EVENT_CAPABILITY
MYTTY_EVENT_CAPABILITY authorizes event emission for that surface only. It does not authorize terminal input, screen capture, or events for another surface. Mytty revokes it when the surface closes.
Codex is the exception. Codex runs sessions inside a shared background process, so a Codex hook inherits the variables of whichever pane started that process, not those of the pane its session runs in. Mytty therefore ignores capability and event.surfaceID on Codex events and works out the pane from event.sessionID instead, see Codex routing. mytty-agent-hook codex still sends the variables when it has them, and also works when they are missing.
Transport
Connect to MYTTY_EVENT_SOCKET, a user-only Unix stream socket with mode 0600. Send one UTF-8 JSON envelope, terminated by a newline, per connection. Maximum request size is 64 KiB. Dates are ISO 8601.
{
"schemaVersion": 1,
"capability": "value from MYTTY_EVENT_CAPABILITY",
"event": {
"schemaVersion": 1,
"id": { "rawValue": "B9AA8B83-B42D-4C11-B838-36B84C73032A" },
"runID": { "rawValue": "74C4B46D-9251-46AD-9200-61C97D98D43D" },
"sessionID": "0190f6f3-2a50-7000-8000-000000000001",
"surfaceID": { "rawValue": "value from MYTTY_SURFACE_ID" },
"provider": "codex",
"kind": "approval-requested",
"occurredAt": "2026-07-16T07:00:00Z",
"message": "Approve the dependency update."
}
}
The server returns one JSON response and closes the connection:
{ "ok": true, "inserted": true }
An idempotent retry returns inserted: false. Invalid JSON, authorization failure, oversized input, and internal storage failure return ok: false with a stable error code. Authorization responses never echo the capability back.
Envelope fields
| Field | Type | Notes |
|---|---|---|
schemaVersion (outer) |
Int | Envelope schema version, currently 1 |
capability |
String | Must equal MYTTY_EVENT_CAPABILITY for this surface. Ignored for codex |
event.schemaVersion |
Int | Event schema version, currently 1 |
event.id |
{rawValue: UUID} |
Must stay stable across a hook's retried delivery of the same event |
event.runID |
{rawValue: UUID} |
Stable for the lifetime of one agent run (one prompt/turn) |
event.sessionID |
String? | Provider's own session/conversation identifier; shown only while that provider is the foreground agent |
event.surfaceID |
{rawValue: <MYTTY_SURFACE_ID>} |
Must equal MYTTY_SURFACE_ID for this pane. Ignored for codex, where Mytty fills it in |
event.provider |
String | One of codex, claude-code, opencode, antigravity, cursor |
event.kind |
String | See event kinds below |
event.occurredAt |
String | ISO 8601 timestamp |
event.message |
String? | Optional human-readable detail, e.g. an approval prompt |
event.hookName |
String? | The originating hook's name, or a mytty.-prefixed marker (e.g. mytty.native.commandFinished, mytty.startupSweep) when Mytty synthesized the event itself rather than receiving it from a provider's hooks -- see Native run estimation |
Event kinds
AgentEventKind (wire values):
| Kind | Meaning |
|---|---|
started |
A new prompt or turn began |
running |
Work resumed after an input or approval request |
input-requested |
The agent needs free-form input from the user |
approval-requested |
The agent needs an approve/deny decision |
succeeded |
The run completed without error |
failed |
The run ended with an error |
disconnected |
The run ended because the provider process disconnected |
The reducer also derives an internal idle AgentRunState (not a wire event kind) once a run has gone quiet after succeeded/failed/etc; see AgentRunState in Sources/MyTTYCore/AgentEvent.swift.
Lifecycle rules
Hooks should emit started before any waiting or terminal event. Emit running when work begins again after an input or approval request. Human-readable terminal output must never be parsed to synthesize these events. Only structured hook payloads count.
Attention item creation
Mytty creates Attention drawer items only for:
- approval requests
- input requests
- failures
- disconnects
- successful work that ran for at least five minutes
Acknowledged or otherwise resolved items remain visible in the drawer for 24 hours after resolution.
Response codes
AgentEventServerResponse has three fields: ok, inserted (only set on success), error (only set on failure, a stable code string).
| Response | Meaning |
|---|---|
{"ok": true, "inserted": true} |
Event accepted and newly recorded |
{"ok": true, "inserted": false} |
Idempotent retry of an already-recorded event (same event.id) |
{"ok": false, "error": "request-too-large"} |
Envelope exceeded the 64 KiB limit |
{"ok": false, "error": "unauthorized"} |
capability did not match this surface's MYTTY_EVENT_CAPABILITY. For codex: the session could not be matched to exactly one pane, or the event has no sessionID |
{"ok": false, "error": "invalid-request"} |
Envelope could not be decoded as the expected JSON shape |
{"ok": false, "error": "internal-error"} |
Event storage failed on Mytty's side |
Codex routing
For codex events Mytty decides the pane from event.sessionID alone, using the Codex panes of all windows. In order:
- A pane whose own Codex process has that session's transcript open.
- A pane this session was bound to by an earlier event.
- A pane that was started with
codex resume <session-id>. - The only Codex pane without a binding whose working directory equals the working directory recorded in the session's transcript. Only transcripts of interactive Codex sessions (
codex-tuistarted by the user) qualify, so sub-agent,codex execand desktop app threads are never bound to a pane. - On a
SessionStartorUserPromptSubmitevent only: the only pane in that working directory that is still held by a binding that may be out of date, for example after/newor a resume inside the Codex screen.
A binding ends when the pane closes or its Codex process is replaced. Whenever no pane matches, or more than one does, the event is answered with unauthorized instead of being guessed. In particular, when two Codex panes in the same working directory are both still without a binding, events for a new session in that directory are rejected until one of them is bound. Codex writes a session's transcript only when its first prompt is submitted, so events that arrive before that (the SessionStart of a freshly started session) are rejected too.
Native run estimation
For a provider whose hook integration isn't installed, Mytty still detects a run starting and finishing on its own: it watches which agent executable is in a pane's foreground process, and reads OSC 133 when the shell reports that foreground command has finished. Events synthesized this way carry an event.hookName under the mytty.native. prefix (mytty.native.launchObserved, mytty.native.commandFinished, mytty.native.processExited) instead of a provider's own hook name, and always have event.sessionID set to null, since there's no hook payload to read a session identifier from.
This estimation only runs for a provider whose integration status is not installed; once hooks are installed for a provider, its runs go back to being reported by the provider's own hooks exclusively, and the native estimator stays out of the way. More generally, any hookName beginning with mytty. (not just mytty.native.) marks an event Mytty synthesized itself rather than received from a provider's hooks — see Sources/MyTTYCore/NativeAgentRunEstimator.swift and Sources/MyTTYCore/AgentHookEventAdapter.swift.
For Claude Code, Codex, and Cursor specifically, Mytty goes a step further: it also reads the provider's own local data — the same transcript (Claude Code, Codex) or store.db (Cursor) the session inspector already parses on every poll tick for the model name and context remaining — to split that one coarse run into a run per prompt turn, matching what a real hook integration would report (UserPromptSubmit on a new prompt, Stop when it finishes). The moment that read reports a turn, the coarse process-level run goes idle and per-turn runs (event.hookName under mytty.native.turn*, e.g. mytty.native.turnStarted, mytty.native.turnCompleted) take over reporting the lifecycle. Each turn run's ID is derived from the same turn identifier the provider's own hooks use for that turn (Claude Code's prompt ID, Codex's turn ID, Cursor's conversation ID plus the prompt blob's own rowid), so a hook that starts covering the pane mid-turn — or Claude Code's ESC-interruption handling, which has no hook of its own — lands on the exact same run rather than opening a duplicate one. Cursor writes no marker for an interrupted turn, so its turn phase is never .interrupted — a native-estimated run there still relies on process exit to end an abandoned turn.
On launch, before anything else reads run state, Mytty also sweeps the log for a run a previous app instance left non-terminal — every pane process dies with the app, so a run still running/waitingInput/waitingApproval after a crash, force-quit, or SIGKILL can only be dead — and records a synthesized disconnected event for it (event.hookName is mytty.startupSweep).
See also
- Agent providers covers file locations, per-provider hook-to-event mapping, and status bar/session-inspector sources.
- mytty-ctl reference documents the
list/waitsurface that reads theAgentRunStatethis protocol produces.