Skip to content

Open API (Terminal I/O) ​

The Open API is a set of HTTP / WebSocket interfaces for reading terminal screens and scrollback, writing raw input to a pane, resizing panes, executing commands synchronously, and subscribing to events. It does not create or destroy tabs/panes - that's the Tabs & Panes API's job.

Table of Contents ​


Overview ​

OperationEndpointDescription
List sessionsGET /api/sessionsMetadata for all PTY sessions + active_pane_id
Read screenGET /api/sessions/:pane_id/screenCurrent visible area (plain / ansi)
Read scrollbackGET /api/sessions/:pane_id/scrollbackLast N lines of history
Write input (raw bytes)POST /api/sessions/:pane_id/inputInject bytes into a specific pane
ResizePOST /api/sessions/:pane_id/resizeChange PTY cols/rows
Synchronous execPOST /api/sessions/:pane_id/runSend command and wait for exit_code + stdout
Async sendPOST /api/sessions/:pane_id/sendSend command + \n, fire-and-forget
Structured readGET /api/sessions/:pane_id/readScreen content + cursor + cwd in one call
Write to active panePOST /api/inputFalls back to active pane when pane_id is omitted
Event streamWS /ws/eventsWebSocket for command execution + event subscription

input vs send: input writes raw bytes (no trailing newline, no audit); send appends \n and writes an audit log entry (agent token only). Use input for simple scripts, send for command-level automation.

screen/scrollback vs read: screen/scrollback return plain/ansi text; read returns structured JSON (with cursor/cwd/scrollback). Operator scenarios use the former; agent scenarios use the latter.

The Open API operates on existing PTY sessions. To create new sessions, use the Tabs API.


Enable Switch ​

The Open API is disabled by default. Set open_api.enabled = true to unlock /api/sessions/* and /api/input.

bash
curl -X PUT -H "Authorization: Bearer <token>" \
     http://localhost:8999/api/settings \
     -d '{"open_api":{"enabled":true}, /* ...echo other settings back */ }'

When disabled, every Open API endpoint returns:

json
{ "error": "open_api is disabled" }

HTTP 403.


Authentication ​

The Open API uses dual-track auth:

  • Session Cookie: browser same-origin contexts (operator mode)
  • Global Bearer Token: Authorization: Bearer <global-token>, full capabilities
  • Agent Bearer Token: fine-grained token created via /api/tokens, validated by capability

run / send / read / WS /ws/events accept all three credentials; sessions / screen / scrollback / input / resize only accept session cookie or global token (no capability check).

Agent Token ​

Fine-grained token created via /api/tokens with per-permission scoping:

bash
# Create a read-only token
curl -X POST -H "Authorization: Bearer <global-token>" \
     http://localhost:8999/api/tokens \
     -d '{
       "name": "monitoring-agent",
       "capabilities": ["terminal:read"],
       "expires_in": 86400
     }'
# Returns: {"token": "dnt_...", "token_info": {...}}

Token format: dnt_<64-hex>, stored as SHA-256 hash.

Audit Log ​

When a request uses an agent token, run / send write to the audit log (~/.config/dinotty/audit.log); session cookie / global token requests skip audit (operator mode = trusted UI user).


HTTP Endpoints ​

GET /api/sessions ​

List all PTY sessions.

Response (200):

json
{
  "sessions": [
    {
      "pane_id": "pane-1",
      "tab_id": "tab-aaa",
      "shell_type": "zsh",
      "status": "connected",
      "size": { "cols": 120, "rows": 32 },
      "cwd": "/Users/dev/project"
    },
    {
      "pane_id": "pane-2",
      "tab_id": "tab-bbb",
      "shell_type": "ssh",
      "status": "detached",
      "size": { "cols": 80, "rows": 24 },
      "cwd": null
    }
  ],
  "active_pane_id": "pane-1"
}

status values:

  • connected - PTY is alive
  • detached - PTY has exited but the layout hasn't been cleaned up yet

cwd is the working directory the server derived from shell integration (OSC 7 / synchronized probing); may be null for SSH sessions or shells without integration.


GET /api/sessions/:pane_id/screen ​

Read the current visible screen.

Query params:

ParamTypeDefaultDescription
formatstringplainplain strips ANSI; ansi preserves colors and cursor sequences

Response (200):

json
{
  "pane_id": "pane-1",
  "content": "$ ls -la\ntotal 0\ndrwxr-xr-x  2 dev  staff   64 Aug  8 10:00 .\n",
  "size": { "cols": 120, "rows": 32 }
}

content is a newline-joined string. ansi mode is good for recording / replaying terminal frames; plain is good for log scraping and regex matching.


GET /api/sessions/:pane_id/scrollback ​

Read scrollback history.

Query params:

ParamTypeDefaultDescription
linesint200 (ansi) / all (plain)Last N lines to return
formatstringplainplain / ansi

Response (200):

json
{
  "pane_id": "pane-1",
  "lines": [
    "$ npm install",
    "added 312 packages in 4s",
    "$ npm run build",
    "> build",
    "> esbuild src/main.ts --bundle"
  ],
  "total": 1247
}

total is the scrollback buffer's full line count, useful for telling whether earlier output exists. lines is capped at 10000.


POST /api/sessions/:pane_id/input ​

Inject raw input bytes into a specific pane (no trailing newline added).

Request body:

json
{ "data": "ls -la\r" }

data is a string written to the PTY as UTF-8 bytes. Use \r for Enter, for Ctrl+C, for Ctrl+D.

Response:

  • 200 {"ok": true}
  • 404 - pane not found
  • 500 - PTY write failed (session has exited)

Note: This endpoint bypasses the Mission Control safety net. Even when MC is open, input still reaches the PTY. If you need "user-perspective" semantics (drop input while MC is open), send Input over /ws/sync instead.


POST /api/sessions/:pane_id/resize ​

Resize the pane (PTY cols × rows).

Request body:

json
{ "cols": 120, "rows": 32 }

Response:

  • 200 {"ok": true}
  • 400 - cols or rows is 0
  • 404 - pane not found
  • 500 - PTY resize failed

Resize triggers TIOCSWINSZ; programs running inside the shell receive SIGWINCH.


POST /api/sessions/:pane_id/run ​

Synchronously execute a command, waiting for completion or timeout. Relies on Shell Integration for command boundary detection.

Request body:

json
{
  "command": "ls -la",
  "cwd": "/tmp",           // optional, working directory; Windows example: "C:\\Users\\dev\\project"
  "env": {"KEY": "val"},   // optional, environment variables (not yet implemented)
  "timeout": 30000,        // optional, timeout in ms (default 300000, max 3600000)
  "strip_ansi": true       // optional, strip ANSI escapes (default true)
}

pane_id is specified in the path, not the request body.

Success response (200):

json
{
  "exit_code": 0,
  "stdout": "file1.txt\nfile2.txt\n",
  "stderr": "",
  "duration": 150,
  "pane_id": "pane-abc123",
  "method": "shell_integration"
}

method field:

ValueDescription
shell_integrationCommand completion detected via OSC 133 (most accurate)
prompt_detectionDetected via prompt pattern matching (fallback)
timeoutCommand timed out

Capability: terminal:write (agent token path)


POST /api/sessions/:pane_id/send ​

Send a command to the terminal (auto-appends \n), fire-and-forget.

Request body:

json
{
  "command": "echo hello"
}

Response (200):

json
{"ok": true, "pane_id": "pane-abc123"}

Capability: terminal:write (agent token path)


GET /api/sessions/:pane_id/read ​

Structured read of the terminal screen, including cursor / cwd / scrollback.

Query params:

ParamTypeDefaultDescription
scrollbackintnoneReturn last N history lines (max 10000)
strip_ansibooltrueStrip ANSI escapes

Response (200):

json
{
  "pane_id": "pane-abc123",
  "lines": ["$ ls -la", "total 0", "drwxr-xr-x  ..."],
  "scrollback": ["previous command output..."],
  "cursor": {"row": 5, "col": 12},
  "cwd": "/Users/dev/project"
}

Capability: terminal:read (agent token path)


POST /api/input ​

Inject input into the active pane. Equivalent to POST /api/sessions/:pane_id/input but the caller doesn't have to track the active pane.

Request body:

json
{
  "pane_id": "pane-1",
  "data": "echo hi\r"
}
FieldRequiredDescription
datayesBytes to inject
pane_idnoTarget pane; falls back to active_pane_id if omitted, then to the first session if active is empty

Response:

  • 200 {"ok": true}
  • 400 - no session available
  • 404 - specified pane_id does not exist

WebSocket ​

Connect: ws://localhost:8999/ws/events

Auth: Bearer header or ?token=<agent_token> query param (use query param when browser WS clients can't set headers). Session cookie path is not supported (WS route is in the outer auth exempt list).

Client message format ​

Execute command:

json
{
  "type": "run",
  "id": "req-1",           // request ID for matching responses
  "command": "npm test",
  "timeout": 60000
}

Subscribe to events (all events are auto-subscribed):

json
{"type": "subscribe"}

Heartbeat:

json
{"type": "ping"}

Server message format ​

Command result:

json
{
  "type": "result",
  "id": "req-1",
  "exit_code": 0,
  "stdout": "All tests passed\n",
  "stderr": "",
  "duration": 3200,
  "pane_id": "pane-abc123",
  "method": "shell_integration"
}

Event push:

json
{
  "type": "event",
  "event": {
    "event": "command_finished",
    "data": {
      "pane_id": "pane-abc123",
      "command": "",
      "exit_code": 0,
      "duration_ms": 150,
      "stdout": "",
      "method": "shell_integration"
    }
  }
}

Error:

json
{
  "type": "error",
  "id": "req-1",
  "error": {"code": "NOT_FOUND", "message": "No active session"}
}

Heartbeat response:

json
{"type": "pong"}

Concurrency Control ​

  • Each token allows up to 10 concurrent run requests
  • Exceeding returns 429 Too Many Requests with a Retry-After: 5 header
  • send and read are not concurrency-limited

Shell Integration ​

The run endpoint relies on the OSC 133 Shell Integration protocol for command boundary detection, falling back to prompt detection when the shell doesn't fully support it:

ESC ] 133 ; A ESC \    -> Prompt start
ESC ] 133 ; B ESC \    -> Command start (user pressed Enter)
ESC ] 133 ; D ; N ESC \ -> Command finished, N is the exit code

Dinotty auto-injects or enables the integration based on the local shell:

  • zsh: injects OSC 133 via precmd_functions and preexec_functions hooks
  • bash: injects OSC 133 via PROMPT_COMMAND and BASH_ENV trap
  • PowerShell / pwsh (Windows): injects a prompt function at startup for window title, cwd, and prompt boundary sync; command completion may still use prompt_detection fallback
  • cmd.exe / sh / other shells: OSC 133 not guaranteed; auto-falls back to prompt detection

On Windows, the command field is sent to the actual shell of the pane; PowerShell accepts Get-ChildItem, cmd accepts dir. Windows paths in JSON strings need to be written as C:\\Users\\dev\\project.


Capability Requirements ​

Only the agent token path requires capability checks; session cookie / global token pass automatically.

OperationRequired Capability
POST /api/sessions/:pane_id/runterminal:write
POST /api/sessions/:pane_id/sendterminal:write
GET /api/sessions/:pane_id/readterminal:read
WS /ws/eventsnone (subscribe-only)

Error Format ​

json
{ "error": "pane not found" }
HTTPMeaning
400Invalid params (cols/rows=0, no active pane)
401Unauthenticated
403open_api.enabled = false or token lacks capability
404Pane not found
429Concurrent run limit exceeded
500PTY write / resize failed

Released under the MIT license