CLI: Command-Line Interface

Nexion ships with a built-in CLI tool (nexion) that lets you control the app programmatically from any terminal. Manage tabs, send commands to terminals, automate browser actions, stream events, and more: all via a fast Unix socket connection.

Installation

The CLI binary is bundled inside Nexion.app. To install it system-wide:

$ nexion install-cli
Installed: /usr/local/bin/nexion -> /Applications/Nexion.app/Contents/MacOS/nexion-cli

This creates a symlink at /usr/local/bin/nexion pointing to the binary inside the app bundle. You can also run it directly:

$ /Applications/Nexion.app/Contents/MacOS/nexion-cli --help

Architecture

The CLI communicates with the running Nexion app over a Unix domain socket using the JSON-RPC 2.0 protocol with length-prefixed framing.

  • • Socket: ~/.nexion/nexion.sock. User-scoped, created when Nexion starts.
  • • Authentication: Token-based. Nexion generates a random 32-byte token at startup, written to ~/.nexion/auth-token (mode 0600). The CLI reads it automatically.
  • • Framing: 4-byte big-endian length prefix + UTF-8 JSON payload. This avoids ambiguity with newline-delimited JSON when payloads contain terminal output or HTML.
  • • No dependencies: The CLI is a lightweight (~2 MB) standalone binary. It does not link GhosttyKit or any app frameworks.

Global Flags

Flag Description
--json Output raw JSON instead of formatted tables
--plain Output plain TSV (no colors, pipe-friendly)
--socket Override the IPC socket path (default: ~/.nexion/nexion.sock)

Quick Open & Browse

Top-level shortcuts to spin up a new project tab (terminal + file tree) or a new browser tab in one command. Both are convenience wrappers over tab new and activate the new tab by default.

nexion open

Open a directory as a new project tab. The path defaults to the current working directory, so you can cd into a project and run nexion open with no arguments.

nexion open
$ nexion open                                # Open $PWD as a new project tab
$ nexion open ~/Projects/MyApp              # Open a specific directory
$ nexion open . --no-activate              # Open in background (don't switch to it)

nexion browse

Open a new browser tab. Bare hostnames are auto-prefixed with https://. Without an argument, opens the browser homepage.

nexion browse
$ nexion browse                              # New browser tab (homepage)
$ nexion browse github.com                   # https:// auto-prefixed
$ nexion browse https://nexion.one/docs      # Full URL
$ nexion browse localhost:3000 --no-activate    # Background tab

Tab Commands

Tabs are unified: both project tabs and browser tabs are managed with the same commands.

nexion tab list

List all open tabs.

nexion tab list
$ nexion tab list
#     TYPE      NAME                      PATH/URL                                  ACTIVE
0     project   MyProject                 /Users/ramin/Projects/MyProject           $
1     browser   GitHub                    https://github.com
2     project   Nexion                    /Volumes/Personal/Nexion
$ nexion tab list --type project    # Filter by type
$ nexion tab list --json              # Raw JSON output

nexion tab active

Show information about the currently active tab.

$ nexion tab active
[0] project: MyProject
    /Users/ramin/Projects/MyProject

nexion tab new

Create a new tab.

$ nexion tab new --path /path/to/project    # New project tab
$ nexion tab new --url https://example.com  # New browser tab
$ nexion tab new                             # New browser tab (homepage)

nexion tab activate

Switch to a tab by index or UUID.

$ nexion tab activate 2                     # By index (0-based)
$ nexion tab activate a1b2c3d4-...          # By UUID

nexion tab close / rename

$ nexion tab close                          # Close active tab
$ nexion tab close --id <uuid>              # Close specific tab
$ nexion tab rename "My New Name"           # Rename active tab
$ nexion tab rename "New Name" --id <uuid>  # Rename specific tab

Terminal Commands

Send text and key sequences to the terminal in the active pane.

nexion terminal
$ nexion terminal send "echo hello"         # Send text + Enter
$ nexion terminal send-keys "Ctrl-C"        # Send key sequence
$ nexion terminal cwd                       # Print working directory
$ nexion terminal cd /tmp                   # Change directory
$ nexion terminal clear                     # Clear screen
$ nexion terminal info                      # Session info: cwd, tmux, foreground job, agent state
$ nexion terminal send "make test" --pane-id <id>  # Write to one pane, whatever has focus

Every one of these acts on the focused pane of the active tab unless you name a pane with --pane-id, which you get from nexion pane list --json. Without it a script writes wherever the user happens to be standing, which is what you want when the script is driving its own terminal and a trap everywhere else. A pane id that names nothing is an error, not a quiet fall back to the pane in front.

send-keys understands Ctrl-, Alt-, Shift- and Cmd- combinations, the arrows, Tab and Shift-Tab, Home, End, PageUp, PageDown, Delete, Escape, Enter and the function keys. A name it does not know is refused rather than typed out as text.

Keep Awake

Stop the Mac from sleeping while a job runs. Nexion does this on its own while a watched job holds a terminal's foreground, but a script that knows exactly how long it needs can say so.

nexion awake
$ nexion awake status                       # Is the Mac being held awake, and why
$ nexion awake on                           # Hold until told otherwise
$ nexion awake on --for 2h                  # Hold for a while: 90s, 30m, 2h
$ nexion awake off                          # Release your hold
# Wrap a long job
$ nexion awake on --for 2h && ./overnight-build.sh; nexion awake off

awake status exits 0 when the Mac is being held awake and 1 when it is not, so if nexion awake status; then … works without parsing anything. With --json it reports active, source (manual or automatic), remainingSeconds when a timer is running, and reason naming the job responsible when Nexion decided on its own.

awake off releases your hold only. Anything Nexion is holding open on its own, because an agent or a build is still running, carries on: turning off your own request should not tell the Mac to sleep through somebody else's work.

Notify Commands

Send notifications to Nexion from any script, terminal, or AI agent. Notifications appear as a floating bubble, tab badge, native macOS notification, and optional sound.

nexion notify

Send a notification message to the current tab.

nexion notify
$ nexion notify "Build completed!" --source make
$ nexion notify "Tests passed" --source swift
# Pipe JSON from stdin (used by hook scripts)
$ echo '{"message":"Done!"}' | nexion notify --stdin-json
# Use after any long-running command
$ make build && nexion notify "Build OK" --source make

AI Agent Hooks

Nexion installs wrapper scripts at ~/.nexion/bin/ for Claude Code and Codex. These wrappers inject notification hooks so Nexion is notified when the agent finishes a task or needs your attention: no configuration required.

  • • Claude Code: Hooks into Notification and Stop lifecycle events via --settings
  • • Codex: Injected via -c notify=[...] configuration flag
  • • Other agents: Use nexion notify or the IPC method ai.notification directly

Session Context & MCP

Inside a Nexion pane, an agent also learns the workspace automatically. The Claude Code wrapper injects a session-start hook that names what the workspace can do (it re-fires after /clear and compaction, so the context never disappears mid-session), and wires up an MCP server that hands the agent typed tools.

That hook is a table of contents, not a manual. It is paid for by every session in every pane, so what stays in it is the identity of the workspace and the two things an agent would never think to ask about: how to hand you a file, and how to get your attention. Everything else lives in pages the agent pulls when it needs one, which is also the only way the detail that matters could exist at all — that simulator screenshots are pixels while its input is points, which wait condition is the useless one, what makes a canvas render empty boxes. None of that fits in a document injected into every session.

  • • nexion agent-context: prints the document injected at session start. Add --hook-json for the raw hook envelope.
  • • nexion skills list|search <question>|get <id>, and the same as a skills MCP tool: the pages themselves. Ten of them, one per part of the workspace, searchable in the words somebody would actually type. They are read-only text, so the tool executes nothing, needs no permission, and answers even when the app is not running. Plugins contribute pages under their own plugin.<id>. namespace, which is the first way a plugin’s commands are documented to an agent at all.
  • • nexion mcp: a Model Context Protocol server over stdio, exposing 50 typed mcp__nexion__* tools (attach, notify, and the simulator, browser, canvas, notes and keep-awake surface) that bridge to the running app. Registered for Claude Code, Codex, Gemini and opencode; a self-defined nexion server is left untouched. The browser and canvas tools take an optional pane_id, so an agent can work in one pane of a split rather than a whole tab; omit it when the tab holds exactly one pane of that kind.

What Agents Cannot Do

The MCP surface is narrower than the CLI on purpose. There is no tool to type into a terminal, run a command, change configuration, load a plugin, or touch git or SSH. An agent talked into misbehaving by something it read cannot make Nexion execute anything, and that is a property of the tool list rather than a promise.

Page content is the obvious way to talk an agent into something, so browser_text, browser_html and browser_find return their result wrapped and labelled as untrusted data, carrying the URL it came from. An agent that finds an instruction inside is told to report it rather than follow it.

Three tools sit outside that comfort and are off until you turn them on in Settings → Privacy → Agent tools:

  • • browser_eval runs JavaScript inside whatever session the pane is logged into.
  • • browser_history and browser_history_forget read where you have been, and delete the record of it.

While off they are not refused, they are absent from tools/list: a tool that exists and fails teaches a model to rephrase and retry, one that was never offered gives it nothing to work with. The same panel withdraws agent access altogether, which removes Nexion’s entry from the agent config files it wrote rather than merely refusing calls.

Shell Integration

Nexion installs a shell integration script at ~/.nexion/shell-integration.zsh (sourced from .zshrc). It provides automatic notifications without any manual setup:

  • • Long command completion: Commands running longer than NEXION_NOTIFY_THRESHOLD (default: 30s) emit a notification with exit status when they finish
  • • Idle detection: Commands running longer than NEXION_IDLE_THRESHOLD (default: 5s) trigger an alert: useful for sudo, python input(), and other programs waiting for user input
  • • Smart filtering: Interactive commands (vim, ssh, tmux) and fast commands (ls, cd, git) are automatically excluded

Configure thresholds in your .zshrc (before the Nexion source line):

export NEXION_NOTIFY_THRESHOLD=10   # Notify after 10s instead of 30s
export NEXION_IDLE_THRESHOLD=3     # Idle alert after 3s instead of 5s

tmux Support

Notifications work inside tmux sessions managed by Nexion. Environment variables (NEXION_PANE_ID, NEXION_SOCKET, etc.) are automatically propagated into the tmux session environment via setenv, so new panes and splits inherit them. Both the IPC notification path (Claude/Codex hooks) and the shell integration path (OSC 9) work correctly in tmux.

IPC Method: ai.notification

For direct socket integration, send a JSON-RPC request to the ai.notification method:

ai.notification
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "ai.notification",
  "params": {
    "pane_id":     "<uuid>",      // exact session match
    "project_id":  "<uuid>",      // fallback: tab UUID
    "project_path": "/path/to/project", // fallback: path prefix match
    "message":     "Task completed",
    "source":      "claude"
  }
}

Resolution priority: pane_id (exact match) → project_id (tab UUID) → project_path (prefix match). At least one must be provided.

Pane Commands

Control split panes within the active tab. The same pane commands work for terminal (project) tabs and web browser tabs: the handler dispatches by tab kind. Browser tabs add a --url seed on split and a pane move with quadrant drop zones; terminal-specific ops (zoom, layout, resize, rotate, break) stay terminal-only.

nexion pane
$ nexion pane list                                # List panes (terminal or browser)
$ nexion pane split --dir v                       # Split right (side-by-side)
$ nexion pane split --dir h                       # Split down (stacked)
$ nexion pane split --dir v --url https://github.com  # Browser: split + seed URL
$ nexion pane split --kind canvas --file notes.excalidraw  # Drawing beside the terminal
$ nexion pane split --kind web --private            # Anonymous pane beside a logged-in one
$ nexion pane close                               # Close focused pane
$ nexion pane focus --index 1                    # Focus pane by 0-based index
$ nexion pane move --target-index 2 --zone right  # Browser: move focused pane onto target&#39;s right quadrant
# Terminal-only ops
$ nexion pane zoom                                # Toggle pane zoom
$ nexion pane layout tiled                       # Apply layout preset
$ nexion pane resize right 10                     # Resize focused pane
$ nexion pane rotate                              # Rotate pane positions
$ nexion pane break                               # Break pane to new tab
$ nexion pane detach                              # Move the focused pane to a window of its own
$ nexion pane detach --index 1                   # Move a specific pane
$ nexion pane recall                              # Bring every detached pane back where it came from

pane detach moves the live pane, it does not open a second one: the shell keeps running, a page keeps the form you had half filled in, a drawing keeps its editor. Closing that window is what brings the pane home, and pane recall is the same thing from a script. A pane returns as a split beside the pane you are in now; a tab that left whole returns to its own row, in its project and its space.

pane list returns a uniform shape across tab kinds: terminal panes carry cwd, browser panes carry url, title, and profile_id, and canvas panes carry file and dirty. Terminal panes also report what they are running: foregroundJob (absent when the pane is sitting at a shell prompt), sshTarget for an ssh typed by hand, and agentState of working, waiting or idle. Web and canvas panes each cap at 4 per tab; pane split returns an explicit error when a cap is hit. A canvas pane needs a file: --kind canvas --file creates it when it is not there yet, since a drawing with nowhere to save would be a pane that loses work.

Browser Commands

Automate the built-in web browser from the command line. Every browser action defaults to the focused leaf; pass --pane (0-based DFS) to target a specific pane inside a split tab. browser state is the canonical aggregate read: it replaces the old browser url and browser title.

nexion browser
# Navigation
$ nexion browser navigate https://example.com
$ nexion browser back
$ nexion browser forward
$ nexion browser reload
$ nexion browser state                      # url, title, profile, private, error_page, camera/microphone, selection, ...
# JavaScript &amp; DOM
$ nexion browser eval "document.title"
$ nexion browser eval-file script.js
$ nexion browser click "#submit-btn"
$ nexion browser type "#search" "query text"
$ nexion browser select "article h1"
$ nexion browser wait-for ".loaded" --timeout 10
# Export
$ nexion browser screenshot --output page.png
$ nexion browser pdf --output page.pdf
$ nexion browser html                       # Print page HTML
$ nexion browser text                       # Print visible text
$ nexion browser print                      # Raise the system print panel (needs a person to confirm)
$ nexion browser source                     # Show the page source in the app viewer
# Search &amp; zoom
$ nexion browser find "search term"
$ nexion browser zoom 1.5
# Camera, microphone &amp; inspector
$ nexion browser capture status             # Is the page recording?
$ nexion browser capture mute / unmute      # Silence the devices, session stays alive
$ nexion browser capture stop               # Revoke them; the page must ask again
$ nexion browser inspector open           # open | close | toggle | status
# Downloads
$ nexion browser download https://site/file.zip   # Uses the page's cookies; lands in the Downloads card
$ nexion browser downloads                  # State, progress, destination, error
# Site permissions (camera, microphone, notifications)
$ nexion browser permission list
$ nexion browser permission get             # The site in the focused pane
$ nexion browser permission set notifications denied --host slack.com
$ nexion browser permission reset slack.com     # Or --all
# Profiles &amp; data
$ nexion browser profile list
$ nexion browser profile switch private     # Whole tab. Name or UUID
$ nexion browser profile switch private --pane-id <id>  # One pane only
$ nexion browser cookie list --domain example.com
$ nexion browser userscript list
# History
$ nexion browser history list --limit 20 -v    # Most recent first; -v adds count and time
$ nexion browser history search "github"
$ nexion browser history delete --host example.com
$ nexion browser history clear --confirm

Simulator Commands

Drive the iOS Simulator from the command line: list and boot devices, capture the screen into the Attach card, install and launch apps, open deep links, and send test pushes. Available on machines with Xcode installed.

nexion sim
$ nexion sim list                              # Devices grouped by runtime, with state
$ nexion sim boot                              # Boot a device (default: last used)
$ nexion sim screenshot --attach                # PNG path; --attach drops it in the Attach card
$ nexion sim record start / stop --attach      # Screen recording into the Attach card
$ nexion sim url myapp://path                    # Open a URL or deep link
$ nexion sim launch com.example.app --install App.app
$ nexion sim push com.example.app --title "Hi" --body "there"

An AI agent can drive the device UI too. The see-and-tap verbs need a one-time nexion sim setup (a small verified tool download):

nexion sim (drive)
$ nexion sim setup                             # One-time: provision the driving tools
$ nexion sim describe                          # Screen geometry (points, pixels, density) as JSON
$ nexion sim tap 120 340 --pixels          # Screenshots are pixels; HID input is points
$ nexion sim swipe 120 500 120 200 --pixels
$ nexion sim text -- "hello"                  # Type into whatever has focus
$ nexion sim button HOME                       # HOME, LOCK, SIDE_BUTTON, SIRI, APPLE_PAY

The loop for an agent is: screenshot, read the image, tap --pixels where it wants, then screenshot again to confirm. Coordinates read off a screenshot are in pixels, so pass --pixels; without it, taps are interpreted in points.

Event Streaming

Subscribe to real-time events from Nexion. Events are streamed as JSON-RPC notifications over a persistent connection.

nexion event
$ nexion event subscribe "tab.*"            # All tab events
$ nexion event subscribe "*"                # All events
$ nexion event subscribe "terminal.*"       # Terminal events only
$ nexion event list                         # List available event types

Each event is a single JSON line, making it easy to pipe to jq or other tools:

$ nexion event subscribe "tab.*" | jq '.params'
{"index": 2}
{"type": "browser"}

Available event types:

Category Events
tab.*created, closed, activated, renamed
pane.*created, closed, focused
terminal.*cwd-changed, shell-exited
editor.*mode-changed
browser.*navigated, loaded, title-changed
workspace.*activated, saved
git.*status-changed, branch-changed
plugin.*activated, deactivated
config.*changed

Other Commands

Workspace

$ nexion workspace list                     # Saved workspaces
$ nexion workspace activate "Dev Setup"     # Activate workspace
$ nexion workspace save "Current Layout"    # Save current layout
$ nexion workspace delete "Old Layout"      # Delete workspace

SSH

$ nexion ssh bookmarks                       # List bookmarks & ~/.ssh/config hosts
$ nexion ssh connect user@host               # Open SSH connection
$ nexion ssh connect myserver --port 2222    # Connect with custom port
$ nexion ssh disconnect myserver --user root --port 2222  # Disconnect a host
$ nexion ssh disconnect                        # Disconnect all
$ nexion ssh status                            # Active connections

Configuration

$ nexion config get terminal.theme          # Read a setting
$ nexion config set terminal.theme dracula  # Write a setting
$ nexion config list                        # List all keys
$ nexion config path                        # Print config file path
$ nexion config edit                        # Open in $EDITOR

Values are typed on the way in. A bare true or false is stored as a yes/no, a bare number as a number, and anything else as text. Quote it to force text where a value would otherwise be read as one of those: nexion config set some.key '"true"'.

A setting written this way takes effect immediately, exactly as if you had changed it in Preferences: the listener starts, the timer re-arms, the assertion drops. No relaunch.

Agents talking to each other

One agent can find the others, read what they said, send them something and wait for the answer. The same five things are a typed panes tool over MCP, so an agent uses them without shelling out.

$ nexion pane list --all                     # every project tab in this space
$ nexion terminal read --pane-id <id> --source transcript
$ nexion terminal send --pane-id <id> "controlla i test e dimmi"
$ nexion terminal wait --pane-id <id> --for reply
$ nexion terminal report "migrazione fatta" --status done

Read the transcript, not the screen. --source transcript returns what the agent SAID, from the file it writes its conversation in, as turns with roles. An agent terminal redraws in place, so its screen is not what it said: measured while building this, an agent answered a question twice and its screen showed an empty input box both times. Claude Code, Codex, Gemini and opencode are read; screen, history and output remain for everything else.

Wait for a reply, not for quiet. --for reply ends when a new turn from that agent appears, and hands the text back with it. idle is already true the moment before an agent starts, and --for text matches raw output, where a phrase a TUI paints in colour is split by escape sequences: wait for a marker you control, not for a phrase the model chose.

Some agents cannot be read at all. Copilot and Antigravity keep what the agent DID, the files it touched and the commands it ran, and nowhere the words it said. Those answer by reporting: terminal report posts a structured result on their own pane, and a --for reply waiting there receives it. It costs one call and works for anything that can make one.

Writing is off until you allow it. panes send over MCP asks for Settings > Privacy > Agent tools > "Allow agents to type into each other's panes", off by default. Reading, listing, waiting and reporting never ask. And a pane with no agent in it refuses text unless the caller says to_shell: at a prompt a sentence is not read, it is run.

A handle to an agent, not to a pane. Every listing carries an occupancy token, #, that changes when the agent in that pane is replaced. Pass it back on a write and a pane whose occupant has changed refuses instead of accepting your sentence on the new one's behalf.

Git

$ nexion git status                         # Git status of active project
$ nexion git branch                         # Current branch
$ nexion git log --count 5                  # Recent commits
$ nexion git diff                           # Diff
$ nexion git refresh                        # Force refresh git state

git status and git branch answer from a fresh look whenever anything in the working tree has moved since the last one, so an agent that commits and asks in the same breath is not handed the state from before its own commit. On a quiet tree the answer comes from what is already known. Measured on the Nexion repository: 47 ms for a question about a quiet tree, 137 ms for one right after a change. git refresh is still there for the rest of the app.

Notes

$ nexion notes list
$ nexion notes add "TODO: fix the build" --tags bug,urgent
$ nexion notes delete <id>
$ nexion notes search "build"

Spaces

A space is a context: its own projects, its own tabs, its own colour and browser profile. A script can build one and drop you into it.

nexion space
$ nexion space list                          # Spaces, with the current one marked
$ nexion space new "Client work"              # Create one and go to it
$ nexion space switch <name-or-id>           # Go to a space
$ nexion space next                          # The one after this, and prev for the one before
$ nexion space edit <name-or-id> --name "Work"  # Name, icon, colour, browser profile
$ nexion space rm <name-or-id>               # Its projects move to the first space

Projects and tasks

A project is a folder with a list of tasks that outlives the tabs. A task is one piece of work: its tab, its worktree when it has one, and the agent conversation it carries. Closing a task keeps the record, so it can be reopened where it left off.

nexion task
$ nexion project list                          # Projects, open or not
$ nexion project new ~/code/api                # Adopt a folder as a project
$ nexion project open api                      # Open or focus its tab
$ nexion task list --project api               # Its tasks, with their state
$ nexion task new "Fix the flaky test" --project api
$ nexion task new "Rate limits" --project api --branch rate-limits --agent claude
$ nexion task open "Fix the flaky test"          # Reopen it where it left off
$ nexion task close --id <id>                  # End the session; the row stays
$ nexion task rename "Old name" --to "New name"
$ nexion task rm --id <id>                     # Delete the record; the worktree is left alone

A task is named by its title, or with --id when two share one; --project narrows the search. With --branch a new task gets a git worktree of its own and the project's setup entries are linked into it, so two tasks on the same repository never fight over one checkout.

Devices

The phones and iPads paired with this Mac, which was the one thing in the app a script could not reach.

nexion device
$ nexion device list                         # Paired devices, and which are connected now
$ nexion device disconnect <name-or-id>     # Drop the link, keep the pairing
$ nexion device rm <name-or-id>             # Forget it; it has to pair again

Window

$ nexion window info                        # Position, size, state
$ nexion window sidebar toggle              # Toggle sidebar
$ nexion window focus                       # Bring Nexion to front

There is one window, and these speak about it. The contexts you switch between are spaces, above. A --window flag on any command is still accepted and ignored, so scripts written before spaces keep working.

Editor

Control the built-in code editor and inspector pane.

nexion editor
$ nexion editor open src/main.swift           # Open file in preview
$ nexion editor open src/main.swift --mode edit  # Open file in edit mode
$ nexion editor status                        # Current file & dirty state
$ nexion editor mode edit                     # Switch to edit mode
$ nexion editor mode preview                  # Switch to preview mode
$ nexion editor mode diff                     # Switch to diff mode
$ nexion editor save                          # Save current file

Attachments

Manage file attachments in the active project tab. Files are stored in .nexion/attachments/ inside the project root.

nexion attach
$ nexion attach list                          # List attached files
$ nexion attach add screenshot.png notes.md  # Attach files
$ nexion attach remove screenshot.png           # Remove by name
$ nexion attach open screenshot.png             # Open with default app
$ nexion attach clear                          # Remove all attachments

Icon Packs

Manage file icon themes.

nexion icon-pack
$ nexion icon-pack list                    # List installed packs
$ nexion icon-pack active                   # Show active pack
$ nexion icon-pack activate catppuccin-mocha  # Activate a pack
$ nexion icon-pack deactivate                # Use system icons

Plugins

$ nexion plugin list
$ nexion plugin enable <id>
$ nexion plugin disable <id>
$ nexion plugin reload <id>
$ nexion plugin exec <command> --args '{"key":"value"}'
$ nexion plugin commands                    # List registered commands

plugin list reports two different facts: enabled is what you asked for, and ACTIVE is what is actually running. They differ when a plugin fails to load, and by design when Nexion was started without its plugins: hold Shift while it launches, or pass --safe-mode, or set NEXION_SAFE_MODE=1. nexion version --json says whether this is such a start, and why.

Theme

Switch appearance mode and theme presets on the fly.

nexion theme
$ nexion theme dark                              # Switch to dark mode
$ nexion theme light                             # Switch to light mode
$ nexion theme system                            # Follow system appearance
$ nexion theme dark --terminal "Nexion dark" --preview "Material Dark"
$ nexion theme light --terminal "Nexion light" --preview "Material Light"

Status & Version

$ nexion status
Nexion is running
$ nexion version
Nexion 2026.8 (build 4198)
$ nexion completions zsh                    # Generate shell completions

Uninstall

To make notifications and MCP tools work, Nexion writes itself into places outside its own folder: a # Nexion block in ~/.zshrc, a claude shim, and server or hook entries in Codex, Gemini and opencode. Dragging the app to the Trash leaves all of it behind, still launching a server for an app that is gone.

nexion uninstall
$ nexion uninstall --dry-run              # List what would go, change nothing
$ nexion uninstall                        # Remove them, after confirming
$ nexion uninstall --yes                  # No prompt, for scripts
$ nexion uninstall --cli                  # Also drop the /usr/local/bin/nexion symlink

Removal is surgical: entries are matched by the markers and paths the installer wrote, so a nexion server you configured by hand, and anybody else's hooks in the same files, survive. Your data in ~/.nexion is never touched, only the integration artifacts. Open a new terminal afterwards for the shell change to land.

Environment Variables

Nexion injects environment variables into every terminal session, so scripts and tools can detect and interact with the app:

Variable Example Description
NEXION1Always set: detect if running inside Nexion
NEXION_VERSION2026.8App version (YYYY.M)
NEXION_SOCKET~/.nexion/nexion.sockIPC socket path
NEXION_TOKEN_FILE~/.nexion/auth-tokenAuth token file path
NEXION_PROJECT_IDa1b2c3d4-...UUID of the parent tab
NEXION_PROJECT_PATH/path/to/projectProject root directory
NEXION_PROJECT_NAMEMyProjectTab / project name
NEXION_PANE_IDe5f6g7h8-...UUID of the terminal pane

You can use these in shell scripts to build Nexion-aware workflows:

build.sh
# Only run inside Nexion
if [ -n "$NEXION" ]; then
    nexion tab rename "Building..."
    make build
    nexion tab rename "$NEXION_PROJECT_NAME"
fi

IPC Protocol

For advanced integrations, you can bypass the CLI and talk to Nexion’s IPC socket directly. This is useful for building autonomous workflows, MCP servers, or custom integrations that need low-latency, bidirectional communication.

The protocol is straightforward: connect to the Unix socket, authenticate, and send JSON-RPC 2.0 messages with length-prefixed framing. Every method available via nexion maps to a JSON-RPC method (e.g. nexion tab list → tab.list).

Connection Details

Socket~/.nexion/nexion.sock, or read $NEXION_SOCKET
Auth token~/.nexion/auth-token, or read $NEXION_TOKEN_FILE. File is mode 0600, regenerated on each app launch.
Framing4-byte big-endian uint32 length prefix, followed by UTF-8 JSON payload.
TimeoutUnauthenticated connections are dropped after 2 seconds.

Handshake

The first message on every connection must be an auth request:

1. Authenticate
# Send:
{"jsonrpc":"2.0", "id":1, "method":"auth", "params":{"token":"<contents of ~/.nexion/auth-token>"}}
# Receive:
{"jsonrpc":"2.0", "id":1, "result":{"status":"ok"}}

Request & Response

2. Send commands
# Request - list all tabs:
{"jsonrpc":"2.0", "id":2, "method":"tab.list", "params":{}}
# Success response:
{"jsonrpc":"2.0", "id":2, "result":[
  {"index":0, "type":"project", "name":"MyProject", "path":"/Users/ramin/Projects/MyProject", "active":true},
  {"index":1, "type":"browser", "name":"GitHub", "url":"https://github.com"}
]}
# Error response:
{"jsonrpc":"2.0", "id":2, "error":{"code":-32600, "message":"Invalid request"}}

Event Subscription

After authenticating, you can subscribe to events. Nexion will push notifications (no id field) over the same connection:

3. Subscribe to events
# Subscribe:
{"jsonrpc":"2.0", "id":3, "method":"event.subscribe", "params":{"pattern":"tab.*"}}
# Incoming event (pushed by Nexion):
{"jsonrpc":"2.0", "method":"event.tab.activated", "params":{"index":2, "type":"project", "name":"Nexion"}}

Method Reference

Every CLI command maps to a JSON-RPC method using dot notation. Parameters are passed as a JSON object:

CLI CommandJSON-RPC MethodParams
nexion tab listtab.list{"type": "project"} (optional)
nexion tab new --path /footab.new{"path": "/foo"}
nexion tab activate 2tab.activate{"index": 2} or {"id": "uuid"}
nexion tab closetab.close{"id": "uuid"} (optional)
nexion terminal send "text"terminal.send{"text": "echo hello"}
nexion terminal send-keys "Ctrl-C"terminal.sendKeys{"keys": "Ctrl-C"}
nexion pane listpane.list{}. Works for terminal & browser tabs.
nexion pane split --dir vpane.split{"dir": "v", "kind": "web|canvas", "url": "https://…", "file": "….excalidraw", "private": true, "profile_id": "…"}. url is web-only, file canvas-only and required, private / profile_id web-only and optional.
nexion pane move --target-index N --zone rightpane.move{"target_index": 0, "zone": "left|right|top|bottom"}. Browser-only.
nexion browser navigate browser.navigate{"url": "https://…", "pane": 1}. pane is optional.
nexion browser statebrowser.state{"pane": 0}. Aggregate read; replaces browser.url & browser.title.
nexion browser eval "js"browser.eval{"script": "document.title"}
nexion config get config.get{"key": "terminal.theme"}
nexion config set config.set{"key": "terminal.theme", "value": "dracula"}
nexion ssh bookmarksssh.bookmarks{}
nexion ssh connect user@hostssh.connect{"host": "user@host"}, {"port": 2222} (optional)
nexion ssh disconnect hostssh.disconnect{"host": "..."}, {"user": "root"}, {"port": 2222} (all optional; omit the host for all). A connection is identified by user, host and port together, so a host on a non-default port needs them.
nexion ssh statusssh.status{}
nexion editor open editor.open{"path": "src/main.swift"}, {"mode": "edit"} (optional)
nexion editor statuseditor.status{}
nexion editor mode editor.mode{"mode": "edit"}
nexion editor saveeditor.save{}
nexion attach listattach.list{}
nexion attach add attach.add{"paths": ["/abs/path"]}
nexion attach remove attach.remove{"name": "file.png"}
nexion attach clearattach.clear{}
nexion attach open attach.open{"name": "file.png"}
nexion icon-pack listicon-pack.list{}
nexion icon-pack activate icon-pack.activate{"id": "catppuccin-mocha"}
nexion icon-pack deactivateicon-pack.deactivate{}
nexion theme dark --terminal "X"theme.apply{"mode": "dark"}, {"terminal": "..."} {"preview": "..."} (optional)
nexion event subscribe "tab.*"event.subscribe{"pattern": "tab.*"}

Python Example

nexion_client.py
import socket, json, struct, os
def connect():
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    sock.connect(os.path.expanduser("~/.nexion/nexion.sock"))
    with open(os.path.expanduser("~/.nexion/auth-token")) as f:
        token = f.read().strip()
    send_rpc(sock, 1, "auth", {"token": token})
    recv_rpc(sock)
    return sock
def send_rpc(sock, id, method, params=None):
    msg = {"jsonrpc": "2.0", "id": id, "method": method}
    if params: msg["params"] = params
    payload = json.dumps(msg).encode()
    sock.sendall(struct.pack("!I", len(payload)) + payload)
def recv_rpc(sock):
    length = struct.unpack("!I", sock.recv(4))[0]
    data = b""
    while len(data) < length:
        data += sock.recv(length - len(data))
    return json.loads(data)
# Usage
sock = connect()
send_rpc(sock, 2, "tab.list")
tabs = recv_rpc(sock)
print(json.dumps(tabs, indent=2))

Node.js Example

nexion_client.mjs
import { createConnection } from "net";
import { readFileSync } from "fs";
import { homedir } from "os";
function connect() {
  return new Promise((resolve) => {
    const sock = createConnection(`${homedir()}/.nexion/nexion.sock`);
    const token = readFileSync(`${homedir()}/.nexion/auth-token`, "utf-8").trim();
    sock.once("connect", () => {
      send(sock, 1, "auth", { token });
      recv(sock).then(() => resolve(sock));
    });
  });
}
function send(sock, id, method, params) {
  const payload = Buffer.from(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
  const header = Buffer.alloc(4);
  header.writeUInt32BE(payload.length);
  sock.write(Buffer.concat([header, payload]));
}
function recv(sock) {
  return new Promise((resolve) => {
    let buf = Buffer.alloc(0);
    sock.on("data", (chunk) => {
      buf = Buffer.concat([buf, chunk]);
      if (buf.length >= 4) {
        const len = buf.readUInt32BE(0);
        if (buf.length >= 4 + len) {
          resolve(JSON.parse(buf.subarray(4, 4 + len).toString()));
        }
      }
    });
  });
}
// Usage
const sock = await connect();
send(sock, 2, "tab.list");
const tabs = await recv(sock);
console.log(tabs);

Shell Completions

Generate completions for your shell:

shell completions
# Zsh
$ nexion completions zsh > ~/.zfunc/_nexion
# Bash
$ nexion completions bash > /usr/local/etc/bash_completion.d/nexion
# Fish
$ nexion completions fish > ~/.config/fish/completions/nexion.fish

LLM Integration

A plain-text CLI reference is available at /llms.txt for use with LLMs and AI coding agents. This is a compact, token-efficient version of the full CLI documentation: ideal for feeding to Claude, ChatGPT, Copilot, or any other AI assistant.

Claude Code

Add the following to the CLAUDE.md file in your project root so Claude Code can use the Nexion CLI:

CLAUDE.md
# Nexion
This project runs inside Nexion. Fetch https://nexion.one/llms.txt for the CLI reference.

OpenAI Codex

Add the reference to your AGENTS.md file (Codex reads this automatically):

AGENTS.md
# Nexion
This project runs inside Nexion, a native macOS workspace.
Run `curl -s https://nexion.one/llms.txt` to get the full CLI and IPC reference.

ChatGPT

When using ChatGPT with Code Interpreter or browsing enabled, paste this at the start of your conversation:

ChatGPT prompt
I work inside Nexion, a macOS workspace app with a CLI.
Here is the full CLI reference: https://nexion.one/llms.txt
Use `nexion <command>` for shell commands or the IPC socket for direct integration.

You can also use ChatGPT’s Custom Instructions or a GPT’s system prompt to include the reference permanently.

OpenClaw

Add the reference to your system prompt or workspace instructions:

System prompt
This environment runs inside Nexion (macOS workspace for AI coding agents).
CLI reference available at: https://nexion.one/llms.txt
You can control Nexion via the `nexion` CLI or directly via its Unix socket IPC (JSON-RPC 2.0).

Any Other Agent

For any LLM or agent that can execute shell commands, you can fetch the reference inline:

# Fetch and read inline
$ curl -s https://nexion.one/llms.txt
# Or save locally for offline use
$ curl -o ~/.nexion/cli-reference.txt https://nexion.one/llms.txt