DeviceTerm Integration Guide

August 10, 2026 ยท View on GitHub

This guide is the machine contract for the deviceterm CLI: the public JSON shapes, authorization scopes, completion semantics, and stability promises. Most users can continue using xcrun simctl and xcrun devicectl without consuming these contracts.

Use deviceterm help for command syntax and deviceterm agents for in-terminal automation notes. Workflows live in the companion guides: AUTOMATION.md for workspace control, orchestration, and events, and USAGE.md for driving devices inside a tab.

Contents

Contract Rules

Select Machine-Readable Output

Pass --json to commands that normally provide human-readable lists, reports, or receipts:

deviceterm panes list --json
deviceterm tap 0.5 0.5 --json
deviceterm doctor --json

The flag is global and may appear before or after a command's operands.

These commands always emit JSON, with or without --json:

  • ax tree
  • ax point
  • ax sweep
  • events, as one JSON object per line

These commands have no DeviceTerm JSON output:

  • help
  • agents
  • completions install
  • with-pane

with-pane inherits its child process's stdout and stderr. The child may emit JSON independently.

Read Stdout, Stderr, and Exit Status Separately

Successful JSON goes to stdout with a trailing newline. Errors remain human-readable text on stderr, even when you pass --json.

DeviceTerm does not emit JSON error envelopes. Check the process exit status before decoding stdout:

if report=$(deviceterm doctor --json); then
  printf '%s\n' "$report" | jq '.checks'
else
  status=$?
  printf '%s\n' "$report" | jq '.checks' 2>/dev/null || true
  exit "$status"
fi

Most command, usage, transport, and daemon failures exit with status 1. Special cases are:

CommandExit Behavior
doctor0 when no check has status: "fail"; otherwise 1
events0 when the daemon closes the stream normally
with-paneChild exit status, or 128 + signal when signaled
with-pane spawn failure127

Exact error sentences are diagnostic prose. Do not parse them as a structured contract.

Handle Objects and Optional Fields Defensively

JSON object order is not significant. DeviceTerm currently sorts many locally encoded receipt keys for deterministic tests, but integrations must not depend on key order.

A missing optional value normally omits its key instead of emitting null:

{"ok":true,"paneId":"P1","udid":"U1","x":0.5,"y":0.5}

This receipt has no shortId key. Test key presence instead of comparing with null:

deviceterm tap 0.5 0.5 --json | jq 'has("shortId")'

Ignore unknown keys and tolerate unknown enum values. Compatible releases may add optional fields, event types, capability flags, or enum cases.

Apply the Stability Categories

The public CLI, JSON output, and exit behavior follow DeviceTerm's release version.

During 0.x, a minor release may introduce breaking public changes. Patch releases remain compatible within the same minor series. Starting with 1.0, breaking public changes require a new major release.

This guide uses three stability categories:

CategoryPromise
StableExisting names, types, and semantics change only through the applicable SemVer boundary
Stable-additiveThe existing shape has the stable promise, but compatible releases may add optional fields or enum values
Best-effortThe value depends on Apple frameworks, runtime behavior, or diagnostic prose and may change without defining a DeviceTerm JSON compatibility boundary

DeviceTerm-owned JSON receipts, payloads, and wrapper fields are stable-additive unless this guide says otherwise. Human output, diagnostic details, and Apple-sourced accessibility nodes are best-effort.

Separate Release and Wire Versions

deviceterm version reports one public release version and two internal wire versions:

  • deviceterm is the public release version governing the CLI and JSON contract.
  • daemon is the live daemon's internal wire version.
  • rpcWire is the internal wire version expected by the bundled CLI.

Compare daemon with rpcWire when diagnosing an interrupted update. The public deviceterm version does not need to equal either internal value.

DaemonProtocolInfo.wireVersion coordinates the bundled app, daemon, CLI, and shim. It is not an end-user compatibility version and must not select your JSON decoder.

Distinguish Identifiers

DeviceTerm exposes several identifier layers:

FieldMeaning
sessionIdDaemon session UUID. Each terminal session has one, so a split tab can have several
paneIdUUID for one device pane
shortIdShort display and reference handle. Optional during version skew
nameOptional human-assigned or creation-time name. It may be absent or ambiguous
displayTitleLive GUI title. It is display metadata, not an identifier
udid in a paneThe pane's device key: a Simulator UDID or physical CoreDevice device ID
id in the device rosterA Simulator UDID or physical CoreDevice device ID

Workspace receipts generally echo the reference supplied by the caller, such as "current" or "abc123". They do not promise to replace it with a resolved UUID.

Respect Authorization Scope

The CLI reads session credentials from the DeviceTerm tab environment. Do not add credential flags or pass DEVICETERM_SESSION_CAP as a command operand.

The capability is only one authentication factor. The daemon also checks that the caller belongs to the terminal bound to that session and repeats the provenance check for scoped requests.

This guide uses four scope labels:

ScopeRequirement
LocalNo daemon connection
Daemon-wideDaemon connection, but no authenticated tab required
SessionLive authenticated DeviceTerm terminal session
OrchestrationSession scope plus a live orchestration grant issued by the GUI

A role such as "orchestrator" is descriptive metadata. Cross-tab input and capture require a live grant, not the role string alone. Only the GUI issues a grant; see AUTOMATION.md.

Private sessions remain opaque to other callers. Lists omit private sessions, panes, and ownership annotations unless the caller owns that private tab.

Surface Matrix

CommandMachine OutputScopeCompletion MeaningStability
tabs list --jsonArray of tab rowsDaemon-wideCurrent visible session snapshotStable-additive
tabs current --jsonOne tab rowCurrent session environmentMatching live session foundStable-additive
panes list --jsonArray of pane rowsSessionCurrent panes owned by the sessionStable-additive
devices list --jsonArray of device roster rowsSessionCurrent owned-Simulator and connected-device snapshotStable-additive
windows list --jsonArray of window rowsDaemon-wideCurrent caller-visible window projectionStable-additive
tab info --jsonTab information objectSessionCurrent GUI workspace snapshotStable-additive
pane info --jsonSimulator pane information objectSessionCurrent GUI workspace snapshotStable-additive
doctor --jsonDoctor reportNone required; session fields are conditionalChecks completedStable-additive except diagnostic prose
version --jsonVersion reportLocal, with optional daemon probeLocal report completedStable-additive
dump-config --jsonConfiguration reportLocalConfiguration file parsedStable-additive
tap, swipe, app-switcher, long-press, pinch with --jsonInput receiptSessionDaemon completed the input dispatch callStable-additive
button, key, text, rotate, crown with --jsonInput receiptSessionDaemon completed the input dispatch callStable-additive
tab open, tab close, tab rename, tab select, tab move with --jsonWorkspace receiptSessionGUI returned success for the requested mutationStable-additive
pane open --terminal, pane close with --jsonWorkspace receiptSessionGUI returned success for the requested mutationStable-additive
device attach --jsonDevice attachment receiptSessionGUI accepted the attachment; rendering may still be pendingStable-additive
window open, window close, window focus with --jsonWorkspace receiptSessionGUI returned success for the requested mutationStable-additive
tab set-private --jsonPrivacy receiptSession and tab ownershipReports whether the requested state was confirmedStable-additive
tab send-input --jsonInput receiptOrchestrationInstant input was dispatched; positively paced typing was enqueued and may still be runningStable-additive
tab capture --json{text}OrchestrationVisible viewport capturedStable-additive
ax tree, ax pointDeviceTerm wrapper containing an Apple accessibility nodeSessionAccessibility query completedStable-additive wrapper; best-effort node
ax sweepDeviceTerm sweep wrapper containing Apple nodesSessionSweep completedStable-additive wrapper; best-effort children
eventsJSON Lines streamSessionSubscription remains active until EOF or terminationStable-additive
with-paneChild-owned stdout and stderrSessionChild process exitedStable exit forwarding
pane rename, pane moveNo success shapeSessionUnsupported; command failsStable unsupported status
help, agentsProseLocal, with optional daemon discoveryDocumentation printedNot a JSON contract
completions installProse and a written completion fileLocalCompletion file installedNot a JSON contract

Discovery and State

Version Report

Run:

deviceterm version --json

Shape:

{
  "deviceterm": "0.1.0",
  "daemon": "0.1.0",  // optional; omitted when the live probe fails
  "rpcWire": "0.1.0",
  "macOS": "26.4.1"
}

All fields except daemon are required.

For update diagnosis:

report=$(deviceterm version --json)

if ! printf '%s\n' "$report" | jq -e 'has("daemon")' >/dev/null; then
  printf 'daemon version probe did not succeed\n' >&2
  exit 1
fi

if ! printf '%s\n' "$report" | jq -e '.daemon == .rpcWire' >/dev/null; then
  printf 'daemon and bundled RPC wire versions differ\n' >&2
  exit 1
fi

A missing daemon field means the live version probe did not succeed. It does not distinguish an unreachable daemon from authentication, transport, RPC, or decoding failure. Handle that case separately from a present mismatch between daemon and rpcWire, which indicates an interrupted or incomplete update.

Configuration Report

Run:

deviceterm dump-config --json

Shape:

{
  "entries": [
    {
      "key": "auto-update",
      "value": "check",
      "source": "default"
    },
    {
      "key": "quit-with-sims-default",
      "value": "",
      "source": "unset",
      "note": "the Quit prompt is shown; set keep or shutdown to suppress it"
    },
    {
      "key": "tab-close-default",
      "value": "shutdown",
      "source": "file"
    }
  ],
  "warnings": [
    "unknown config key: typo-here"
  ]
}

entries and warnings are always present. Each entry has:

FieldTypeMeaning
keystringRecognized DeviceTerm configuration key
valuestringEffective value; empty when source is "unset"
sourcestring"default", "file", or "unset"
notestring, optionalPresent only on "unset" entries; what the app does while the key is absent

The two prompt-suppression keys (tab-close-default, quit-with-sims-default) apply no default when absent: the app shows the prompt. Their absent entries report source: "unset" with an empty value and an explanatory note rather than claiming the documented default is effective. Keys whose default genuinely applies on absence (simulator-app-advisory, auto-update) keep reporting source: "default".

Entries are sorted by key. Warning text is diagnostic prose and best-effort.

Tab Rows

tabs list --json returns an array. tabs current --json returns one object with the same row shape:

{
  "current": true,
  "sessionId": "550E8400-E29B-41D4-A716-446655440000",
  "shortId": "abc123",               // optional
  "name": "auth-feature",            // optional
  "displayTitle": "vim Login.swift", // optional
  "label": "agent"                   // optional
}

current and sessionId are required. Other keys are omitted when unavailable.

tabs list returns one row per live terminal session, not one row per GUI tab. A tab with split terminal panes can produce several rows.

Public sessions are visible to every caller. A private session is visible only to its owner. An out-of-tab caller sees public sessions and every row has current: false.

name is creation-time session metadata. tab rename changes the GUI title but does not mutate this field.

displayTitle is the GUI's current normalized title for the primary terminal session. It may disappear after a daemon restart until the GUI republishes it. Do not use it as a --tab reference.

Pane Rows

panes list --json returns the current session's device panes:

{
  "paneId": "F3A61C00-3F4B-44F0-8898-18544176A338",
  "udid": "A1B2C3D4-E5F6-47A8-9B0C-D1E2F3A4B5C6",
  "state": "rendering",
  "family": "phone",
  "shortId": "phn001",
  "name": "Primary Phone",
  "capabilities": {
    "touch": true,
    "key": true,
    "text": true,
    "button": true,
    "rotate": true,
    "crown": true,
    "accessibility": true,
    "location": true
  },
  "target": {
    "sim": {
      "udid": "A1B2C3D4-E5F6-47A8-9B0C-D1E2F3A4B5C6"
    }
  }
}

Required fields:

FieldTypeMeaning
paneIdstringPane UUID
udidstringDevice key for either backend
statestringCurrent pane lifecycle

Current lifecycle values are:

  • "booting"
  • "rendering"
  • "shutdown"
  • "failed"

Optional fields:

FieldMeaning
family"watch", "phone", "pad", "tv", "unknown", or a future value
shortIdShort pane reference
nameOptional pane name
capabilitiesPer-pane supported operation families
targetBackend-neutral device discriminator

A physical-device target uses:

{
  "device": {
    "deviceId": "00008130-001C195E0E91802E"
  }
}

A current daemon emits capabilities and target. Their optional encoding permits an older daemon to remain decodable during an update.

If target is absent, treat the pane as a Simulator for compatibility with older releases. If a new capability flag is absent from a present capabilities object, treat that capability as unsupported.

Device Roster Rows

devices list --json returns owned booted Simulators and connected physical devices:

{
  "id": "00008130-001C195E0E91802E",
  "kind": "device",
  "name": "Development iPhone",
  "model": "iPhone 17 Pro",
  "osVersion": "27.0",
  "state": "connected",
  "attached": true,
  "ownerSessionId": "550E8400-E29B-41D4-A716-446655440000"
}

Fields:

FieldTypeMeaning
idstringSimulator UDID or physical-device ID
kindstring"sim" or "device"
namestring, optionalHuman-readable device name
modelstring, optionalPhysical-device hardware model
osVersionstring, optionalPhysical-device OS version
statestring, optionalCoreSimulator state or "connected"
attachedbooleanWhether a pane visible to the caller mirrors it
ownerSessionIdstring, optionalVisible owner session

The roster is not a replacement for simctl list or devicectl list. It excludes shutdown and never-booted Simulators, and externally booted Simulators remain absent until DeviceTerm claims them.

When another caller owns a private attachment, the entry reports attached: false and omits ownerSessionId. This is intentionally indistinguishable from an unattached device.

Window Rows

windows list --json returns:

[
  {
    "index": 1,
    "isKey": true,
    "tabCount": 3,
    "selectedTabShortId": "abc123"
  }
]

index, isKey, and tabCount are required. selectedTabShortId is omitted when unavailable.

Without --all, an in-tab caller receives its own window. An out-of-tab caller receives an empty array.

With --all, DeviceTerm returns the caller-visible window projection. Windows containing only foreign private tabs are omitted, and indices and counts are computed after that filtering.

Tab Information

tab info --json returns:

{
  "sessionId": "550E8400-E29B-41D4-A716-446655440000",
  "shortId": "abc123",
  "name": "auth-feature",
  "role": "agent",
  "isCurrent": true,
  "simPanes": [
    {
      "paneId": "F3A61C00-3F4B-44F0-8898-18544176A338",
      "udid": "A1B2C3D4-E5F6-47A8-9B0C-D1E2F3A4B5C6",
      "shortId": "phn001",
      "displayName": "iPhone 17 Pro",
      "family": "phone"
    }
  ]
}

Required top-level fields are sessionId, role, isCurrent, and simPanes. The other fields are optional. cwd and label are reserved optional fields; the current GUI omits them.

sessionId, shortId, and name describe the GUI tab's primary terminal session. isCurrent is true when the calling terminal belongs to the resolved tab, including a non-primary split terminal.

simPanes contains Simulator panes only. It does not enumerate physical-device panes. Use panes list --json for the backend-neutral pane roster.

Pane Information

pane info --json returns:

{
  "paneId": "F3A61C00-3F4B-44F0-8898-18544176A338",
  "udid": "A1B2C3D4-E5F6-47A8-9B0C-D1E2F3A4B5C6",
  "shortId": "phn001",
  "name": "Primary Phone",
  "displayName": "iPhone 17 Pro",
  "family": "phone",
  "linkedSessionId": "550E8400-E29B-41D4-A716-446655440000"
}

shortId and name are optional. Every other field is required.

pane info currently resolves Simulator panes only. Use panes list --json to inspect physical-device panes.

linkedSessionId is the primary terminal session of the containing tab.

Doctor Report

Run:

deviceterm doctor --json

Shape:

{
  "ok": true,
  "checks": [
    {
      "name": "Daemon socket",
      "status": "ok",
      "detail": "/path/to/deviceterm.sock"
    }
  ],
  "session": {
    "sessionId": "550E8400-E29B-41D4-A716-446655440000",
    "shortId": "abc123",
    "name": "auth-feature"
  },
  "targets": [],
  "role": "agent",
  "allowedMethods": [
    "daemon.events",
    "pane.input.tap",
    "panes.list"
  ]
}

ok and checks are always present. ok is true when no check has status: "fail". Warnings do not make the command fail.

Optional fields:

FieldPresence
sessionLive session identity was resolved
targetsSession authentication reached panes.list; may be an empty array
roleLive daemon or DEVICETERM_SESSION_ROLE environment fallback supplied a role
allowedMethodsDaemon capabilities query succeeded

targets contains PanesListEntry objects from panes list --json. It represents linked device panes, including Simulator and physical-device panes.

Each check has:

{
  "name": "Session authenticates (cap + provenance)",
  "status": "ok",
  "detail": "panes.list accepted"
}

Current status values are "ok", "warn", and "fail". Branch on status; display detail without parsing it.

Current check names are:

NameMeaning
DEVICETERM_SESSIONSession environment value and UUID shape
DEVICETERM_SESSION_CAPCapability environment presence
DEVICETERM_DAEMON_SOCKDaemon socket environment path
DEVICETERM_SHIM_DIRPer-session shim directory
xcrun resolves to shimWhether xcrun resolves through the DeviceTerm shim
Daemon socketDaemon socket reachability
Daemon pingDaemon handshake and wire version
Session live in daemonSession appears in tabs.list
Session authenticates (cap + provenance)Session credentials and terminal provenance authenticate

Check names and detail text are best-effort diagnostics. Pin the DeviceTerm release if an integration must branch on a check name.

When the daemon socket is unreachable:

  • ok is false.
  • Daemon socket has status: "fail".
  • Daemon ping is absent because the ping is not attempted.
  • allowedMethods is omitted.
  • session and targets are omitted.
  • role may still appear from the tab environment.

Treat a role with a failed socket check as the caller's intended role, not proof that its methods are currently available.

Action Receipts

A successful action receipt contains "ok": true. A failed action prints an error to stderr, exits nonzero, and emits no success object.

Input receipts identify the resolved pane. Workspace receipts usually echo the caller's unresolved reference.

Input Receipts

Every input receipt begins with:

{
  "ok": true,
  "udid": "A1B2C3D4-E5F6-47A8-9B0C-D1E2F3A4B5C6",
  "paneId": "F3A61C00-3F4B-44F0-8898-18544176A338",
  "shortId": "phn001"
}

shortId is optional. The other fields are required.

The command adds these fields:

CommandAdditional Fields
tapx, y
app-switcherx, y, containing the fixed gesture start at 0.5, 0.99
swipedispatched?, steps?, durationMs?
long-pressx, y, durationMs?
pinchdurationMs?
buttonbutton
keykeyCode, down
textbytes
rotateorientation
crowndelta, velocity?, durationMs?

Example tap receipt:

{
  "ok": true,
  "paneId": "F3A61C00-3F4B-44F0-8898-18544176A338",
  "shortId": "phn001",
  "udid": "A1B2C3D4-E5F6-47A8-9B0C-D1E2F3A4B5C6",
  "x": 0.5,
  "y": 0.5
}

Example swipe receipt:

{
  "dispatched": "drag",
  "durationMs": 250,
  "ok": true,
  "paneId": "F3A61C00-3F4B-44F0-8898-18544176A338",
  "shortId": "phn001",
  "steps": 15,
  "udid": "A1B2C3D4-E5F6-47A8-9B0C-D1E2F3A4B5C6"
}

dispatched is "tap" when a sub-frame swipe collapses to a tap-shaped dispatch, or "drag" when interpolation occurs. The three swipe acknowledgment fields may all be absent when a newer CLI talks to an older daemon.

keyCode is a hexadecimal string such as "0x30". orientation uses camel-case wire values such as "landscapeLeft" even though CLI input accepts kebab-case.

text.bytes reports the UTF-8 byte count and never echoes the typed content. crown.velocity echoes a supplied value even though the daemon currently ignores that option; the key is omitted when the option is absent.

An input receipt confirms that the daemon completed its dispatch call. It does not confirm that the target application handled the input or that the screen changed.

Workspace Receipts

Workspace receipts echo the requested tab, pane, or window reference. "current" remains "current".

CommandSuccess Shape
tab open{ok, window}
tab close{ok, tab, mode}
tab rename{ok, tab, name?}
tab select{ok, tab}
tab move{ok, tab, toIndex?, toWindow?}
pane open --terminal{ok, tab}
pane close{ok, pane, mode}
device attach{ok, target, kind}
window open{ok}
window close{ok, window, mode}
window focus{ok, window}
tab send-input{ok, tab, bytes, typeDelayMillis?}
tab set-private{ok, tab, isPrivate, committed}

Optional destination or name keys are omitted when the corresponding option is absent.

Example tab move:

{
  "ok": true,
  "tab": "abc123",
  "toIndex": 1,
  "toWindow": "2"
}

Example device attachment:

{
  "kind": "device",
  "ok": true,
  "target": "00008130-001C195E0E91802E"
}

The attachment receipt confirms that the GUI accepted the attachment request. It does not include the new paneId or confirm that a display stream is rendering. Observe panes list --json or events when readiness matters.

tab open does not return the new session ID. pane open --terminal does not return the new terminal session ID, and window open does not return a window identifier.

Unsupported Workspace Verbs

pane rename and pane move are present in the command catalog but are not implemented. They fail with intent.internalError.

Do not decode or depend on the dormant PaneRename or PaneMove receipt structs. No public success shape exists for these commands.

Accessibility

Accessibility commands always emit JSON and require session scope. They support Simulator panes only; see the physical-device limits in USAGE.md.

Apple Node Dictionaries

DeviceTerm wraps each accessibility result under a command-specific top-level key. Read an ax tree node through .tree and an ax point node through .element. ax sweep also uses .tree because its synthetic root has the same recursive shape as an accessibility tree.

ax tree returns:

{
  "tree": {
    "role": "Button",
    "label": "Continue",
    "identifier": "continue-button",
    "subrole": "AXCloseButton",
    "value": "Continue",
    "frame": {
      "x": 20,
      "y": 80,
      "w": 120,
      "h": 44
    },
    "children": []
  }
}

ax point returns one node without a children array:

{
  "element": {
    "role": "Button",
    "label": "Continue",
    "frame": {
      "x": 20,
      "y": 80,
      "w": 120,
      "h": 44
    }
  }
}

The top-level tree and element keys are DeviceTerm-owned and stable-additive. Current nested node fields are:

  • role
  • label, optional
  • identifier, optional
  • subrole, optional
  • value, optional
  • frame with x, y, w, and h
  • children on tree nodes

These dictionaries derive from private Apple accessibility frameworks. Their roles, values, nesting, availability, and field behavior are best-effort. Do not treat the nested node schema as a DeviceTerm-owned compatibility contract.

On watchOS, ax tree can return an empty children array even when elements are visible. The object under tree may include a diagnostic note directing you to ax sweep or ax point.

Sweep Wrapper

ax sweep samples the screen with point queries, removes duplicate elements, and returns a DeviceTerm-owned wrapper:

{
  "tree": {
    "role": "AXSweepRoot",
    "frame": {
      "x": 0,
      "y": 0,
      "w": 1,
      "h": 1
    },
    "children": [],
    "step": 0.04,
    "sweepedPoints": 625
  }
}

The synthetic object under tree has these stable-additive fields:

FieldTypeMeaning
rolestringAlways "AXSweepRoot"
frameobjectNormalized full-screen frame
childrenarrayUnique Apple accessibility nodes
stepnumberClamped step used by the sweep
sweepedPointsintegerNumber of sampled grid points

The objects inside children remain best-effort Apple node dictionaries.

A successful empty children array means the bridge responded but the sweep found no unique elements. A systemic bridge failure exits nonzero and prints an error instead of returning a successful empty wrapper.

Orchestration

Hold a Live Grant

Cross-tab terminal input and capture require a live orchestration grant, checked for each request. A caller without one receives error.role_violation, including a caller whose environment still says its role is "orchestrator".

Only the GUI issues a grant, and the CLI cannot grant authority to itself. The grant lifecycle is described in AUTOMATION.md.

Send Input

Run:

deviceterm tab send-input --tab abc123 'make test\n' --json

Receipt:

{
  "bytes": 10,
  "ok": true,
  "tab": "abc123"
}

With paced typing:

{
  "bytes": 10,
  "ok": true,
  "tab": "abc123",
  "typeDelayMillis": 40
}

The receipt reports UTF-8 bytes and never includes the text.

A successful receipt means DeviceTerm dispatched instant input synchronously. For a positive typeDelayMillis, it means paced typing was enqueued and may still be running. Neither result confirms that the target shell executed the command.

Concurrent paced calls to the same tab are typed in order. The CLI caps typeDelayMillis at 1000.

Capture a Viewport

Run:

deviceterm tab capture --tab abc123 --json

Shape:

{
  "text": "visible terminal contents\n"
}

The capture contains the currently visible terminal viewport. It does not include scrollback.

The JSON string preserves the captured content. DeviceTerm adds only the newline that terminates the outer JSON document.

Set Privacy

Run:

deviceterm tab set-private true --json

Confirmed result:

{
  "committed": true,
  "isPrivate": true,
  "ok": true,
  "tab": "current"
}

Unconfirmed result:

{
  "committed": false,
  "isPrivate": true,
  "ok": true,
  "tab": "current"
}

committed: true means the daemon confirmed the requested state. committed: false means the transition remains unconfirmed and the GUI may still be converging.

A definite rejection is a command failure, not a receipt with committed: false.

Privacy behavior, including what other callers can no longer see or do, is described in AUTOMATION.md.

Run a Child With a Pane Target

with-pane resolves a device pane, sets DEVICETERM_TARGET_PANE to that pane's device key, and runs the child with inherited standard streams:

deviceterm with-pane phn001 sh -c \
  'deviceterm tap 0.5 0.5 --json'

with-pane emits no receipt of its own. The child's stdout and stderr pass through unchanged.

The wrapper returns the child's exit status. If a signal terminates the child, it returns 128 + signal; a spawn failure returns 127.

Events

deviceterm events subscribes to the current session's event stream. It emits one JSON object per line until the session closes, the daemon exits, the connection fails, or the process is terminated.

Output is always JSON Lines:

deviceterm events \
  | jq --unbuffered 'select(.type == "pane.stateChanged")'

Recipes that combine the stream with state polling, and the subscribe-before-triggering rule they follow, are in AUTOMATION.md.

Event Shape

Every event has type and ts. Per-type optional fields are omitted when unused:

{
  "paneId": "F3A61C00-3F4B-44F0-8898-18544176A338",
  "state": "rendering",
  "ts": "2026-08-08T15:30:12.123Z",
  "type": "pane.stateChanged",
  "udid": "A1B2C3D4-E5F6-47A8-9B0C-D1E2F3A4B5C6"
}

ts is an ISO 8601 UTC timestamp.

Current event types are:

typeAdditional FieldsMeaning
pane.stateChangedpaneId, udid, statePane entered booting, rendering, shutdown, or failed
device.bootedudidSimulator boot was observed
device.shutdownudidSimulator shutdown was observed
session.createdsessionId, shortId, name?Session became live
session.closedsessionIdSession closed

For pane.stateChanged, udid is the pane's target key. It can therefore contain a Simulator UDID or physical-device ID.

New event types may be added compatibly. Ignore unknown type and state values.

Delivery and Ordering

A subscription receives events in the order the daemon's event broker publishes them. Publishers serialize through the broker, and each subscriber receives that order through its stream buffer.

The broker does not retry delivery. Device lifecycle sources are debounced where they report the same observed transition, but integrations should still make state changes idempotent instead of treating the stream as a transaction log.

A slow consumer does not block publishers. The daemon uses an unbounded in-memory stream buffer, so consumers should continue draining the stream rather than leaving it unread.

Loss and Restart Behavior

The stream has no replay or durable journal, and the CLI emits no public readiness record. Events published before a subscription is established are not delivered later.

Events can be lost when:

  • the subscriber is not yet connected;
  • the deviceterm events process exits or is killed;
  • the daemon restarts;
  • the session closes; or
  • the socket connection fails.

A daemon restart closes the stream, and the CLI exits successfully on EOF. Events published during the restart window are unavailable.

Use list commands as the source of current truth. Use events as a low-latency notification that tells you when to refresh that state.

Scope and Privacy

The stream requires an authenticated, live DeviceTerm tab session.

A session-scoped subscriber receives:

  • pane.stateChanged for panes owned by that session;
  • its own session.closed as the final session event;
  • its own session lifecycle events when they occur after subscription; and
  • every Simulator device.booted and device.shutdown event.

A CLI subscriber normally cannot observe its own session.created event because the session exists before its shell can start the subscription. It never receives another session's private lifecycle events.

The validated GUI uses a separate privileged subscription and can observe every session. CLI callers cannot request that scope.

Events Not Included

pane.surfaceChanged is not part of deviceterm events. Surface rotation belongs to the GUI's private per-pane rendering subscription.

device.booted and device.shutdown include no device name, runtime, or model. For an owned or attached Simulator, refresh devices list --json or panes list --json when you need metadata. An external, unclaimed Simulator can emit either event while remaining absent from both lists; use xcrun simctl list devices --json as the fallback metadata source for its UDID.

session.closed includes no close mode. The GUI does not publish whether the tab detached or shut down its Simulators.

session.created.name is the optional creation-time session name. tab rename changes the GUI title and does not mutate that field.