Skip to content

Socket API

Preview docs describe unreleased preview builds. Stable docs remain at /docs/.

Nagi exposes a local socket API for scripts and agents that need to inspect or control a running session.

Most automation should start with the CLI wrappers. Use the raw socket API only when you need direct request/response control or long-lived event subscriptions.

LayerUse it for
Agent skillTeaching a coding agent how to use Nagi from inside a pane.
CLI wrappersShell scripts, simple orchestration, and human debugging.
Raw socket APICustom tools, protocol clients, and event subscribers.

The layers share the same control surface.

The installed CLI can print the socket protocol schema bundled with that Nagi binary:

Terminal window
nagi api schema
nagi api schema --json
nagi api schema --output nagi-api.schema.json

Plain nagi api schema prints a short summary. --json prints the full JSON Schema document for tools, and --output PATH writes that document to a file. The schema covers raw requests, success responses, error responses, emitted events, and subscription events.

The socket API can:

  • create, list, focus, rename, and close workspaces
  • create, list, focus, rename, and close tabs
  • list, inspect, split, swap, focus, resize, rename, read, close, and send input to panes
  • list, inspect, read, send to, rename, focus, start, and attach agents through CLI helpers
  • report custom agent state from hooks and plugins
  • subscribe to events and wait for output or state changes
  • create, inspect, configure, and start durable coding-agent missions
  • install and uninstall built-in integrations
  • stop the server and reload config

Create a workspace:

Terminal window
nagi workspace create --cwd ~/project --label api

Create a tab:

Terminal window
nagi tab create --label logs

Split a pane and run a command:

Terminal window
nagi pane split w1:p1 --direction right
nagi pane run w1:p2 "npm test"

Inspect and rearrange panes:

Terminal window
nagi pane layout --current
nagi pane neighbor --direction right --current
nagi pane resize --direction right --amount 0.1 --current
nagi pane swap --direction right --current
nagi pane zoom --on --current
nagi pane split w1:p1 --direction right --ratio 0.333

Wait for an agent:

Terminal window
nagi wait agent-status w1:p1 --status done

Read pane output:

Terminal window
nagi pane read w1:p2 --source recent --lines 50

Raw socket method names use dot notation:

AreaMethods
Serverping, server.stop, server.reload_config, server.agent_manifests, server.reload_agent_manifests
Notificationnotification.show
Clientclient.window_title.set, client.window_title.clear
Sessionsession.snapshot
Missionmission.create, mission.list, mission.get, mission.configure, mission.start, mission.respond, mission.proof.get, mission.handoff.preview, mission.handoff.start, mission.close
Workspaceworkspace.create, workspace.list, workspace.get, workspace.focus, workspace.rename, workspace.move, workspace.report_metadata, workspace.close
Worktreeworktree.list, worktree.create, worktree.open, worktree.remove
Tabtab.create, tab.list, tab.get, tab.focus, tab.rename, tab.move, tab.close
Panepane.split, pane.swap, pane.move, pane.zoom, pane.layout, pane.process_info, pane.neighbor, pane.edges, pane.focus_direction, pane.resize, pane.list, pane.current, pane.get, pane.rename, pane.send_text, pane.send_keys, pane.send_input, pane.read, pane.graphics.info, pane.graphics.set, pane.graphics.clear, pane.graphics.stream, pane.report_agent, pane.report_agent_session, pane.report_metadata, pane.clear_agent_authority, pane.release_agent, pane.close, pane.wait_for_output
Popuppopup.close
Layoutlayout.export, layout.apply, layout.set_split_ratio
Agentagent.list, agent.get, agent.read, agent.explain, agent.send, agent.rename, agent.focus, agent.start
Eventsevents.subscribe, events.wait
Integrationsintegration.install, integration.uninstall
Pluginsplugin.link, plugin.list, plugin.unlink, plugin.enable, plugin.disable, plugin.action.list, plugin.action.invoke, plugin.log.list, plugin.pane.open, plugin.pane.focus, plugin.pane.close

The mission API persists intent and ownership today, but its managed provider boundary is intentionally narrow:

MethodRequired parametersCurrent behavior
mission.createmission_id, title, absolute repository_path, objective, acceptance_criteriaCreates a durable draft. IDs are 1–128 characters; there must be 1–16 non-empty criteria. Repeating the exact request is idempotent; changing it returns mission_conflict.
mission.listnoneReturns durable mission summaries.
mission.getmission_idReturns the full public mission projection.
mission.configuremission_id, checksFreezes 1–32 command or manual checks. Every acceptance criterion must be covered. A different second configuration is rejected.
mission.startmission_id, run_id, provider, mode; optional worktree_path, execute_declared_checksClaims one canonical Git checkout and starts Codex, Claude Code, OpenCode, or a configured ACP agent in managed mode. The closure plan must already exist.
mission.respondmission_id, run_id, attention_id, decision; optional answersReserved for the interactive consent path. It currently always returns interactive_consent_required and does not send a provider response.

execute_project_recipe is persisted for restart recovery but cannot be authorized through the public socket. Sending it as true returns interactive_consent_required. The local mission cockpit previews the .nagi/project.toml scope and is the only path that can approve setup and service execution.

provider accepts codex, claude_code, open_code, or acp; mode accepts managed or passthrough. Every provider supports managed starts. ACP uses the local stdio command configured in providers.acp.command and always requires explicit write-scope confirmation in the local cockpit. Every passthrough start returns an availability error. Public managed starts receive read-only workspace access. There is no API switch that silently grants write access.

mission.handoff.preview creates a redacted, digest-bound snapshot of the objective, decisions, checks and current Git workspace. Starting the continuation requires mission.handoff.start with that exact artifact_sha256 and generated_at_millis. Any intervening workspace or mission change returns handoff_artifact_changed. The next provider continues the same mission in the same worktree, prior evidence becomes stale, and the source run remains in run_history.

Mission errors use stable codes where callers need branching behavior:

CodeMeaning
feature_unavailableMission storage is unavailable on this platform.
mission_not_found, mission_conflictThe mission is missing, or an immutable definition/configuration differs.
invalid_mission, invalid_repository, invalid_closureIDs, text, Git root, criteria coverage, or check declarations are invalid.
provider_unavailable, provider_mode_unavailableThe provider executable is unavailable, or passthrough mode is not wired.
run_conflict, invalid_run, provider_start_failedThe run ID conflicts, cannot form a claim identity, or the provider process did not start.
interactive_consent_requiredmission.respond is blocked until the mission cockpit can confirm the decision.
handoff_artifact_changed, handoff_attention_unresolvedThe inspected snapshot is stale, or source-run attention still needs a decision.

The generated schema is the source of truth for nested checks, path rules, response decisions, result objects, and field-length constraints. Check the schema bundled with the exact Nagi binary you are controlling.

Some CLI commands are conveniences around these methods. For example, nagi agent wait resolves an agent target and then subscribes to pane agent state events.

session.snapshot returns a one-time bootstrap snapshot for clients that keep their own local runtime cache. The response includes version/protocol metadata, focused workspace/tab/pane ids, workspace records, tab records, pane records, tab layout snapshots, and agent records. It is not a subscription; after reading it, subscribe to resource events and update the local cache from those events. Call session.snapshot again after reconnecting or when the local cache may be stale. Attached worktree provenance is included on workspace records. Full repo worktree discovery remains worktree.list.

From the CLI, nagi api snapshot prints the live session.snapshot response as JSON for clients and agents that want a simple bootstrap command.

Pane control methods use public pane ids such as w1:p1. Methods whose schema makes pane_id optional use the server’s active focused pane when it is omitted. pane.move always requires the source pane_id.

pane.send_keys and pane.send_input.keys accept Nagi key-combo strings: plain printable keys, special keys like enter and esc, modifier chords like ctrl+h, control+j, alt+x, and shift+tab, function keys like f1, and named punctuation like minus and plus. They do not accept prefix+ binding strings.

{"id":"req_current","method":"pane.current","params":{"caller_pane_id":"w1:p1"}}
{"id":"req_layout","method":"pane.layout","params":{"pane_id":"w1:p1"}}
{"id":"req_neighbor","method":"pane.neighbor","params":{"pane_id":"w1:p1","direction":"right"}}
{"id":"req_edges","method":"pane.edges","params":{"pane_id":"w1:p1"}}
{"id":"req_focus","method":"pane.focus_direction","params":{"direction":"right"}}
{"id":"req_resize","method":"pane.resize","params":{"pane_id":"w1:p1","direction":"right","amount":0.1}}
{"id":"req_zoom","method":"pane.zoom","params":{"pane_id":"w1:p1","mode":"toggle"}}
{"id":"req_split","method":"pane.split","params":{"direction":"right","ratio":0.333,"env":{"NAGI_ROLE":"tests"}}}
{"id":"req_process","method":"pane.process_info","params":{"pane_id":"w1:p1"}}

pane.current returns a single PaneInfo. When caller_pane_id is present, Nagi returns that pane. When it is omitted, Nagi returns the active focused pane.

PaneInfo includes scroll when terminal scroll metrics are available:

{
"offset_from_bottom": 12,
"max_offset_from_bottom": 240,
"viewport_rows": 30
}

Clients can treat offset_from_bottom == 0 as at-bottom state.

Pane graphics let a plugin place image data over a pane. They are available only when [experimental].kitty_graphics = true; otherwise every pane graphics method returns feature_disabled. Calling pane.graphics.info returns the attached client’s cell width and height in pixels without creating a graphics layer. pane.graphics.set accepts png, rgb, or rgba data in data_base64, and pane.graphics.clear removes the layer.

{"id":"graphics_info","method":"pane.graphics.info","params":{"pane_id":"w1:p1"}}
{"id":"graphics_set","method":"pane.graphics.set","params":{"pane_id":"w1:p1","format":"png","image_width":800,"image_height":600,"data_base64":"...","placement":{"viewport_col":0,"viewport_row":0,"grid_cols":80,"grid_rows":30}}}
{"id":"graphics_clear","method":"pane.graphics.clear","params":{"pane_id":"w1:p1"}}

For repeated frames, open a dedicated socket with pane.graphics.stream. After Nagi replies with ok, send one JSON header and then exactly data_length raw bytes per frame. A stream owns that pane’s graphics layer until the socket closes; concurrent set, clear, or stream requests return stream_conflict.

{"id":"graphics_stream","method":"pane.graphics.stream","params":{"pane_id":"w1:p1"}}
{"format":"png","image_width":800,"image_height":600,"data_length":12345,"placement":{"viewport_col":0,"viewport_row":0,"grid_cols":80,"grid_rows":30}}

pane.layout returns the tab layout snapshot with workspace_id, tab_id, zoomed, outer area, focused_pane_id, pane rects, and split rects/ratios. pane.neighbor and pane.edges include that same layout snapshot so clients can make the next decision without private layout state.

pane.process_info returns the pane’s shell pid, foreground process group id when available, and foreground processes with pid, name, argv/cmdline, and cwd when the platform exposes them.

layout.export returns a portable tab layout tree. Omit tab_id and pane_id to export the active tab, pass tab_id to export that tab, or pass pane_id to export the tab containing that pane.

{"id":"req_export","method":"layout.export","params":{"tab_id":"w1:t1"}}

The response includes workspace_id, tab_id, zoomed, focused_pane_id, and root. root is a BSP tree of pane and split nodes. Pane nodes can include pane_id, label, cwd, and argv command. Split nodes use direction (right or down), ratio, first, and second.

layout.apply creates a fresh tab from a declarative tree. If tab_id is provided, Nagi creates the replacement tab first and then closes the old tab. This restores structure, labels, cwd, env, and optional argv commands; it does not preserve live PTYs, scrollback, or running processes.

{
"id": "req_apply",
"method": "layout.apply",
"params": {
"workspace_id": "wabc",
"tab_label": "dev",
"focus": true,
"root": {
"type": "split",
"direction": "right",
"ratio": 0.65,
"first": {
"type": "pane",
"label": "editor",
"cwd": "/repo"
},
"second": {
"type": "pane",
"label": "tests",
"cwd": "/repo",
"command": ["sh", "-c", "just test"],
"env": { "NAGI_ROLE": "tests" }
}
}
}
}

layout.set_split_ratio updates an existing split in a tab layout. The response is type: "layout_split_ratio_set" with the updated portable layout.

{"id":"req_ratio","method":"layout.set_split_ratio","params":{"tab_id":"w1:t1","path":[],"ratio":0.6}}

Process-launching methods accept an env object. Nagi applies those key/value pairs to the newly launched process only. Nagi also injects NAGI_SOCKET_PATH, NAGI_ENV=1, NAGI_WORKSPACE_ID, NAGI_TAB_ID, and NAGI_PANE_ID into managed pane processes. Nagi-managed variables are authoritative when they conflict with caller-provided env.

pane.swap supports directional and explicit forms:

{"id":"req_swap_dir","method":"pane.swap","params":{"pane_id":"w1:p1","direction":"right"}}
{"id":"req_swap_explicit","method":"pane.swap","params":{"source_pane_id":"w1:p1","target_pane_id":"w1:p2"}}

Swap is same-tab only. It preserves split shape, split ratios, pane ids, and running processes. The response is type: "pane_swap" with changed, optional reason, source_pane_id, optional target_pane_id, focused_pane_id, and layout. Reason values are no_neighbor, same_pane, not_found, and cross_tab. When a tab is zoomed, swap keeps zoom active and mutates the hidden full-tab layout.

pane.move moves a running pane to a different tab, a new tab, or a new workspace:

{"id":"req_move_tab","method":"pane.move","params":{"pane_id":"w1:p2","destination":{"type":"tab","tab_id":"w1:t2","target_pane_id":"w1:p3","split":"right","ratio":0.5},"focus":true}}
{"id":"req_move_new_tab","method":"pane.move","params":{"pane_id":"w1:p2","destination":{"type":"new_tab","workspace_id":"w1","label":"logs"},"focus":true}}
{"id":"req_move_new_workspace","method":"pane.move","params":{"pane_id":"w1:p2","destination":{"type":"new_workspace","label":"logs","tab_label":"main"},"focus":true}}

Existing-tab moves require split: "right" | "down". target_pane_id is optional and defaults to the target tab’s focused pane. Same-tab layout changes remain pane.swap; moving to the source tab returns changed: false with reason: "same_tab". Moves involving a zoomed source or target tab return changed: false with reason: "zoomed_tab".

The response is type: "pane_move" with changed, optional reason, previous_pane_id, previous_workspace_id, previous_tab_id, the moved pane, optional source_layout, target_layout, optional created workspace or tab records, optional closed workspace or tab ids, and focused_pane_id. Cross-workspace moves keep the internal pane and terminal alive but assign a new public pane id in the destination workspace. Subscribers can listen for pane.moved; Nagi does not emit fake pane close/create events for the moved terminal process.

pane.zoom toggles, enables, or disables zoom for the target pane’s tab:

{"id":"req_zoom_toggle","method":"pane.zoom","params":{"pane_id":"w1:p1"}}
{"id":"req_zoom_on","method":"pane.zoom","params":{"pane_id":"w1:p1","mode":"on"}}
{"id":"req_zoom_off","method":"pane.zoom","params":{"pane_id":"w1:p1","mode":"off"}}

Omitting pane_id targets the server’s active focused pane. The response is type: "pane_zoom" with changed, zoom_changed, focus_changed, optional reason, pane_id, focused_pane_id, zoomed, and layout. changed is true when either zoom state or focus changed. Reason values are single_pane, already_zoomed, and already_unzoomed.

The CLI wrapper for notification.show is:

Terminal window
nagi notification show "build failed" --body "api workspace" --position top-left --sound request

Show a user notification through the configured toast delivery:

{"id":"req_notify","method":"notification.show","params":{"title":"build failed","body":"api workspace","position":"top-left","sound":"request"}}

title is required and must contain visible text after control characters and repeated whitespace are removed. body is optional. Nagi collapses newlines, tabs, carriage returns, and repeated whitespace into spaces, then trims notification text to 80 characters for title and 240 characters for body. An empty sanitized title returns invalid_params. position is optional and applies only when ui.toast.delivery = "nagi"; desktop positions are relative to the full Nagi frame, and omitted positions use ui.toast.nagi.position. Terminal, system, and off delivery ignore position. sound is optional and can be none, done, or request; it defaults to none and plays only when the notification is shown.

The response reports whether anything was shown:

{"id":"req_notify","result":{"type":"notification_show","shown":true,"reason":"shown"}}

Possible reasons are shown, disabled, rate_limited, no_foreground_client, and busy. disabled means ui.toast.delivery = "off". busy means an existing in-app toast was not replaced. Terminal and system delivery are best-effort through the current foreground attached Nagi client.

Set or clear the foreground client’s outer terminal window title:

{"id":"req_title","method":"client.window_title.set","params":{"title":"nagi api"}}
{"id":"req_title_clear","method":"client.window_title.clear","params":{}}

client.window_title.clear restores Nagi’s default title. The response is type: "client_window_title" with changed and reason set, cleared, or no_foreground_client.

Worktree methods manage Git checkouts as Nagi workspaces. worktree.create creates a checkout and returns the new workspace, tab, root_pane, and worktree records. If the requested branch already exists locally, it checks out that branch; otherwise it creates the branch from the requested base or HEAD. worktree.open opens an existing checkout or returns the already-open workspace. worktree.remove runs git worktree remove against a linked child workspace and never deletes the branch.

Create a worktree from a source workspace:

{"id":"req_1","method":"worktree.create","params":{"workspace_id":"w1","branch":"worktree/api","focus":false}}

Open an existing checkout:

{"id":"req_2","method":"worktree.open","params":{"workspace_id":"w1","branch":"worktree/api","focus":true}}

Remove a linked checkout:

{"id":"req_3","method":"worktree.remove","params":{"workspace_id":"2","force":false}}

Use at most one of workspace_id or cwd for worktree.list, worktree.create, and worktree.open; omit both to use the active workspace. Use exactly one of path or branch for worktree.open. Raw socket cwd and path values must be absolute; the CLI expands relative --cwd and --path values before sending requests. Workspace responses include optional worktree provenance when a workspace belongs to a Nagi worktree group. Worktree commands can emit workspace.updated when an existing workspace gains or changes worktree provenance.

Worktree commands also emit lifecycle events. worktree.create emits workspace.created, tab.created, pane.created, and worktree.created. worktree.open emits worktree.opened, and it also emits workspace/tab/pane creation events when it opens a new Nagi workspace. worktree.remove emits worktree.removed; if the linked workspace is still open, it also emits workspace.closed.

The plugin API is an early host surface for executable workflow tools. A plugin is a package with a nagi-plugin.toml manifest. The manifest declares shareable actions, event hooks, terminal pane entrypoints, and link handlers. Actions and panes are manifest-only; runtime action registration and runtime argv pane creation are not part of v1.

Installed and linked plugins persist across restarts. Nagi writes a plugins.json registry file alongside session.json on plugin.link, plugin.unlink, plugin.enable, and plugin.disable. The nagi plugin install CLI also writes the same registry when Nagi is not running, then startup loads it automatically. On startup, Nagi re-reads each manifest from its original path; if the file is missing or unparseable the entry is kept with a warnings field so plugin.list surfaces it. Native plugins execute with the current user’s OS permissions. Enabling one therefore requires trust_native: true on plugin.link. Omitting it or sending false may register a disabled plugin, but an enabled link and every later enable attempt fail with plugin_native_trust_required. Registry entries from older Nagi versions migrate to disabled and untrusted.

Event hook on values are validated against the known Nagi event names at link time. An unrecognised name is not an error — the link still succeeds — but the returned plugin info includes a warning (e.g. "unknown event 'worktree.craeted'"). Check the warnings field in the plugin.link and plugin.list responses.

Link a local plugin manifest:

{"id":"req_plugin_link","method":"plugin.link","params":{"path":"/path/to/plugin","enabled":true,"trust_native":true}}

plugin.link also accepts optional source metadata. The CLI uses this when it installs from GitHub so plugin.list can show origin, requested ref, resolved commit, and managed checkout path:

{"id":"req_plugin_link","method":"plugin.link","params":{"path":"/managed/plugin/nagi-plugin.toml","enabled":true,"trust_native":true,"source":{"kind":"github","owner":"owner","repo":"nagi-plugin","subdir":"worktree-bootstrap","requested_ref":"main","resolved_commit":"abc123","managed_path":"/data/plugins/github/<managed-checkout>","installed_unix_ms":1780000000000}}}

The path can be a plugin directory containing nagi-plugin.toml or a direct manifest path. The manifest shape is:

id = "example.worktree-bootstrap"
name = "Worktree Bootstrap"
version = "0.1.0"
min_nagi_version = "0.7.0"
description = "Prepare new worktrees"
platforms = ["linux", "macos", "windows"]
[[build]]
command = ["bun", "install"]
[[actions]]
id = "bootstrap"
title = "Bootstrap worktree"
contexts = ["workspace"]
command = ["bun", "run", "bootstrap.ts"]
[[events]]
on = "worktree.created"
command = ["bun", "run", "bootstrap.ts"]
[[panes]]
id = "board"
title = "Worktree board"
placement = "overlay"
command = ["bun", "run", "board.ts"]
[[link_handlers]]
id = "github-issue"
title = "Open GitHub issue"
pattern = "^https://github\\.com/[^/]+/[^/]+/(issues|pull)/[0-9]+$"
action = "bootstrap"

min_nagi_version is required. The server refuses to link a plugin when the field is missing, invalid, or newer than the running Nagi binary.

Declare platforms at the top level with the OS identifiers (linux, macos, windows) your plugin supports. Omitting platforms is allowed for local development — plugin.link succeeds but the response includes a warning. Individual build commands, actions, event hooks, panes, and link handlers can declare their own platforms to override the plugin-level list; if omitted they inherit from the plugin. Invoking an action or opening a pane whose effective platforms do not include the current OS returns a platform_unsupported error.

List, enable, disable, or unlink linked plugins:

{"id":"req_plugin_list","method":"plugin.list","params":{}}
{"id":"req_plugin_disable","method":"plugin.disable","params":{"plugin_id":"example.worktree-bootstrap"}}
{"id":"req_plugin_enable","method":"plugin.enable","params":{"plugin_id":"example.worktree-bootstrap"}}
{"id":"req_plugin_unlink","method":"plugin.unlink","params":{"plugin_id":"example.worktree-bootstrap"}}

Actions are resolved from the linked manifest. plugin.action.list returns all actions across installed plugins; pass plugin_id to filter.

{"id":"req_plugin_actions","method":"plugin.action.list","params":{}}
{"id":"req_plugin_actions_filtered","method":"plugin.action.list","params":{"plugin_id":"example.worktree-bootstrap"}}

plugin.action.list returns each action’s effective platforms after applying plugin-level inheritance.

Invoke an action by its qualified id or bare action id:

{"id":"req_plugin_invoke","method":"plugin.action.invoke","params":{"action_id":"example.worktree-bootstrap.bootstrap","context":{"invocation_source":"keybinding"}}}

plugin.action.invoke resolves the manifest action, starts the manifest command, and returns the Nagi-built invocation context plus the started command log record. Missing context fields are filled from the active workspace, tab, focused pane, worktree provenance, and request id. Invoking an action from a disabled plugin returns a plugin_disabled error.

Nagi injects NAGI_SOCKET_PATH, NAGI_BIN_PATH, NAGI_ENV=1, NAGI_PLUGIN_ID, NAGI_PLUGIN_ROOT, NAGI_PLUGIN_CONFIG_DIR, NAGI_PLUGIN_STATE_DIR, NAGI_PLUGIN_CONTEXT_JSON, and available NAGI_WORKSPACE_ID, NAGI_TAB_ID, and NAGI_PANE_ID values. Action commands also receive NAGI_PLUGIN_ACTION_ID; event hooks receive NAGI_PLUGIN_EVENT and NAGI_PLUGIN_EVENT_JSON; pane commands receive NAGI_PLUGIN_ENTRYPOINT_ID.

List recent action and event command logs:

{"id":"req_plugin_logs","method":"plugin.log.list","params":{"plugin_id":"example.worktree-bootstrap","limit":20}}

Event hooks run for enabled installed plugins when Nagi emits a matching event name such as worktree.created.

There is no Nagi-managed plugin storage API in v1. NAGI_PLUGIN_CONFIG_DIR and NAGI_PLUGIN_STATE_DIR are path discovery only; plugins own their files, schemas, migrations, and cleanup.

Open a managed terminal UI:

{"id":"req_plugin_pane","method":"plugin.pane.open","params":{"plugin_id":"example.board","entrypoint":"board","placement":"zoomed","target_pane_id":"w1:p1","env":{"NAGI_ROLE":"board"},"focus":true}}

plugin.pane.open requires an installed, enabled, platform-compatible plugin, then launches the requested manifest [[panes]] entrypoint as an argv-backed terminal pane. Manifest pane placement defaults to overlay; request placement overrides the manifest with overlay, popup, split, tab, or zoomed. Overlay and popup placements use the active tiled pane as launch context. Popup terminals are session-modal and do not change the tab layout; optional width and height fields set their outer size as terminal cells or percentages such as "80%". Omitted dimensions default to half the terminal size, with too-small values clamped to the popup minimum. A popup has no pane ID, remains outside all pane.* and agent APIs, emits no pane lifecycle events, leaves plugin focus context on the underlying tiled pane, and does not export NAGI_PANE_ID to its process. Popup launch returns ok; popup.close closes the active popup and returns popup_not_open when none exists. Split and zoomed panes target an existing pane; tab panes can target a workspace. Split, tab, zoomed, and overlay panes behave like normal Nagi panes, and plugin.pane.focus and plugin.pane.close continue to operate on those panes.

Nagi uses newline-delimited JSON over a local socket. On Unix, that socket is a Unix domain socket. On Windows, it is a named pipe.

Send one request per line:

{"id":"req_1","method":"ping","params":{}}

A successful response includes the same id:

{"id":"req_1","result":{"type":"pong"}}

Event subscriptions keep the connection open after the initial response.

The default socket lives under your Nagi config directory.

Named sessions have separate sockets:

~/.config/nagi/nagi.sock
~/.config/nagi/sessions/<name>/nagi.sock

Resolution order:

  1. explicit CLI --session <name>
  2. NAGI_SOCKET_PATH
  3. NAGI_SESSION=<name>
  4. default session socket

Use NAGI_SOCKET_PATH only for low-level overrides.

For plugins, prefer invoking NAGI_BIN_PATH and the CLI wrappers when you need portable Windows behavior. Raw socket clients are responsible for using the platform-native local socket form.

Integrations report agent state with pane.report_agent.

{
"id": "req_1",
"method": "pane.report_agent",
"params": {
"pane_id": "w1:p1",
"source": "custom:docs",
"agent": "docs-bot",
"state": "working",
"message": "building docs"
}
}

state is semantic. It affects waits, notifications, and rollups. Report display-only values separately through metadata.

Session-only official integrations report native session references with pane.report_agent_session. State-reporting integrations can still include native session references in pane.report_agent. State-independent session reports do not affect waits, notifications, or rollups.

{
"id": "req_2",
"method": "pane.report_agent_session",
"params": {
"pane_id": "w1:p1",
"source": "nagi:codex",
"agent": "codex",
"agent_session_id": "..."
}
}

pane.get, pane.list, agent.get, and agent.list expose a read-only agent_session object when Nagi has a stored native session reference:

{
"agent_session": {
"source": "nagi:codex",
"agent": "codex",
"kind": "id",
"value": "..."
}
}

If no native session reference is stored, the field is omitted.

pane.get, pane.list, agent.get, and agent.list also expose foreground_cwd when Nagi can resolve the cwd of the process currently controlling the pane PTY. The existing cwd field remains the pane/workspace cwd used for labels, follow-cwd behavior, and restored session state.

PaneInfo and AgentInfo expose optional terminal_title and terminal_title_stripped fields. terminal_title is the latest OSC 0/2 title after safety normalization. terminal_title_stripped removes one recognized leading activity or spinner glyph and following whitespace. These server-owned values are ephemeral across a cold restart and are independent of the metadata title and semantic agent state.

Use pane.report_metadata when a user hook wants to customize presentation without taking over lifecycle state from a Nagi integration.

{
"id": "req_2",
"method": "pane.report_metadata",
"params": {
"pane_id": "w1:p1",
"source": "user:claude-title",
"agent": "claude",
"title": "Refactor auth middleware",
"display_agent": "Claude: auth",
"state_labels": {
"working": "refactoring auth",
"idle": "ready",
"done": "review ready"
},
"tokens": {
"summary": "refactor auth",
"model": "opus"
},
"ttl_ms": 3600000
}
}

Metadata reports are display-only. Valid metadata can override the pane title, displayed agent name, visible state labels, and arbitrary named tokens. working, blocked, idle, waits, notifications, and rollups still come from semantic state. Native session restore comes from stored official session references. agent is an optional guard for presentation fields against the authoritative agent label; applies_to_source similarly guards presentation fields against the active lifecycle authority source. These guards do not apply to token patches: token reporters own clearing and TTL refresh. Use display_agent to change the visible name. state_labels keys must be idle, working, blocked, done, or unknown.

Token maps are per-resource patches. A string sets a key, JSON null clears it, and omitted keys remain unchanged. The latest accepted update wins. Optional TTL applies independently to token keys updated by that report. Pane tokens are exposed by pane and agent get/list responses and can be rendered as $name in Agent sidebar rows. A report may mention at most 16 token keys, and a pane or workspace may retain at most 32 keys. Token names are 1–32 ASCII letters, digits, underscores, or hyphens.

Workspace tokens use the same contract:

{"id":"req_3","method":"workspace.report_metadata","params":{"workspace_id":"w1","source":"user:jj","tokens":{"jj_status":"2 changes","old":null},"ttl_ms":5000}}

Workspace get/list responses expose the resulting tokens map, and Space sidebar rows can render values such as $jj_status. Changes and TTL expiry emit workspace.metadata_updated with the latest workspace snapshot. This metadata event is available to API subscribers but does not invoke plugin event hooks.

Presentation text is normalized before storage. Nagi trims surrounding whitespace, removes control characters, and caps title, display_agent, each state label, and token values at 80 characters. Empty normalized token values clear that key.

source and applies_to_source are source identifiers. They must be 80 characters or fewer and may contain only ASCII letters, digits, colon, dot, underscore, and hyphen.

Use ttl_ms for short-lived metadata. It must be between 1 and 86400000 milliseconds. Omit ttl_ms for metadata that should stay until replaced, cleared, or the pane/workspace closes. Presentation fields retain their existing source-scoped expiry behavior; each token updated by the call receives its own deadline. Token metadata is not restored after a server restart.

Use seq when a hook may send updates out of order. For the same source, reports with a sequence number less than or equal to the last accepted sequence are accepted by the API but ignored by the pane state. A pane or workspace accepts sequenced token reports from at most 32 distinct sources during its lifetime; clearing or expiry does not release those source slots.

Subscribe to events when you need a long-lived stream:

{
"id": "sub_1",
"method": "events.subscribe",
"params": {
"subscriptions": [
{ "type": "pane.agent_status_changed", "pane_id": "w1:p1", "agent_status": "blocked" }
]
}
}

The first response acknowledges the subscription. Later lines are pushed events.

Workspace event subscriptions include workspace.created, workspace.updated, workspace.metadata_updated, workspace.renamed, workspace.moved, workspace.closed, and workspace.focused. workspace.metadata_updated reports token changes and TTL expiry without invoking plugin event hooks. Other workspace events describe Nagi UI/runtime lifecycle. workspace.created includes optional workspace.worktree provenance when the workspace belongs to a worktree group. workspace.moved includes the moved workspace_id, requested insert_index, and updated ordered workspaces list. workspace.closed includes a final workspace snapshot when Nagi can still identify it before removal. Tab event subscriptions include tab.created, tab.closed, tab.focused, tab.renamed, and tab.moved. tab.moved includes the moved tab_id, workspace_id, requested insert_index, and updated ordered tabs list for that workspace. Pane event subscriptions include pane.created, pane.updated, pane.closed, pane.focused, pane.moved, pane.exited, pane.agent_detected, pane.output_matched, pane.agent_status_changed, and pane.scroll_changed. Terminal-title changes can emit pane.updated, but spinner-only raw-title changes do not emit it when terminal_title_stripped is unchanged. pane.scroll_changed is scoped to one pane_id and emits pane_id, workspace_id, and the current scroll metrics whenever Nagi observes a changed scroll snapshot. Layout event subscriptions include layout.updated. The event carries the updated PaneLayoutSnapshot for one tab. Clients that bootstrap with session.snapshot should replace the cached layout with the same workspace_id and tab_id.

Worktree event subscriptions include worktree.created, worktree.opened, and worktree.removed. Worktree events describe Git checkout lifecycle. worktree.created includes the opened workspace and created worktree. worktree.opened includes the target workspace, opened worktree, and already_open. worktree.removed includes the workspace_id, removed worktree, and forced.

Use events.subscribe for lifecycle events. Dedicated wait helpers are documented separately when a one-shot wait is supported.

Use pane.read through the CLI unless you are writing a protocol client.

Terminal window
nagi pane read w1:p1 --source visible --lines 80
nagi pane read w1:p1 --source recent --lines 120
nagi pane read w1:p1 --source recent-unwrapped --lines 120
nagi pane read w1:p1 --source detection

recent-unwrapped is useful for logs because it ignores soft wrapping. detection returns the bottom-buffer snapshot used by agent screen detection.

Use waits to coordinate agents and scripts.

Terminal window
nagi wait agent-status w1:p1 --status done
nagi wait agent-status w1:p1 --status blocked

Agent waits observe semantic state, not arbitrary command completion.

Successful responses look like this:

{
"id": "req_1",
"result": {
"type": "pane_info",
"pane": {
"pane_id": "w1:p1",
"terminal_id": "term_abc123",
"workspace_id": "w1",
"tab_id": "w1:t1",
"focused": true,
"agent_status": "working",
"revision": 42
}
}
}

server.agent_manifests returns the active agent detection manifest sources and remote update diagnostics without reloading rules:

{
"id": "req_1",
"result": {
"type": "agent_manifest_status",
"last_check_unix": 1781043522,
"last_result": "checked",
"manifests": [
{
"agent": "cursor",
"source": "/home/me/.config/nagi/agent-detection/cursor.toml",
"source_kind": "local override",
"active_version": "2026.06.10.1",
"cached_remote_version": "2026.06.10.1",
"local_override_shadowing_remote": true,
"remote_update_result": "current"
}
]
}
}

Fields such as last_check_unix, last_result, active_version, cached_remote_version, remote_update_result, remote_update_error, remote_last_checked_unix, and warning are omitted when not available. server.reload_agent_manifests returns agent_manifest_reload with the same manifests item shape after reloading the in-memory rule cache.

agent.explain evaluates the target pane’s detection snapshot in the running server using the server’s active manifest cache:

{
"id": "req_2",
"method": "agent.explain",
"params": { "target": "w1:p1" }
}

The response contains the same explain object printed by nagi agent explain --json, including the final state, manifest source and version, matched rule, evaluated rule evidence, skip-state reason, idle fallback reason, and screen_detection_skip_reason when a full lifecycle hook authority makes screen rules non-authoritative.

Clients need a running server that supports agent.explain; after upgrading Nagi, restart or live-handoff the server before relying on this method.

Errors look like this:

{
"id": "req_1",
"error": {
"code": "not_found",
"message": "pane not found"
}
}

Nagi has a protocol version for client/server compatibility. Protocol changes are reviewed with release compatibility in mind.

Check the server protocol with ping or nagi status before depending on new behavior. Handle unknown fields gracefully.