---
title: "Installation"
canonical_url: https://www.superlogical.com/rex/docs/installation
---

# Installation

**At the current stage of beta testing, the macOS application and the Linux
and FreeBSD server are available to all testers. The Windows server is
available to testers with the Windows role in Discord.**

We have Rex clients for iOS, Linux, and Windows at varying stages of
readiness. We hope to expand the scope of public testing quickly, but they
aren't part of testing yet.


The macOS
client is fully self-contained with a local server that lets you get up
and running quickly. We plan on adding support for more platforms for the
client in the future.

<h2 id="linux"><OsIcon name="linux" />Linux <Tag>Server only</Tag></h2>

See [Installing on Linux](https://www.superlogical.com/rex/docs/linux/installation.md) for the details.


<h2 id="freebsd"><OsIcon name="freebsd" />FreeBSD <Tag>Server only</Tag></h2>

See [Installing on FreeBSD](https://www.superlogical.com/rex/docs/freebsd/installation.md) for the details.


<h2 id="windows"><OsIcon name="windows" />Windows <Tag>Server only</Tag></h2>

See [Installing on Windows](https://www.superlogical.com/rex/docs/windows/installation.md) for the details.



{#if windowsMethods}

Adding the Rex WinGet source needs administrator rights.

{/if}

---
title: "Configuration File"
canonical_url: https://www.superlogical.com/rex/docs/customize/config
---

# Configuration File

Rex configuration is written in [Lua](https://www.lua.org) and lives in
a single file: `init.lua`. It binds keys, defines custom actions, declares
the other servers you connect to, and more. It can load other Lua files
(via `require`) so you can split your configuration up as you'd like.

Rex doesn't require any configuration, so start using Rex without it
and start a configuration file when there is something you want to change.

## File Location

On **macOS and Linux**, the file is at `$XDG_CONFIG_HOME/rex/init.lua` when
that environment variable is set and `~/.config/rex/init.lua` when it isn't.

On **Windows**, the file is at `%AppData%\rex\init.lua`.

Set `REX_CONFIG` to the path of another file to use that one instead.
This environment variable must be set in the environment of the Rex server
process, since the server is what reads the file.

## Quick Example

Here is a minimal configuration example with one keybinding:

```lua
rex.bind("cmd+d", "pane.split", { direction = "right" })
```

Reload the configuration with the CLI. Connected clients will be notified of
the new configuration and it will take effect immediately. Rex does not
currently watch for file changes; you must use the CLI.

```sh
rex config reload
```

The configuration file is read by the Rex server, so it lives on the same
machine as the server. `rex config reload` must also be run on that machine;
it is refused over a remote connection.

Press <kbd>Cmd</kbd>+<kbd>D</kbd> and the focused pane splits to the right.

The [Lua API reference](https://www.superlogical.com/rex/docs/reference/lua.md) lists all available Lua APIs.

## Validation

`rex config check` loads the file without changing anything and reports any
issues. If there are no issues, it reports the changes it found:

```
$ rex config check
path: /Users/you/.config/rex/init.lua
exists: yes
bindings: 8
removals: 0
modes: 1
actions: 0
```

Both commands print the file's mistakes and exit with status 1 if any of them
is an error. Give `rex config check` a path to check a file other than your
own, such as a draft.

### Validation Errors

A bad call to a `rex` function is skipped, and the rest of the file still
loads. Here line 2 passes a table where a key belongs:

```lua
rex.bind("cmd+d", "pane.split", { direction = "right" })
rex.bind({ "cmd+k" }, "pane.split")
rex.bind("ctrl+b>c", "window.new")
```

```
$ rex config check
...
bindings: 2
...
error init.lua:2: rex.bind: expected a string, got table
Error: the configuration has errors
```

The other two bindings still work.

A mistake Lua itself can't get past, such as a syntax error, fails the whole
load. When that happens on a reload, the server keeps the bindings and actions
from the last load that worked.

Some mistakes can't be seen from the file alone. Whether `pane.split` is a
real action depends on the client, because clients provide those actions.
`rex keymap` asks the running server for the merged result and reports these.

## Splitting Configuration

`require` loads other Lua files stored beside it:

```lua
-- ~/.config/rex/init.lua
require("keys")
```

```lua
-- ~/.config/rex/keys.lua
rex.bind("cmd+d", "pane.split", { direction = "right" })
rex.bind("ctrl+b>c", "window.new")
```

Mistakes are reported with the name of the file they are in, such as
`keys.lua:2`.

## Execution Model

The file runs from top to bottom each time it loads.

This load-time execution is for defining bindings, actions, etc. The callbacks
for these are deferred until they're invoked. Rex APIs such as listing sessions
or creating a new window do not work at load-time, since API calls are made on
behalf of a client and load-time has no client. They're reported as a warning
and the rest of the file still loads.

Example:

```lua
rex.action{
  name  = "scratch",
  title = "Open Scratch Window",
  run   = function(ctx, args)
    rex.session.new_window{
      layout = rex.layout.block{
        flavor = "com.superlogical.terminal.shell",
      },
    }
  end,
}
```

This defines the `scratch` action, but it doesn't invoke the callback
with `rex.session.new_window` until the action is actually invoked via
the command palette, a keybinding, etc.

To run Lua once from top to bottom against a live session, write a separate
script and run it with `rex do` instead.

---
title: "Keyboard Shortcuts"
canonical_url: https://www.superlogical.com/rex/docs/customize/keyboard-shortcuts
---

# Keyboard Shortcuts

Keyboard shortcuts are defined in the [configuration file](https://www.superlogical.com/rex/docs/customize/config.md)
with `rex.bind` and removed with `rex.unbind`. A shortcut binds a key to an
**action**, such as splitting a pane or switching sessions.

```lua
rex.bind("cmd+e", "pane.zoom")
rex.bind("shift+cmd+d", "pane.split", { direction = "down" })
```

Run `rex config reload` after changing the file for the shortcuts to take
effect.

## Binding a Key

`rex.bind` takes a key, the name of an action, and optionally a table of
arguments for that action:

```lua
rex.bind(key, action, args)
```

Some actions require arguments and some have optional arguments. For
example, `pane.resize` requires a `direction` and accepts an optional
`amount`:

```lua
rex.bind("ctrl+alt+left", "pane.resize", { direction = "left" })
rex.bind("ctrl+alt+shift+left", "pane.resize", {
  direction = "left",
  amount = 15,
})
```

Binding a key that already has a binding replaces it. The newest binding
for a key always wins.

## Finding Actions

Actions are provided by the client, so the list depends on which client you
use. List the actions of a connected client with the CLI:

```
$ rex client ls
LABEL     CLIENT                KIND  PRINCIPAL          AGE     ID
my-mac    Rex Beta 0.1.0 (996)  app   uid 501 over unix  1h ago  db494d

$ rex -C my-mac actions
NAME                   TITLE             ARGS
client.tab.new         New Tab
client.tab.goto        Go to Tab         index
pane.split             Split Pane        direction
pane.zoom              Zoom Pane
pane.resize            Resize Pane       direction, amount?
pane.send_key          Send Key          key
session.switch         Change Session…
...
```

The `ARGS` column lists the arguments each action takes. An argument followed
by `?` is optional.

You can also define your own actions in Lua and bind keys to them. See
[Custom Actions](https://www.superlogical.com/rex/docs/customize/actions.md).

## Writing Keys

A key is written as zero or more modifiers followed by one key, joined by
`+`. Case and the order of modifiers don't matter, so `shift+cmd+d` and
`Cmd+Shift+D` are the same key.

| Modifier | Also Written |
| --- | --- |
| `cmd` | `command`, `super`, `meta`, `win` |
| `ctrl` | `control` |
| `alt` | `opt`, `option` |
| `shift` | |

The key is a character such as `d`, `1`, or `,` or a name such as `enter`,
`escape`, `tab`, `space`, `backspace`, `delete`, `left`, `right`, `up`, `down`,
`home`, `end`, `pageup`, `pagedown`, or `f1` through `f12`.

Keys follow your keyboard layout: `ctrl+a` is whichever key types "a". To bind
a physical key regardless of layout, write its US-layout name in brackets,
such as `ctrl+[KeyA]`. The names are the
[W3C `KeyboardEvent.code` values](https://www.w3.org/TR/uievents-code/#code-value-tables),
such as `[Digit1]`, `[Slash]`, and `[ArrowLeft]`.

Write the key without shift applied. On a US layout, plus is `cmd+shift+=`,
not `cmd++`. Bindings are always matched on unshifted keys.

A shortcut can also be several keys pressed one after another, such as
`ctrl+b>c`. See [Key Sequences](https://www.superlogical.com/rex/docs/customize/key-sequences.md).

Shortcuts can also be grouped into a mode that is only active when you enter
it, such as a mode for resizing panes. See [Key Modes](https://www.superlogical.com/rex/docs/customize/key-modes.md).

## Removing a Shortcut

`rex.unbind` removes every binding for a key. It can be used to remove
a binding you set as well as unbind a key that the client defaults with:

```lua
rex.unbind("cmd+d")
```

Pass an action name to remove the key only when it is bound to that action:

```lua
rex.unbind("cmd+t", "client.tab.new")
```

An unbound key isn't handled by Rex at all, so it is sent to the focused
terminal.

To change what a default shortcut does, bind it. There is no need to unbind
it first:

```lua
-- Cmd+K clears the screen instead
rex.bind("cmd+k", "pane.send_key", { key = "ctrl+l" })
```

## How Shortcuts Are Resolved

A key can get its meaning from three places. From lowest to highest priority:

1. The default shortcuts the client ships with.
2. Your configuration file.
3. Shortcuts you set in the client itself, such as with the app's
   **Change Shortcut...** command.

A shortcut set in the app therefore wins over the configuration file. If a
binding in your file appears to do nothing, check that the app doesn't have its
own shortcut for the same key.

## Binding a Lua Function

A key can run a Lua function instead of an action. Return `true` to say the
key was handled:

```lua
rex.bind("ctrl+shift+n", function(ctx, ev)
  rex.session.new_window{
    layout = rex.layout.block{
      flavor = "com.superlogical.terminal.shell",
    },
  }
  return true
end)
```

The function can call any Rex API, such as `rex.session.new_window` above.
The [Lua API reference](https://www.superlogical.com/rex/docs/reference/lua.md) has the full list.

If the function returns `false` or nothing, Rex tries the next-newest function
bound to the same key, and sends the key to the focused terminal if none of
them handle it.

A function binding has no name, so it doesn't appear in the command palette
and can't be run from the CLI. For anything you'd want to do more than one
way, define a [custom action](https://www.superlogical.com/rex/docs/customize/actions.md) and bind the key to that.

## Inspecting Bindings

A configuration that loads other files with `require` can look at what they
bound and remove what it doesn't want. `rex.bindings()` returns every binding
made so far, in the order they were made.

```lua
require("keys")

-- Remove every sequence keys.lua bound behind ctrl+b
for _, b in ipairs(rex.bindings()) do
  if b.key:find("ctrl+b>", 1, true) == 1 then
    rex.unbind(b)
  end
end
```

Each binding is a table with `key`, `mode`, `action`, `args`, and `source`
(the file and line that made it). Passing one to `rex.unbind` removes exactly
that binding.

`rex.bindings` also accepts a filter on `key`, `mode`, or `action`, such as
`rex.bindings{ action = "pane.split" }`. `rex.modes()` does the same for
[key modes](https://www.superlogical.com/rex/docs/customize/key-modes.md).

These functions only see your configuration file and the files it loads. They
can't see the client's default shortcuts or the ones set in the app.

## Debugging

`rex config check` reports mistakes in the file itself, such as a key that
can't be parsed:

```
$ rex config check
...
error init.lua:1: unknown key "foo"
Error: the configuration has errors
```

`rex keymap` asks the running server what it loaded and lists each binding
with the line that made it:

```
$ rex keymap
KEY          ACTION          ARGS                  SOURCE
shift+cmd+d  pane.split      {"direction":"down"}  init.lua:2
cmd+e        pane.zoom       -                     init.lua:1
...
```

A misspelled action name is not an error in `rex config check`, because the
file alone can't know which actions a client has. The client reports it as a
warning when it loads the keymap.

---
title: "Key Sequences"
canonical_url: https://www.superlogical.com/rex/docs/customize/key-sequences
---

# Key Sequences

A key sequence is two or more keys pressed one after another, such as
<kbd>Ctrl</kbd>+<kbd>B</kbd> and then <kbd>C</kbd>. Sequences are bound with
`rex.bind` like any other [keyboard shortcut](https://www.superlogical.com/rex/docs/customize/keyboard-shortcuts.md),
with the keys joined by `>`:

```lua
rex.bind("ctrl+b>c", "client.tab.new")
rex.bind("ctrl+b>z", "pane.zoom")
```

Each key in a sequence is written the same way as a single key, modifiers
included. A sequence can be longer than two keys:

```lua
rex.bind("ctrl+b>shift+5", "pane.split", { direction = "right" })
rex.bind("ctrl+x>ctrl+k>s", "session.switch")
```

Press <kbd>Escape</kbd> partway through a sequence to cancel it.

## Example: Tmux Bindings

```lua
rex.bind("ctrl+b>c", "client.tab.new")
rex.bind("ctrl+b>n", "client.tab.next")
rex.bind("ctrl+b>p", "client.tab.previous")
-- % and " on a US layout
rex.bind("ctrl+b>shift+5", "pane.split", { direction = "right" })
rex.bind("ctrl+b>shift+'", "pane.split", { direction = "down" })
rex.bind("ctrl+b>z", "pane.zoom")
rex.bind("ctrl+b>x", "pane.close")

-- Press Ctrl+B twice to send one to the terminal
rex.bind("ctrl+b>ctrl+b", "pane.send_key", { key = "ctrl+b" })
```

Note the last binding. Once <kbd>Ctrl</kbd>+<kbd>B</kbd> starts a sequence
it no longer reaches the terminal by itself, so programs that use it need
another way to receive it.

## Binding vs. Sequence Conflict

A key either runs an action immediately or starts a sequence. It can't do
both, because Rex would have to wait to find out which you meant.

When a key is bound both ways, the newer binding wins and the older one never
runs:

```lua
rex.bind("cmd+k", "pane.zoom")         -- never runs
rex.bind("cmd+k>s", "session.switch")  -- cmd+k starts a sequence
```

With the two lines swapped, <kbd>Cmd</kbd>+<kbd>K</kbd> zooms immediately and
the sequence can never start.

The same rule applies across the [three places shortcuts come from](https://www.superlogical.com/rex/docs/customize/keyboard-shortcuts.md#how-shortcuts-are-resolved).
A sequence in your configuration file hides the client's default shortcut for
its first key, and a shortcut set in the app hides a sequence in the file that
starts with the same key.

Rex doesn't reorder anything to avoid this. Instead, the client is given a
warning that names both bindings and where each was written.

## Removing a Sequence

`rex.unbind` removes exactly one binding:

```lua
rex.unbind("ctrl+b>c")   -- removes this one sequence
rex.unbind("ctrl+b")     -- removes ctrl+b alone, and no sequences
```

Unbinding a key never removes the sequences that start with it. Each sequence
has to be unbound by its full name. A future change to Rex will allow
removing all sequences by prefix.

## Binding a Lua Function

A sequence can run a Lua function. The function is called when the last key of
the sequence is pressed:

```lua
rex.bind("ctrl+x>ctrl+s", function(ctx, ev)
  -- ev.prefix is { "ctrl+x" } and ev.keybinding is "ctrl+s"
  return true
end)
```

`ev.keybinding` is the last key and `ev.prefix` is a list of the keys before
it. One function bound to several sequences can use these to tell which one
was pressed. A function bound to a single key has no `ev.prefix`.

If the function returns `false` or nothing, only the last key is sent to the
focused terminal. The earlier keys were already consumed by the client.

---
title: "Key Modes"
canonical_url: https://www.superlogical.com/rex/docs/customize/key-modes
---

# Key Modes

A key mode is a named set of [keyboard shortcuts](https://www.superlogical.com/rex/docs/customize/keyboard-shortcuts.md)
that only apply while the mode is active. Modes let ordinary keys do something
else for a while, such as making the arrow keys resize panes. They are also
called "key tables" in other software.

```lua
rex.bind("cmd+r", "client.mode.enter", { name = "resize" })

rex.bind("resize/left", "pane.resize", { direction = "left" })
rex.bind("resize/right", "pane.resize", { direction = "right" })
rex.bind("resize/up", "pane.resize", { direction = "up" })
rex.bind("resize/down", "pane.resize", { direction = "down" })
```

Press <kbd>Cmd</kbd>+<kbd>R</kbd> to enter the mode. The arrow keys now resize
the focused pane. Press <kbd>Escape</kbd> to leave the mode, and the arrow
keys go back to what they did before.

## Binding a Key in a Mode

To bind a key in a mode, write the mode's name and a slash `/` before the key:

```lua
rex.bind("resize/shift+left", "pane.resize", {
  direction = "left",
  amount = 15,
})
```

A mode exists as soon as a key is bound in it. A mode name may contain only
lowercase letters, digits, underscores, and hyphens.

The key after the slash can be anything a shortcut outside a mode can be,
including a [key sequence](https://www.superlogical.com/rex/docs/customize/key-sequences.md) such as `resize/g>g`.

Bindings in a mode are separate from bindings outside it. Binding `resize/left`
doesn't change what <kbd>Left</kbd> does outside the mode.

A binding in a mode must name an action. To run Lua from a mode, define a
[custom action](https://www.superlogical.com/rex/docs/customize/actions.md) and bind that.

## Entering and Leaving a Mode

The client provides three actions for moving between modes. Bind them like any
other action:

| Action | Description |
| --- | --- |
| `client.mode.enter` | Enters the mode given by `name`. |
| `client.mode.exit` | Leaves the current mode. |
| `client.mode.exit_all` | Leaves every mode. |

<kbd>Escape</kbd> also leaves the current mode, unless the mode binds
<kbd>Escape</kbd> to something else.

A mode stays active until you leave it. To leave automatically after one
shortcut, enter it with `once`:

```lua
rex.bind("cmd+r", "client.mode.enter", {
  name = "resize",
  once = true,
})
```

Modes can be entered from inside other modes. The most recently entered mode
is checked first, and `client.mode.exit` leaves only that one.

## Unbound Keys in a Mode

By default, a key the mode doesn't bind does what it does outside the mode.
In the resize example, you can still type in the terminal while the mode is
active.

Declare the mode as `exclusive` to ignore those keys instead:

```lua
rex.mode("resize", { exclusive = true })
```

While an exclusive mode is active, only its own bindings do anything. This
avoids typing into a terminal by accident while you think you're resizing.

`rex.mode` is only needed to set options. `exclusive` is the only option.

## Removing Bindings

`rex.unbind` with a mode and a key removes that key's bindings in the mode
and **blocks** the key there. A blocked key does nothing while the mode is
active, even if it is bound outside the mode:

```lua
rex.unbind("resize/left")
```

Pass an action name to remove one binding without blocking the key:

```lua
rex.unbind("resize/left", "pane.resize")
```

To remove a whole mode, including its bindings, blocked keys, and options,
pass a table:

```lua
rex.unbind{ mode = "resize" }
```

`rex.unbind("left")` never touches a mode, and `rex.unbind("resize/left")`
never touches <kbd>Left</kbd> outside the mode.

## Example: Resize Mode

A complete resize mode, entered with <kbd>Cmd</kbd>+<kbd>R</kbd>:

```lua
rex.mode("resize", { exclusive = true })
rex.bind("cmd+r", "client.mode.enter", { name = "resize" })

rex.bind("resize/left", "pane.resize", { direction = "left" })
rex.bind("resize/right", "pane.resize", { direction = "right" })
rex.bind("resize/up", "pane.resize", { direction = "up" })
rex.bind("resize/down", "pane.resize", { direction = "down" })

-- Hold shift for bigger steps
rex.bind("resize/shift+left", "pane.resize", {
  direction = "left",
  amount = 15,
})
rex.bind("resize/shift+right", "pane.resize", {
  direction = "right",
  amount = 15,
})

rex.bind("resize/=", "pane.balance")
rex.bind("resize/enter", "client.mode.exit")
```

## Debugging

`rex keymap` lists every mode the server loaded after its bindings, along
with whether it is exclusive and which keys it blocks:

```
$ rex keymap
KEY           ACTION       ARGS                  SOURCE
resize/left   pane.resize  {"direction":"left"}  init.lua:4
...

MODE    EXCLUSIVE  BLOCKED
resize  yes        -
```

---
title: "Custom Actions"
canonical_url: https://www.superlogical.com/rex/docs/customize/actions
---

# Custom Actions

A custom action is a named Lua function defined in the
[configuration file](https://www.superlogical.com/rex/docs/customize/config.md) with `rex.action`. Once defined, it
works like any built-in action: it can be bound to a key, it appears in the
command palette, and it can be run from the CLI.

```lua
rex.action{
  name  = "scratch",
  title = "Open Scratch Window",
  run   = function(ctx, args)
    rex.session.new_window{
      layout = rex.layout.block{
        flavor = "com.superlogical.terminal.shell",
      },
    }
  end,
}

rex.bind("shift+cmd+n", "scratch")
```

Run `rex config reload` after changing the file for the action to become
available. Then press <kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>N</kbd>, search for
"Open Scratch Window" in the command palette, or run it from the CLI:

```sh
rex do scratch
```

## Defining an Action

`rex.action` takes a table. `name`, `title`, and `run` are required.

| Field | Description |
| --- | --- |
| `name` | How the action is referred to in `rex.bind` and on the CLI. Lowercase letters, digits, and underscores only. |
| `title` | The text shown in the command palette and in menus. |
| `run` | The function to call when the action is run. |
| `description` | A longer explanation for the command palette. |
| `category` | A group for the command palette. Defaults to `Actions`. |
| `keywords` | Extra words the command palette search should match. |
| `args` | The arguments the action takes. See [Arguments](#arguments). |
| `repeats` | Set to `true` to run the action again while its key is held down. |

A custom action's name can't contain a dot. Dotted names such as `pane.split`
belong to clients and blocks. All custom actions have no dot. This is a small
visual language that makes it easier to tell where an action comes from.

Defining the same name twice is reported as a warning and the later
definition wins.

## Arguments

`args` declares each argument an action takes and its type. A trailing `?`
marks an argument as optional:

```lua
rex.action{
  name  = "dev_layout",
  title = "Open Dev Layout",
  args  = { file = "string", ratio = "number?" },
  run   = function(ctx, args)
    local shell = "com.superlogical.terminal.shell"
    local w, err = rex.session.new_window{
      layout = rex.layout.horizontal(args.ratio or 0.5,
        rex.layout.block{ flavor = shell },
        rex.layout.block{
          flavor  = shell,
          options = { command = { "tail", "-f", args.file } },
        }),
    }
    if err ~= nil then error(err, 0) end
    return { window_id = w.window_id }
  end,
}
```

The types are `string`, `number`, `integer`, `boolean`, `object`, `array`, and
`any`.

Declaring arguments lets Rex catch mistakes before the action runs. A binding
that passes a misspelled or wrongly-typed argument is reported as a warning
when the client loads its keymap, and the CLI rejects it outright:

```
$ rex do dev_layout file=app.log bogus=1
Error: no argument named "bogus"; the action takes file, ratio?
```

An action that doesn't declare `args` accepts anything.

For more control, `args` can instead be a [JSON Schema](https://json-schema.org)
written as a Lua table. A table with a `type` key is treated as a schema and
used exactly as written.

## The Run Function

`run` is called with two tables: `ctx` describes where the action was run and
`args` holds its arguments. `args` is always a table, even when no arguments
were passed.

| Field | Description |
| --- | --- |
| `ctx.session_id` | The session the action applies to. |
| `ctx.block_id` | The block the action applies to, usually the focused one. |
| `ctx.client_id` | The client that asked for the action. |
| `ctx.origin` | How the action was run: `key`, `palette`, `cli`, `api`, or `script`. |

A field is absent when Rex doesn't know its value.

Rex API calls inside `run` default to the action's own session, so the
examples on this page never pass a `session_id`. A call that acts on one
block, such as `rex.session.set_block_label`, still needs a `block_id`. Pass
`ctx.block_id` for the focused one. The
[Lua API reference](https://www.superlogical.com/rex/docs/reference/lua.md) has the full list of calls.

### Errors

A failed API call doesn't stop the function. It returns `nil` and an error
message, so check the second return value when the result matters:

```lua
local w, err = rex.session.new_window{ layout = layout }
if err ~= nil then error(err, 0) end
```

Raising an error with `error` fails the action and reports the message to
whoever ran it.

### Return Values

Whatever `run` returns is the action's result, which the CLI prints as JSON:

```
$ rex do dev_layout file=app.log
{"window_id":"window:01a10dd6-ce31-760a-a9e5-c273c73fdd7e"}
```

Returning nothing, `nil`, or `true` means there is no result.

## Running an Action

**From a key.** Bind it by name, the same as a built-in action. See
[Keyboard Shortcuts](https://www.superlogical.com/rex/docs/customize/keyboard-shortcuts.md).

```lua
rex.bind("shift+cmd+l", "dev_layout", { file = "app.log" })
```

**From the command palette.** Search for the action's title.

**From the CLI.** Use `rex do` with the action's name and its arguments as
`key=value` pairs. Add `--json` for formatted output.

```sh
rex do dev_layout file=app.log ratio=0.3
```

`rex actions` lists the custom actions the server has loaded.

## Calling Other Actions

`rex.invoke` runs another custom action from inside `run` and returns its
result. It runs with the same session, block, and client.

```lua
rex.action{
  name  = "dev_layout_main",
  title = "Open Dev Layout for app.log",
  run   = function(ctx, args)
    local result, err = rex.invoke("dev_layout", {
      file = "app.log",
    })
    if err ~= nil then error(err, 0) end
    return result
  end,
}
```

`rex.client.queue` asks the client that ran the action to perform one of its
own actions, such as closing the command palette or zooming a pane. Queued
actions run in order after `run` returns, and are discarded if `run` fails.

```lua
rex.action{
  name  = "focus_mode",
  title = "Focus Mode",
  run   = function(ctx, args)
    rex.client.queue("pane.zoom")
    rex.client.queue("client.sidebar.toggle")
  end,
}
```

`rex.client.queue` needs a client, so it raises an error when the action is
run from the CLI and no client can be found. The app also only accepts actions
from the CLI when Remote Control is turned on in its settings.

## Running Commands

`rex.exec` runs a command and returns what it printed and how it exited:

```lua
rex.action{
  name  = "pull_dotfiles",
  title = "Pull Dotfiles",
  run   = function(ctx, args)
    local r, err = rex.exec(
      { "git", "pull", "--ff-only" },
      { cwd = os.getenv("HOME") .. "/dotfiles", timeout = 20 })
    if r == nil then error(err, 0) end
    if not r.ok then error(r.stderr, 0) end
    return { output = r.stdout }
  end,
}
```

While an action waits for a command, the server keeps handling keys and
other actions, so a slow command doesn't hold anything else up. The action
still has 30 seconds to finish, and any command it started is stopped when it
returns. Use [`rex.exec.start`](https://www.superlogical.com/rex/docs/reference/lua.md#rexexecstartargv-opts) with
`detach = true` for a command that should keep running.

[Lua Scripts](https://www.superlogical.com/rex/docs/automate/lua-scripts.md#running-commands) covers `rex.exec` in
more detail.

## Trying an Action Before Reloading

`rex do` can load a file and run one of its actions without touching the
running configuration. Pass the path to the file and name the action with
`--action`:

```sh
rex do ~/.config/rex/init.lua --action dev_layout file=app.log
```

This is a quick way to iterate on an action. The action really runs, against
the session you run the command in.

---
title: "The CLI"
canonical_url: https://www.superlogical.com/rex/docs/automate/cli
---

# The CLI

`rex` is the command-line interface to Rex.

`rex` is both an administrative tool for Rex as well as a foundation for
automation. As an administrative tool, it can create sessions, split panes,
list active clients, force disconnect a client, etc. As an automation tool,
it has a complete, powerful Lua scripting interface.

Rex adds `rex` to the `PATH` of every terminal it opens, and sets up the
context so that the CLI already knows what session you're in, which block
you're looking at, and so on.

If `rex` isn't found, see [PATH](#path).

Open a terminal and try it:

```sh
rex ls                 # list your sessions
rex session inspect    # show details of the session you are in
rex block inspect      # show details of the terminal you are typing in
```

Run `rex help` for every command, or `rex help <command>` for the details of
one.

## Taxonomy

Rex organizes work into four kinds of things. Every command acts on one or
more of them.

- A **session** is a group of related work, such as one project. The server
  keeps a session running whether or not anything is looking at it.
- A **window** is a tab within a session. It holds a layout of blocks.
- A **block** is one application in a window. Today that means a terminal.
- A **client** is a program connected to the server, such as the app. Clients
  matter when you ask the app itself to do something, like open its command
  palette.

Each one has an ID that never changes and is never reused, such as
`block:01a10d99-0c06-7795-bf67-798ce4c36a33`. Sessions, windows, blocks, and
clients can also have a label, which is a name you choose and can change.

## Command Targets

With no options, a CLI command acts based on its context. Inside a Rex
terminal that is the terminal's own block and session, so `rex split` splits the terminal you
typed it in. Rex knows where you are from three environment variables it sets
in every terminal:

| Variable | Holds |
| --- | --- |
| `REX_SESSION` | The ID of the session the terminal belongs to |
| `REX_BLOCK` | The ID of the terminal's own block |
| `REX_SERVER` | The address of the server that runs the terminal |

To act somewhere else, specify it with a flag:

| Option | Chooses |
| --- | --- |
| `-s`, `--session` | A session |
| `-w`, `--window` | A window within the session |
| `-b`, `--block` | A block |
| `-C`, `--client` | A client. `REX_CLIENT` sets a default. |
| `-S`, `--server` | A server, by the label of a host from `rex hosts` or by address. `REX_SERVER` sets a default. |

```sh
rex send -s build -w 2 'make test'   # type into window 2 of the "build" session
rex capture -b logs                  # print the screen of the block labeled "logs"
rex -S studio ls                     # list the sessions on the host labeled "studio"
```

### Targets

Rex tries to match a session, window, block, or client target (such as `-C`)
in this order and stops at the first kind of match it finds:

1. A full ID, such as `session:01a10cf3-2655-7e48-9b9c-43dd6612bef5`.
2. A case-insensitive label, such as `build`.
3. A position, counting from one. Windows are numbered in tab order, so `-w 2`
   is the second tab.
4. The end of an ID, at least four characters long, such as `12bef5`. Listing
   commands print these short IDs.

If a target matches more than one thing, the command stops without changing
anything, lists what matched, and exits with status 3:

```
Error: session target "work" is ambiguous
  first   …001234
  second  …00abcd
```

A server is the exception. `-S` takes only a host label or an address, and a
value with a port is always an address.

Labels are convenient at a prompt, but they can change and two things can
share one. In a script that has to keep working, save the ID a command returns
and use that.

## Machine-Readable Output

Most commands print a table meant for reading. Pass `--json` to get JSON
instead, which is stable to parse and includes full IDs:

```
$ rex ls --json
{
  "sessions": [
    {
      "session_id": "session:01a10cf3-2655-7e48-9b9c-43dd6612bef5",
      "label": "Research"
    },
    {
      "session_id": "session:01a10d97-414b-79ee-9988-4d96d804ef90",
      "label": "SL Website"
    }
  ]
}
```

The exit status says how a command ended:

| Status | Meaning |
| --- | --- |
| 0 | The command succeeded |
| 1 | The command failed |
| 2 | The command line was invalid |
| 3 | A target was not found or was ambiguous |
| 4 | The server could not be reached |
| 130 | The command was interrupted |

Commands that wait for a program, such as `rex wait` and `rex run --wait`,
exit with that program's status instead.

## PATH

When Rex opens a terminal, it puts the directory that holds the server's `rex`
executable at the front of `PATH`. It also sets `REX_BIN_DIR` to the same
directory. Your shell's startup files run after that. A startup file that sets
`PATH` to a fixed list leaves `rex` out. One that reorders `PATH` can put a
different copy of `rex` ahead of the one that matches your server.

To check, compare the two:

```sh
echo "$REX_BIN_DIR"
echo "$PATH"
```

If `PATH` doesn't start with `REX_BIN_DIR`, add it back to the front at the
end of your shell's startup file. Adding it to the end of `PATH` instead can
still pick up another copy of `rex` first.

```sh
# ~/.zshrc, or ~/.bashrc
if [ -n "$REX_BIN_DIR" ]; then
    export PATH="$REX_BIN_DIR:$PATH"
fi
```

```fish
# ~/.config/fish/config.fish
if set -q REX_BIN_DIR
    fish_add_path --path --prepend --move $REX_BIN_DIR
end
```

### Nix

NixOS and nix-darwin set `PATH` to a fixed list each time a shell starts. This
affects you if your `PATH` includes `/run/current-system/sw/bin`, even if you
only used Nix to install your shell. Add the same check to your system
configuration so it runs in every interactive `bash` and `zsh`:

```nix
environment.interactiveShellInit = ''
  if [ -n "$REX_BIN_DIR" ]; then
    export PATH="$REX_BIN_DIR:$PATH"
  fi
'';
```

For `fish`, put the `fish` example above in `programs.fish.interactiveShellInit`.
With home-manager, use your shell's init option, such as
`programs.zsh.initContent`. Don't add `REX_BIN_DIR` to `home.sessionPath`.
Outside Rex the variable is empty, which adds an empty entry to `PATH`.

---
title: "Agents"
canonical_url: https://www.superlogical.com/rex/docs/automate/agent-skill
---

# Agents

Rex ships with a built-in agent skill that enables agents such as Claude Code
or Codex to efficiently open tabs, split panes, read a terminal's screen,
control connected Rex apps, and more.

Even without the skill, a modern agent is smart enough to discover and use the
[`rex` CLI](https://www.superlogical.com/rex/docs/automate/cli.md) to perform tasks, but the skill makes it far more
efficient and is recommended. You'll use fewer tokens and take less time
to achieve the same tasks.

## What Agents Can Do

Here are some examples of what your agent can do with Rex:

| You ask | The agent |
| --- | --- |
| "Open nvim with top in a narrow pane on the right." | Opens a tab with the editor focused and gives `top` the right third. |
| "Start the dev server in another tab and tell me when it's ready." | Starts the server without taking focus, then reads its tab until the server says it's listening. |
| "Run the tests beside you and fix whatever fails." | Runs the tests in a split, waits for them to finish, and reads the failures from the screen before fixing them. |
| "Run `\dt` in the psql pane and tell me what's there." | Types the command into psql, presses Return, and reads the result. |
| "Switch my Mac to this session and make the text bigger." | Asks the app on that Mac to show the session and increase its font size. |
| "Close everything you opened." | Closes the tabs and panes it created and leaves the rest alone. |

Requests that change the app itself, such as switching your Mac to a session,
need the app running with Remote Control turned on in its settings.

## Agent Setup

On macOS, run **Install Rex for Agents** from the command palette. Press
<kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd> to open the palette.

From the CLI, `rex skill setup` finds the agents installed on this
machine and sets up each one. If you install new agents in the future,
it is safe to run multiple times.

```
$ rex skill setup
Claude Code
  skill  created      ~/.claude/skills/rex

Codex, OpenCode, Pi
  skill  created      ~/.agents/skills/rex

...

Start a new agent session inside Rex to use it.
```

This installs two things:

- The **skill** teaches the agent the `rex` commands and when to use them.
  It reads its guide from the installed `rex`, so upgrading Rex updates what
  the agent knows without running setup again.
- A **hook** tells the agent, at the start of each session, that it is
  running in Rex and which session and terminal it is in, so it doesn't
  spend time or tokens finding out.

Rex adds its hook to the agent's settings file and leaves the rest of the
file as it was. Running setup again reports what is already current as
`unchanged` and updates anything an older Rex installed.

Rex finds an agent by its configuration directory, such as `~/.claude`, or by
its program on your `PATH`. To set up an agent Rex didn't find, name it with
`--agent`. These are all of the options:

| Option | Does |
| --- | --- |
| `--agent NAME` | Sets up only the named agent, whether or not Rex found it. Repeat it to name several. |
| `--dry-run` | Reports what would change without changing anything. |
| `--project` | Sets up the current repository instead of your account. |
| `--force` | Replaces a skill named `rex` that Rex didn't install. |
| `--session-naming` | Has agents name the session after their task without being asked. Running setup again without it keeps the choice already installed. |
| `--json` | Prints the result as JSON. |

### Approving the Hook

Some agents won't run a new hook until you allow it:

- **Codex** asks before it runs a new or changed hook. Approve Rex's hook
  with `/hooks` in Codex. Until then, Codex skips it.
- **Claude Code** doesn't ask about each hook, but it runs no hooks at all
  until you trust the folder it starts in.

## Supported Agents

`rex skill setup` can set up these agents:

| Agent | `--agent` | Skill | Hook |
| --- | --- | --- | --- |
| Claude Code | `claude` | `~/.claude/skills/rex` | `~/.claude/settings.json` |
| Codex | `codex` | `~/.agents/skills/rex` | `~/.codex/hooks.json` |
| OpenCode | `opencode` | `~/.agents/skills/rex` | Not supported |
| Pi | `pi` | `~/.agents/skills/rex` | Not supported |

Rex follows the environment variables that move an agent's directory, such
as `CLAUDE_CONFIG_DIR` and `CODEX_HOME`.

Any other agent that can run commands in a Rex terminal can use Rex too.
`rex skill show` prints the guide the skill gives agents, so tell the agent
to run it and follow the guide.

We'll continue to add support for more common agents over time.

## How It Works

An agent runs in a Rex terminal like any other program, so it has the same
`REX_SESSION` and `REX_BLOCK`
[environment variables](https://www.superlogical.com/rex/docs/automate/cli.md#command-targets) as your shell. They
tell the agent which session and terminal it is in.

You may be looking at a different session than the agent is in. The agent
chooses where to act from what you say:

- **"Here", "beside you", or nothing at all.** The agent's own session and
  terminal.
- **"The focused pane" or "the current tab".** The pane or tab that has focus
  in the session you're viewing.
- **A name.** The session, tab, or pane with that label. A name always wins
  over the agent's own location and the app's focus.

An agent started outside Rex has no `REX_SESSION`. It lists the sessions with
`rex ls` and works in the one that matches your request, or asks you to
choose when more than one does.

## Removing the Skill

`rex skill remove` removes the skill and hooks from every agent Rex knows,
including agents you've since uninstalled:

```sh
rex skill remove
```

Only what Rex installed is removed, including copies of the skill an older
Rex left in other directories the agent reads. Other skills, other hooks, and
the rest of each settings file stay as they were. `--agent`, `--project`, and
`--dry-run` work the same as for `rex skill setup`.

Codex remembers which hooks you approved by their place in `hooks.json`. If
removing or updating Rex's hook moves the hooks after it, Codex asks you to
approve those again, and Rex says so in its output.

## Debugging

`rex skill status` reports what is installed in each agent, without changing
anything:

```
$ rex skill status
Claude Code (found)
  skill  current      ~/.claude/skills/rex
  hook   current      ~/.claude/settings.json

Codex (found)
  skill  outdated     ~/.agents/skills/rex
  hook   current      ~/.codex/hooks.json
...

Run "rex skill setup" to install or update the skill.
```

| State | Meaning |
| --- | --- |
| `current` | Installed and up to date. |
| `outdated` | Installed by an older Rex, or edited since. `rex skill setup` updates it. |
| `missing` | Not installed. |
| `foreign` | A skill named `rex` that Rex didn't install. `rex skill setup` leaves it alone unless you pass `--force`. |
| `unreadable` | The file couldn't be read. |
| `unsupported` | The agent can't hold this item, such as a hook in an agent without hooks. |

If an agent doesn't seem to know it's in Rex, check that its session started
after `rex skill setup` ran, since the hook only runs when a session starts.
In Codex, check that the hook is approved with `/hooks`. In Claude Code,
check that you trusted the folder.

---
title: "Lua Scripts"
canonical_url: https://www.superlogical.com/rex/docs/automate/lua-scripts
---

# Lua Scripts

`rex do` runs a [Lua](https://www.lua.org) script against a Rex server.

A script is extremely powerful. It can do anything a full-fledged client
can: inspect servers, create sessions, create windows, start terminals,
read a terminal screen, react to events, and more.

This script opens a new window running `htop`:

```lua
-- htop.lua
rex.session.new_window{
  layout = rex.layout.block{
    flavor  = "com.superlogical.terminal.shell",
    options = { command = { "htop" } },
  },
}
```

```sh
rex do htop.lua
```

A script uses the same `rex` API as the [configuration file](https://www.superlogical.com/rex/docs/customize/config.md).
Unlike the configuration file, a script runs once from top to bottom
and exits (unless there are active event handlers or timers running).

## Running a Script

`rex do` takes a script in three ways:

```sh
rex do ./layout.lua                           # a file
rex do -e 'return rex.call("session.list")'   # inline with -e
rex do - < layout.lua                         # stdin
```

The argument is treated as a file when it ends in `.lua` or contains a slash.
Anything else is the name of an [action](https://www.superlogical.com/rex/docs/customize/actions.md) to run.

A script run inside a Rex terminal acts on that terminal's session and block.
Use `-s` and `-b` to [target](https://www.superlogical.com/rex/docs/automate/cli.md#command-targets) a different one:

```sh
rex do -s build ./layout.lua
```

Scripts have Lua's full standard library, including `io` and `os`, and
`require` loads other Lua files. The exception is `io.popen` and `os.execute`.
Use [`rex.exec`](#running-commands) to run commands instead.

Scripts are executed locally where the `rex` CLI is, so security and program
access is up to you.

## Arguments

Arguments are passed as `key=value` pairs after the script and read from the
`rex.args` table:

```lua
-- greet.lua
return "hello " .. (rex.args.name or "world")
```

```
$ rex do greet.lua name=rex
"hello rex"
```

A value's type comes from how it is written. `n=2` is a number, `on=true` is a
boolean, and `name=x` is a string. To pass a string that looks like something
else, or a nested value, pass all the arguments as JSON with `--args`:

```sh
rex do greet.lua --args '{"name": "true"}'
rex do greet.lua --args @args.json
```

## Results and Errors

The value a script returns is printed as JSON. Tables become objects or
arrays. Add `--json` for indented output. A script that returns nothing
prints nothing.

```
$ rex do -e 'return { name = "rex", tabs = { 1, 2 } }'
{"name":"rex","tabs":[1,2]}
```

A script that raises an error prints the message and exits with status 1:

```
$ rex do -e 'error("no tests found", 0)'
Error: no tests found
```

To exit with a status of your choosing, call `rex.stop` with it. The script
ends immediately:

```lua
rex.stop(3)
```

`print` writes to the terminal as usual, and `rex.log` writes a log line.

## Calling Rex

A script starts with a few values describing where it was run:

| Value | Description |
| --- | --- |
| `rex.session_id` | The session the script is running against. |
| `rex.block_id` | The block the script is running against, when there is one. |
| `rex.args` | The script's arguments. |

Rex's API is grouped under `rex.session`. Each call takes one table of
arguments and returns one table:

```lua
rex.session.set_block_label{
  block_id = rex.block_id,
  label    = "tests",
}
```

Calls fill in the script's session, so most calls don't need a `session_id`.
A call that acts on one block, such as `set_block_label`, still needs its
`block_id`.

A failed call doesn't stop the script. It returns `nil` and an error message,
so check the second value when the result matters:

```lua
local w, err = rex.session.new_window{ layout = layout }
if err ~= nil then error(err, 0) end
```

Three more ways to call Rex:

- `rex.call(method, args)` calls any server method by name, including those
  outside `rex.session`, such as `rex.call("session.list")`.
- `rex.block.call(creator, method, args)` calls a method on a block, such as
  reading a terminal's screen.
- `rex.invoke(name, args)` runs an action, either one the script defines or one
  from your configuration file. `rex.client.queue(name, args)` asks the app to
  perform one of its own.

The [Lua API reference](https://www.superlogical.com/rex/docs/reference/lua.md) has the full list.

### Layouts

Calls that create blocks take a `layout`. Build one with `rex.layout`:

```lua
local shell = "com.superlogical.terminal.shell"

rex.session.new_window{
  layout = rex.layout.horizontal(0.5,
    rex.layout.block{ flavor = shell },
    rex.layout.block{
      flavor  = shell,
      options = { command = { "htop" } },
    }),
}
```

`rex.layout.block` is one block. `rex.layout.horizontal` and
`rex.layout.vertical` split the space between two layouts, with the first
argument giving the first one's share. They can be nested.

## Sleeping and Waiting

A script often needs to start something and then act on how it ended.

`rex.sleep(seconds)` pauses the script:

```lua
rex.sleep(0.5)
```

`rex.wait(event, filter, timeout)` pauses until an event arrives:

```lua
local ev = rex.wait(
  "terminal.child_exited", { block_id = id }, 600)
```

- `event` is the name of the event to wait for.
- `filter` is optional. A table matches an event whose fields equal the
  table's. A function is called with the event and matches when it returns
  `true`.
- `timeout` is optional, in seconds. Without it, `rex.wait` waits forever.

`rex.wait` returns the event as a table. If the timeout passes first, it
returns `nil` and `"timeout"`.

The [events reference](https://www.superlogical.com/rex/docs/reference/events.md) lists the events and their fields.

## Timers

`rex.every` calls a function at a fixed interval, and `rex.after` calls one
once after a delay:

```lua
rex.every(5, function(ctx)
  -- every five seconds
end)

rex.after(0.5, function(ctx)
  -- once, half a second from now
end)
```

Unlike `rex.sleep`, a timer doesn't pause the script. The script carries on,
and the function is called later.

Both functions return a timer. Call `t:cancel()` to stop it. A timer keeps
running even if the script drops it, so keep it around if it may need to be
stopped.

Timers can be created anywhere, including inside an event handler or another
timer. The function's `ctx` and API calls always default to the script's own
session and block, even when the timer was created in a handler.

Timers never run at the same time as other Lua in the script. A timer that
comes due while a handler is running waits for it to return. If the script is
still busy when the next tick of `rex.every` comes, the function runs once
when it is free rather than once for each tick it missed.

An error in a timer is printed and the timer keeps running, the same as
[an error in a handler](https://www.superlogical.com/rex/docs/automate/lua-events.md#handler-failures).

A script can have at most 1,024 timers at once. A cancelled timer, or a
`rex.after` timer that has already run, doesn't count. Going past the limit
stops the script with status 1, even inside `pcall`, because it almost always
means a handler creates a timer for every event and never cancels it.

## Running Commands

`rex.exec` runs a command and returns what it printed and how it exited:

```lua
local r, err = rex.exec{ "git", "rev-parse", "--abbrev-ref", "HEAD" }
if r == nil then error(err, 0) end

if r.ok then
  rex.session.set_block_label{
    block_id = rex.block_id,
    label    = r.stdout:gsub("\n$", ""),
  }
end
```

The command is a list: the program, then each of its arguments. No shell
reads it, so a path with spaces or a value from `rex.args` needs no quoting.
To use a shell, ask for one: `{ "sh", "-c", line }`.

A command that exits with a non-zero status still returns a result. Check
`r.ok`, or `r.code` for the exact status, and `r.stderr` for what it printed
to standard error. `rex.exec` only returns `nil` and an error message when the
command couldn't run at all, such as when the program isn't installed.

Options go in a second table:

```lua
local r = rex.exec({ "make", "test" }, {
  cwd     = "/src/app",
  env     = { CI = "1" },
  timeout = 300,
})
```

The [Lua API reference](https://www.superlogical.com/rex/docs/reference/lua.md#rexexecargv-opts) lists every option
and field.

### Streaming Output

`rex.exec.start` starts a command without waiting for it and returns a
handle. Pass a function as `stdout` or `stderr` to receive output as it
arrives:

```lua
local h = assert(rex.exec.start({ "make", "build" }, {
  stdout = function(data) io.write(data) end,
}))

local r = h:wait()
```

A command lasts as long as the part of the script that started it. When the
script's top level finishes, or a handler or timer returns, Rex stops the
commands it started. Pass `detach = true` for a command that should keep
running.

To watch a command run, start it in a terminal block instead, as in
[the example below](#example-run-tests-in-a-split).

## Example: Run Tests in a Split

This script runs a command in a new split beside the current terminal. If the
command fails, it zooms the split so the failure is front and center.

```lua
-- tests.lua
local cmd = rex.args.cmd or "make test"

local split, err = rex.session.new_split{
  direction = "horizontal",
  side      = "after",
  focus     = false,
  layout    = rex.layout.block{
    flavor  = "com.superlogical.terminal.shell",
    options = {
      command = { "sh", "-c", cmd },
      exit    = { on_completion = false },  -- keep the output
    },
  },
}
if err ~= nil then error(err, 0) end
local block_id = split.block_ids[1]

-- Wait up to ten minutes for the command to exit
local ev = rex.wait(
  "terminal.child_exited", { block_id = block_id }, 600)
if ev == nil then error("timed out waiting for: " .. cmd, 0) end

if ev.exit_code ~= 0 then
  rex.session.zoom_block{ block_id = block_id, zoom = true }
end

return { block_id = block_id, exit_code = ev.exit_code }
```

```
$ rex do tests.lua cmd="go test ./..."
{"block_id":"block:01a10deb-c752-789f-9add-5dfd3a5b3214","exit_code":1}
```

## Example: Export the Session

This script keeps a copy of the session's layout in a JSON file, written
every five seconds, for another program to read.

```lua
-- export.lua
local path = rex.args.path or "session.json"

local function export()
  local view, err = rex.session.view{}
  if err ~= nil then error(err, 0) end

  local f = assert(io.open(path, "w"))
  f:write(rex.json.encode(view, { indent = true }))
  f:close()
end

export()
rex.every(5, export)
```

```
$ rex do export.lua path=/tmp/work.json
running: 1 timer; Ctrl-C to stop
```

`rex.json.encode` converts a table the same way an API call does, so the file
matches what the server returned. `rex.json.decode` reads it back.

If the server is briefly unreachable, `export` raises an error, which is
printed, and the timer tries again five seconds later.

---
title: "Lua Events"
canonical_url: https://www.superlogical.com/rex/docs/automate/lua-events
---

# Lua Events

A [Lua script](https://www.superlogical.com/rex/docs/automate/lua-scripts.md) can keep running and react to what
happens in Rex: a terminal changes directory, a program exits, a window
closes, a session is created. A script asks for an event with `rex.on`.

```lua
-- label.lua: label each terminal with the directory it is in
rex.on("terminal.pwd_changed", function(ctx, ev)
  local dir = ev.pwd:match("([^/]+)/?$")
  rex.session.set_block_label{
    block_id = ctx.block_id,
    label    = dir,
  }
end)
```

```
$ rex do label.lua
listening: 1 handler, session work; Ctrl-C to stop
```

Change directory in any terminal in the session and its label will update.

## Handlers

`rex.on` takes the name of an event and a function to call each time that
event happens:

```lua
rex.on(event, function(ctx, ev)
  -- ...
end)
```

A script that calls `rex.on` doesn't exit when it reaches its last line.
`rex do` keeps it running and calls its handlers as events arrive, one at a
time.

Handlers are registered while the script first runs. Calling `rex.on` later,
from inside a handler, is an error.

`ev` holds the event's fields, and `ctx` says where it happened:

| Field | Description |
| --- | --- |
| `ctx.session_id` | The session the event happened in. |
| `ctx.block_id` | The block the event came from, for a block event. |

API calls inside a handler default to the event's session rather than the one
the script was started in, so one handler works across every session it hears.

## Event Names

There are three kinds of event name:

| Name | Matches |
| --- | --- |
| `block_closed`, `window_created`, ... | A session or server event, by its type. |
| `terminal.pwd_changed`, `terminal.bell`, ... | One kind of event from a block. |
| `block_event` | Every event from every block. |

A block event is named for the kind of block and the event, joined by a dot.
`terminal.bell` is short for `com.superlogical.terminal.bell`. Either spelling
works.

These are common events:

| Event | When | Fields on `ev` |
| --- | --- | --- |
| `terminal.pwd_changed` | A terminal changes directory. | `pwd`, a `file://` URL |
| `terminal.title_changed` | A terminal's title changes. | `title` |
| `terminal.bell` | A terminal rings its bell. | |
| `terminal.child_exited` | A terminal's program exits. | `exit_code` |
| `block_created`, `block_closed` | A block is created or closed. | `block_id` |
| `window_created`, `window_closed` | A window is created or closed. | `window_id` |
| `session_created`, `session_destroyed` | A session is created or destroyed. | |

The [events reference](https://www.superlogical.com/rex/docs/reference/events.md) has the full list.

Rex doesn't check an event name against a list, so a misspelled name is not an
error. The handler is registered and never called.

## Event Scope

By default, a script reacts to events of one session: the one it was run in,
or the one named with `-s`.

```sh
rex do label.lua            # the current session
rex do -s build label.lua   # the session labeled "build"
```

Pass `--all` to hear every session, including ones created after the script
starts:

```sh
rex do label.lua --all
```

## Stopping

A script with handlers runs until one of these happens:

- A handler calls `rex.stop(code)`. `rex do` exits with that status, or 0
  when it is left out.
- The time given with `--for` runs out, such as `--for 10m`.
- The session it hears is destroyed. A script run with `--all` never stops
  this way.
- You press <kbd>Ctrl</kbd>+<kbd>C</kbd>.

```lua
-- Exit with the status of the first program to fail
rex.on("terminal.child_exited", function(ctx, ev)
  if ev.exit_code ~= 0 then rex.stop(ev.exit_code) end
end)
```

## Handler Failures

An error in a handler or a [timer](https://www.superlogical.com/rex/docs/automate/lua-scripts.md#timers) is printed
and the script keeps running, so one bad event doesn't end a long-lived
script:

```
Error: watch.lua:7: attempt to index a nil value
```

Pass `--fail-fast` to exit with status 1 at the first failure instead.

Remember that a failed API call returns `nil` and a message rather than
raising an error. Check the second return value where it matters.

## Handlers or `rex.wait`

[`rex.wait`](https://www.superlogical.com/rex/docs/automate/lua-scripts.md#sleeping-and-waiting) and `rex.on` both
receive events, and suit different scripts.

- Use `rex.wait` when the script does one thing after another: start a
  command, wait for it to exit, then act on the result.
- Use `rex.on` when the script should respond every time something happens,
  for as long as it runs.

`rex.wait` and `rex.sleep` also work inside a handler. No other event is
handled until they return.

## Timers in Handlers

A handler that needs to do something later can start a
[timer](https://www.superlogical.com/rex/docs/automate/lua-scripts.md#timers) and return, rather than sleep and hold
up every other event.

This is useful for events that arrive in bursts. `session_view_changed`
follows almost every change to the layout, so this calls an `export` function
once, half a second after the last change of a burst:

```lua
local pending
rex.on("session_view_changed", function(ctx, ev)
  if pending then pending:cancel() end
  pending = rex.after(0.5, export)
end)
```

A timer's API calls default to the session the script was started in, not
the event's. To act on the event's session, pass `ctx.session_id` along:

```lua
rex.on("session_view_changed", function(ctx, ev)
  if pending then pending:cancel() end
  pending = rex.after(0.5, function()
    export(ctx.session_id)
  end)
end)
```

## Invalid in Configuration Files

`rex.on` only works in a script run with `rex do`. The
[configuration file](https://www.superlogical.com/rex/docs/customize/config.md) is never sent events, so a handler
registered there is reported as a warning and never called.

To have a handler running all the time, start the script when you start your
session, for example from a terminal you keep open for it.

## Example: Notify on Bell

Many programs ring the terminal bell when they finish or need attention. This
script turns a bell in any terminal, in any session, into a macOS
notification that names the session:

```lua
-- notify.lua
local function notify(title, text)
  rex.exec{
    "osascript", "-e",
    string.format("display notification %q with title %q", text, title),
  }
end

rex.on("terminal.bell", function(ctx, ev)
  local view = rex.session.view{}
  notify("Rex", "Bell in " .. view.label)
end)
```

```sh
rex do notify.lua --all
```

`rex.session.view` is called without a `session_id`, so it describes the
session the bell rang in. That is how one handler can name whichever session
needs you.

---
title: "Shell Scripting"
canonical_url: https://www.superlogical.com/rex/docs/automate/shell-scripting
---

# Shell Scripting

You can also use the [`rex` CLI](https://www.superlogical.com/rex/docs/automate/cli.md) with typical shell scripts
to automate everything in Rex:

```sh
#!/bin/sh
rex new work
rex split -s work -- htop
rex run -s work --label logs -- tail -f app.log
```

For anything beyond a few commands in a row, consider a
[Lua script](https://www.superlogical.com/rex/docs/automate/lua-scripts.md) instead. Lua can do everything on this
page with one connection to the server and without parsing JSON.

## Keeping Track of IDs

Commands that create something print its ID with `--json`. Save the ID and
pass it to later commands, so the script keeps working when a label changes
or focus moves.

```
$ rex split --json -- htop
{
  "anchor_block_id": "block:01a10e00-7268-78c6-8b85-86c56d6a53a9",
  "block_ids": [
    "block:01a10e00-729d-7c38-90f7-d6568611b1c4"
  ],
  "split_ids": [
    "split:01a10e00-72ab-7157-b9e0-0d4c2a8a1a68"
  ],
  "revision": 2
}
```

[`jq`](https://jqlang.org) is the easiest way to pull the ID out:

```sh
block=$(rex split --json -- htop | jq -r '.block_ids[0]')
rex zoom -b "$block"
```

| Command | Creates | ID is at |
| --- | --- | --- |
| `rex new --json` | A session with one window and one terminal | `.session_id` |
| `rex run --json` | A terminal in a new window | `.block_ids[0]`, `.window_id` |
| `rex split --json` | A terminal beside an existing one | `.block_ids[0]` |

A label is a fine alternative when the script sets it. Give the block a label
with `--label` and use that as the target:

```sh
rex split --label logs -- tail -f app.log
rex capture -b logs
```

## Building a Layout

`rex new` creates a session, `rex run` adds a window to it, and `rex split`
splits an existing block.

```sh
#!/bin/sh
# Open a project: an editor with a shell below it, and a second
# window that follows the logs.
rex new api --cwd ~/src/api -- nvim
rex split -s api --split=below --ratio 30 --cwd ~/src/api
rex run -s api --label logs --focus=false -- tail -f ~/src/api/app.log
```

These options are shared by `rex run` and `rex split`:

| Option | Does |
| --- | --- |
| `-- COMMAND` | Runs a command instead of an interactive shell. |
| `--cwd DIR` | Sets the working directory. |
| `--label TEXT` | Labels the new block. |
| `--split DIRECTION` | Places the block `right`, `left`, `above`, or `below` its anchor. |
| `--ratio PCT` | Gives the new block that percentage of the split. |
| `--focus=false` | Leaves focus where it was. |
| `--keep-open` | Keeps the terminal open after its command exits. |

A command runs with the environment of your login shell, so your `PATH`
applies but aliases don't. It isn't parsed by a shell. To use pipes or `&&`,
run a shell explicitly:

```sh
rex run -- sh -c 'make build && make test'
```

## Typing Into a Terminal

`rex send` types text into a terminal, and `rex send-key` presses keys:

```sh
rex send -b logs 'make test'
rex send-key -b logs enter
```

`rex send` doesn't press Enter for you. Follow it with `rex send-key enter`,
or include the newline in the text. It also reads from stdin:

```sh
printf 'make test\n' | rex send -b logs
```

`rex send-key` uses the same key names as
[keyboard shortcuts](https://www.superlogical.com/rex/docs/customize/keyboard-shortcuts.md#writing-keys), so
`rex send-key ctrl+c` interrupts whatever is running.

## Reading a Terminal

`rex capture` prints a terminal's screen and its scrollback:

```
$ rex capture -b logs --trim
hello from logs
```

| Option | Does |
| --- | --- |
| `--trim` | Removes trailing blank lines. |
| `--unwrap` | Joins lines the terminal wrapped. |
| `--format html` | Keeps colors and styling, as HTML. |
| `--format vt` | Keeps the terminal's control sequences. |

A terminal keeps about 64 MiB of scrollback. That is roughly 100,000 lines of
plain text at 80 columns, and fewer in a wider terminal or with styled output.
Once the limit is reached, the oldest lines are dropped. To keep everything a
long command prints, redirect its output to a file.

Full-screen programs, such as editors, pagers, and many TUI apps, draw on a
separate screen that has no scrollback. While one is running, `rex capture`
returns only what that program shows right now. Any history it has, like an
editor's file or a chat app's earlier messages, stays inside the program.

## Running a Command and Waiting

Add `--wait` to `rex run` or `rex split` to wait for the command to finish.
The `rex` command then exits with the command's own status:

```sh
if rex run --wait -- make test; then
  echo "tests passed"
fi
```

`rex wait` does the same for a terminal that is already running, given its
ID or label:

```sh
block=$(rex split --json -- make test | jq -r '.block_ids[0]')
# ... do other things ...
rex wait "$block"
echo "make exited with $?"
```

`rex wait` also accepts a window or a session, and returns when it closes.

Use `--for` to give up after a while. A wait that times out exits with
status 1:

```sh
rex wait --for 10m "$block" || echo "failed or timed out"
```

## Performing Actions

`rex do` performs an action by name, with arguments as `key=value` pairs. It
runs both the app's actions and the
[custom actions](https://www.superlogical.com/rex/docs/customize/actions.md) in your configuration file:

```sh
rex do dev_layout file=app.log
rex do pane.split direction=right
```

Actions such as `pane.split` are performed by the app, so the app must be
running, with Remote Control turned on in its settings.

## Watching Events

`rex events` prints events as they happen and runs until interrupted. With
`-s` it follows one session:

```
$ rex events -s work
14:38:07  block_created         …65951c  session …8f9d5d
14:38:07  session_view_changed  …8f9d5d  revision 11
14:38:07  block_event           …65951c  com.superlogical.terminal.bell {}
```

With `--json` it prints one JSON object per line, ready for `jq`. This prints
the ID of each terminal that rings its bell:

```sh
rex events -s work --json |
  jq -r 'select(.payload.event.payload.name == "bell")
         | .payload.event.payload.block_id'
```

The [events reference](https://www.superlogical.com/rex/docs/reference/events.md#events-on-the-cli) describes the
JSON and lists every event. To act on events rather than print them,
[Lua event handlers](https://www.superlogical.com/rex/docs/automate/lua-events.md) are much less work than a
pipeline.

## Example: Run Tests in a Split

This script runs a command in a split beside the current terminal. If the
command fails, it zooms the split and exits with a failure.

```sh
#!/bin/sh
# tests.sh
cmd="${1:-make test}"

block=$(rex split --keep-open --focus=false --json -- sh -c "$cmd" |
  jq -r '.block_ids[0]')

if ! rex wait --for 10m "$block"; then
  rex zoom -b "$block" --on
  exit 1
fi
```

```sh
./tests.sh 'go test ./...'
```

The [same script in Lua](https://www.superlogical.com/rex/docs/automate/lua-scripts.md#example-run-tests-in-a-split)
is a useful comparison.

---
title: "Server API"
canonical_url: https://www.superlogical.com/rex/docs/automate/server-api
---

# Server API

The server documents its own API. You can list every method, read a method's
arguments and result, and call it, all from the CLI:

```sh
rex api list
rex api describe session.new_split
rex api call server.version
```

Because the documentation comes from the server you are connected to, it
always matches that server's version.

The CLI and Lua APIs all derive dynamically from the server API it is
connected to, so these commands are also important for scripting and
automation at a higher level.

## Discovering Methods

`rex api list` prints every method with a short description:

```
$ rex api list
METHOD              DESCRIPTION
block.close         Close a block within a session. ...
block.event         Deliver an external event to a block within a session.
block.method        Invoke a typed method on a block within a session ...
...
```

A second table lists the [methods that blocks provide](#block-methods), such
as reading a terminal's screen.

Use `-q` to print only the names, which is handy for searching:

```
$ rex api list -q | grep label
client.set_label
session.set_block_label
session.set_label
session.set_window_label
```

## Reading a Method's Documentation

`rex api describe` prints everything the server knows about one method: what
it does, the arguments it takes, what it returns, and an example of each.

```
$ rex api describe session.set_block_label
Update a block's human-readable display label. ...

Method: session.set_block_label

Input schema:
{
  "type": "object",
  "properties": {
    "block_id": { "type": ["string", "null"] },
    "label": { "type": "string" },
    "session_id": { "type": "string" }
  },
  "required": ["session_id"],
  "additionalProperties": false
}

Output schema:
{
  "type": "object",
  "properties": {
    "revision": { "type": "integer", "minimum": 0 }
  },
  "required": ["revision"],
  "additionalProperties": false
}

Request example:
{
  "session_id": "session:<uuid>",
  "block_id": "block:<uuid>",
  "label": "shell"
}

Response example:
{"revision": 42}
```

The schemas are [JSON Schema](https://json-schema.org). To read one:

- `properties` lists each argument and its type.
- `required` lists the arguments that must be present.
- `additionalProperties: false` means a misspelled argument is an error
  rather than being ignored.

The request example is usually the quickest way to see how a call should
look. Some methods also print validation notes, which describe rules the
schema can't express.

## Calling a Method

`rex api call` calls a method and prints its result as JSON:

```
$ rex api call server.version
{
  "rex_version": "0.1.0",
  "platform": "darwin_arm64",
  ...
}
```

Arguments are a single JSON object. Pass it directly, read it from a file
with `@`, or read it from stdin with `-`:

```sh
rex api call -s work session.set_label '{"label": "api"}'
rex api call session.create @layout.json
echo '{"label": "work"}' | rex api call session.create -
```

Most `session.` methods need a `session_id`. Name the session with `-s` and
`rex api call` adds it to the arguments for you:

```sh
rex api call -s work session.view
```

Unlike other `rex` commands, `rex api call` doesn't default to the session
you are in. Inside a Rex terminal, pass `-s "$REX_SESSION"` to use it.

Other IDs are never filled in. A method that acts on a block needs its
`block_id` in the arguments.

The result is indented when printed to a terminal and kept on one line when
piped, so it is ready for [`jq`](https://jqlang.org):

```sh
rex api call -s work session.view | jq '.windows | length'
```

### Errors

A call that fails prints the server's message and exits with a nonzero
status:

```
$ rex api call -s work session.set_block_label '{"lable": "x"}'
Error: ... input: validating: block_id is required (status=400 code=bad_request)
```

The `code` says what kind of failure it was. `bad_request` means the
arguments were wrong, and `method_not_found` means the server has no method
by that name, which can happen when the server is older than the docs you
are reading.

## Calling From Lua

`rex.call` takes a method's full name and its arguments as a table:

```lua
local version = rex.call("server.version")
print(version.rex_version)
```

Every `session.` method that acts on one session also has a helper under
`rex.session`, named after the method:

| API method | Lua |
| --- | --- |
| `session.view` | `rex.session.view{}` |
| `session.set_label` | `rex.session.set_label{ label = "work" }` |
| `session.new_split` | `rex.session.new_split{ ... }` |

The arguments and result are the same as `rex api describe` shows, written as
Lua tables instead of JSON. The helpers fill in `session_id` when it is left
out. `rex.call` doesn't, so pass `rex.session_id` yourself when calling a
`session.` method through it.

A few `session.` methods have no helper because they don't act on an existing
session, such as `session.list` and `session.create`. Call those with
`rex.call`.

A failed call returns `nil` and the server's error message:

```lua
local result, err = rex.session.set_block_label{ lable = "x" }
if err ~= nil then error(err, 0) end
```

The [Lua API reference](https://www.superlogical.com/rex/docs/reference/lua.md#calling-rex) covers these functions in
full.

## Block Methods

Blocks provide methods of their own. `rex api list` prints them in a second
table, named for the kind of block that provides them:

```
Block methods:
CREATOR                    METHOD                             DESCRIPTION
com.superlogical.terminal  com.superlogical.terminal.format   Format the current terminal screen ...
com.superlogical.terminal  com.superlogical.terminal.pwd      Return the working directory ...
com.superlogical.terminal  com.superlogical.terminal.title    Return the current terminal title.
...
```

Describe one the same way as any other method:

```sh
rex api describe com.superlogical.terminal.format
```

To call one, pass the `block_id` to call it on, and put the method's own
arguments under `args`:

```sh
rex api call -s work com.superlogical.terminal.format \
  '{"block_id": "block:01a1...", "args": {"trim": true}}'
```

In Lua, `rex.block.call` builds that wrapper for you and fills in the current
block:

```lua
local terminal = "com.superlogical.terminal"
local screen = rex.block.call(terminal, "format", { trim = true })
print(screen.content)
```

## Machine-Readable Documentation

Both discovery commands take `--json`, which prints the server's own
description of its API:

```sh
rex api list --json       # every method, with schemas and examples
rex api describe session.view --json
```

Each method has a `name`, a `description`, an `input_schema`, an
`output_schema`, and, where the server has them, a `request_example`, a
`response_example`, and `validation_notes`.

This is useful for generating bindings, for checking a script against a
server before running it, and for giving a coding agent the exact shape of
the API it is about to call.

---
title: "Program Status Protocol"
canonical_url: https://www.superlogical.com/rex/docs/build/program-status
---

# Program Status Protocol (OSC 7501)

A terminal escape sequence that lets a program tell the terminal what it
is doing: idle, working, waiting on the user, finished, or failed, and
why.

The protocol shares state and leaves presentation to the terminal. It
does not say how anything is shown, and it avoids terms tied to
particular GUI elements ("notification", "attention", "focus", etc.).

```
# Terraform has finished planning and is waiting for the user to approve
# the apply. The message decodes to "Apply 3 to add, 1 to change, 0 to destroy?".
ESC ] 7501 ; state=blocked:kind=permission:app=terraform:msg=QXBwbHkgMyB0byBhZGQsIDEgdG8gY2hhbmdlLCAwIHRvIGRlc3Ryb3k/ ESC \
```

**Note:** This is a generic terminal protocol. While it is used for rich functionality
within Rex, the protocol itself has no Superlogical-specific functionality
or language. It fills a broad need within terminal applications.

## Motivation

Long-running work is common in terminals: builds, deployments, package
upgrades, data processing, coding agents. Such a program alternates
between working on its own, waiting on the user, and finishing.
Meanwhile the user is usually doing something else and wants to know
when the work finishes or needs them.

Existing sequences cover only slices of this. OSC 9;4 progress can say
busy or not busy, but not that a program is waiting on the user or what
it is working on. Desktop notifications are one-time events with free-form
text. Tools that show many programs at once fall back to
heuristics such as parsing window titles or matching screen contents, or
to program-specific plugins. [Relationship to other sequences](#relationship-to-other-sequences)
goes into detail.

With this protocol the program that knows its state reports it, and the
terminal keeps a current picture of every program running in it.

## In Rex

When your program adopts the Program Status Protocol in Rex, we show
the current active status in a number of ways today:

- Sessions with completed or blocked tasks show up with an indicator in
  the session picker.
- Tab headers for unfocused tabs show an in-progress spinner and a variety
  symbols for blocked states.
- Lua scripts can react to `terminal.program_status_changed` [events](https://www.superlogical.com/rex/docs/automate/lua-events.md).

In the future, we plan to expand this functionality in many more ways,
such as push notifying our mobile applications, a unified priority inbox
view, and many more.

## Conventions

The key words MUST, MUST NOT, SHOULD, and MAY are used as described in
RFC 2119 and RFC 8174, and only when they appear in capitals.

| Name  | Bytes                                                  |
| ----- | ------------------------------------------------------ |
| `ESC` | `0x1B`                                                 |
| `OSC` | `ESC ]`, that is `0x1B 0x5D`                           |
| `ST`  | `ESC \`, that is `0x1B 0x5C`, or `BEL`, that is `0x07` |

A **terminal** is one window, tab, split, or pane attached to one
pseudo-terminal. Rules that say "the terminal" apply to each one
separately and are implemented by whatever emulates it. A **program**
writes to a terminal. A **report** is one sequence sent by a program. A
**record** is what the terminal stores as a result.

## Syntax

```
OSC 7501 ; pairs ST        report
OSC 7501 ; ? ST            feature detection

pairs    := pair (":" pair)*
pair     := key "=" value
key      := [a-z]+
value    := [A-Za-z0-9_.,+/=-]*
```

- The body is a list of `key=value` pairs separated by `:` and nothing
  else. The value character set excludes `:` and `;`, so nothing needs
  escaping. Whitespace around keys and values is removed.
- A malformed pair (no `=`, empty key, or a byte outside the value set)
  is skipped and the rest of the report is still processed.
- Unknown keys MUST be ignored. This is how the protocol is extended.
- If a key repeats, the last value wins.
- Free-text values (`msg`, `title`) are standard base64 of UTF-8 text.
  Padding is optional. The decoded text MUST NOT contain control
  characters (`U+0000`–`U+001F`, `U+007F`, `U+0080`–`U+009F`), including
  newline or tab. These values are meant to be single line values.
- A report is discarded whole if it exceeds one of the [limits](#limits), if its
  base64 does not decode, or if decoded text contains a control
  character. Nothing from a discarded report is applied.

## Records and ids

A terminal holds a **set of records**, one per id. Each record holds
exactly the keys of the last report that used that id.

```
id      := segment ("/" segment)*
segment := [A-Za-z0-9_.+-]{1,32}
```

- A report with no `id` addresses the **root record**. A program that
  only reports its own state uses the root record and can skip the rest
  of this section.
- **Each report replaces its record completely.** A key missing from the
  report is missing from the record afterwards. A program that wants
  `app` or `title` on a record includes them in every report.
- A report whose `id` does not match this grammar is ignored. Falling
  back to the root record would let a malformed id overwrite it.
- `/` gives records a parent and child relationship: `build/test` is a
  child of `build`, and `build` is a child of the root record. A parent
  does not have to exist. Terminals MUST use
  this relationship for clearing (see [States](#states)) and `app` inheritance
  (see [Keys](#keys)), and MAY use it for grouping. It means nothing else.
- Root and child records exist at the same time. A program can be `idle`
  at its prompt while one of its workers is `blocked`.
- Records belong to the terminal, not to a screen. Switching between the
  primary and alternate screen has no effect on them. A full reset
  (`RIS`) removes every record. A soft reset (`DECSTR`) does not.

## States

The `state` key is required. A report with no `state`, or one the
terminal does not recognize, is ignored.

| `state`   | Meaning | Lifetime |
| --------- | ------- | -------- |
| `idle`    | At rest, waiting for the user's next instruction. | Until replaced or cleared. |
| `working` | Running. May carry `progress`. | Until replaced, cleared, process exit, or next shell prompt. |
| `done`    | Finished a piece of work. The result is ready to look at and the user has not seen it yet. | Until replaced or cleared. Survives process exit and shell prompt. |
| `blocked` | Cannot continue until the user does something. `kind` says what, `msg` says why. May carry `progress`. | Same as `working`. |
| `error`   | Failed and stopped. | Same as `done`. |
| `clear`   | Not a state. Removes the addressed record and every record beneath it. With no `id`, removes every record on the terminal. | |

When the user interrupts or cancels a program, report `idle`. A program
that exits as soon as it finishes SHOULD report `done` or `error` right
before exiting, so a record is left behind for the user to find.

**Lifetime.** Records are tied to events the terminal already sees.
There is no heartbeat, and terminals MUST NOT require re-sending to keep
a record alive. When the process attached to the terminal exits, or a new
shell prompt begins (OSC 133 `A`), the terminal MUST drop `working` and
`blocked` records and MAY drop `idle` ones. `done` and `error` survive
both events. The terminal decides when to stop showing them, for example
when the user returns to the terminal and presses a key.

## Keys

| Key        | Value | Notes |
| ---------- | ----- | ----- |
| `state`    | `idle` \| `working` \| `done` \| `blocked` \| `error` \| `clear` | Required. An unrecognized value causes the whole report to be ignored, so a state added later never turns into `idle` on an older terminal. |
| `id`       | path, see [Records and ids](#records-and-ids) | Absent means the root record. |
| `kind`     | `permission` \| `question` \| `auth` | With `blocked` only, ignored otherwise. `permission`: approval to do something. `question`: the user must type an answer. `auth`: a login, token, or credential. Unrecognized values are treated as absent. |
| `progress` | integer `0` to `100` | With `working` or `blocked`, ignored otherwise. Absent means indeterminate: the program is busy but has no percentage to report. Anything else is treated as absent. |
| `app`      | `[A-Za-z0-9_.+-]{1,32}` | Stable identifier for the program, such as `cargo`, `terraform`, or `claude-code`. A key for grouping, filtering, icons, etc. This is not a label; `title` is the label. A record without `app` takes it from its nearest ancestor that has one. A value outside the character set is treated as absent. |
| `title`    | base64 UTF-8 | Short human-readable label for the record. For programs that report several records. The root record's label is normally the window title. |
| `msg`      | base64 UTF-8 | One human-readable line: what the record is doing, waiting for, or finished. Terminals MAY shorten it and MUST NOT read meaning into it. |

## Feature detection

```
program  → OSC 7501 ; ? ST
terminal → OSC 7501 ; ? : states=idle,working,done,blocked,error : kinds=permission,question,auth ST
```

Detection is optional. Terminals MUST ignore unknown OSCs, so a program
MAY send reports without querying first, for example when it cannot
read replies.

A supporting terminal MUST reply with `?`. It MAY add `states`, the
comma-separated `state` values it accepts (not counting `clear`), and
`kinds`, the `kind` values it accepts.

A reply without `states` means exactly `idle`, `working`, `done`, `blocked`,
and `error`. A reply without `kinds` means exactly `permission`, `question`,
and `auth`. Those lists are fixed and do not change in later revisions.
This is how states and kinds are added: a program that wants a state from a
later revision checks the list and falls back to one that is present. Programs
MUST ignore pairs in the reply that they do not understand.

A program that gets no reply within a time of its choosing treats the
protocol as unsupported. To avoid waiting, send primary device
attributes (`CSI c`) right after the query. Every terminal answers that,
so if its reply arrives first, the protocol is unsupported.

## Security

Everything in a report is untrusted input from a program that already
controls the screen.

- Terminals MUST refuse a report whose decoded `msg` or `title` contains
  a control character, MUST NOT treat either as markup, and SHOULD disarm
  text direction overrides and other invisible formatting characters when
  showing them outside the terminal grid.
- The only bytes a terminal writes back are the feature detection
  reply, which contains nothing from any report. Ids, titles, and
  messages are never sent back, and there is no way to read records.
- Anything a terminal shows from a record SHOULD say which terminal it
  came from, so a program cannot pretend to be a program in another
  terminal.
- A program can change state as fast as it can write. Terminals SHOULD
  rate-limit anything a record causes outside the terminal, such as
  desktop notifications or sounds.

## Limits

Every limit is a hard cap on what a terminal has to store or do for one
report, so a hostile or buggy program cannot make memory grow or cause
unbounded work. Terminals MAY choose lower limits. A report that breaks a
limit is discarded whole. Check every pair before touching any stored
record.

| Item | Limit |
| ---- | ----- |
| Whole sequence, `OSC` through `ST` | 4096 bytes. The largest legal report is under 3300. |
| Key length | 16 bytes |
| `msg` | 2732 bytes encoded, 2048 decoded. Check the encoded size before decoding. |
| `title` | 256 bytes encoded, 192 decoded |
| `app` | 32 bytes |
| `id` | 128 bytes total, 32 per segment, 8 levels deep |
| Records per terminal | 256. Terminals MUST support at least 64. When a report would create a record past the cap, the record that was updated least recently is removed to make room. |

## Relationship to other sequences

**[OSC 9;4](https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC) (progress).**
Says only busy, a percentage, or error. It cannot say that a program is
waiting on the user or what it wants, and it carries no message or
identity. It also shares the number 9 with iTerm2's notification sequence,
which has caused garbled output in terminals that know only one of the two.
A program may send both. Terminals MAY map OSC 9;4 to the root record of
this protocol. Because each report replaces its record completely, a
mapped OSC 9;4 would wipe out the `kind` and `msg` of a real report. A
terminal SHOULD stop mapping OSC 9;4 once it has received an OSC 7501
report, until the next full reset.

**OSC 9, [OSC 99](https://sw.kovidgoyal.net/kitty/desktop-notifications/), OSC 777 (desktop notifications).**
A notification is a one-time event. Once it is shown, the terminal has no
record of what is currently true, so it cannot tell that a program is
still waiting or that it recovered. A notification also implies
interrupting the user, which is not always the right behavior. A terminal MAY
produce a notification when a record changes.

**[OSC 133](https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md) (shell integration).**
Marks where prompts and commands begin and end. It is emitted by the
shell, not the program, so it can only say that a command is running or
has finished with an exit code, never what the program is doing or
whether it is stuck.

**OSC 0 and 2 (window title).**
A single free-form string with no structure. Programs that need to show
state today overload it with glyphs and spinners, but this isn't a reliable
machine-readable format for progress.

**[OSC 21337](https://iterm2.com/documentation-session-status.html) (iTerm2 session status) and OSC 9999 (Orca).**
Earlier work on the same idea. They describe presentation, with colors
and indicators, or require JSON parsing, and overall I believe are
not aligned with the right goals.

## Examples

Base64 values are shown decoded in a trailing comment.

### One program

```
OSC 7501 ; state=working:app=brew:msg=SW5zdGFsbGluZyB1cGRhdGVz ST                              # Installing updates
OSC 7501 ; state=blocked:kind=auth:app=brew:msg=UGFzc3dvcmQgcmVxdWlyZWQgdG8gaW5zdGFsbCB1cGRhdGVz ST  # Password required to install updates
OSC 7501 ; state=working:app=brew:msg=SW5zdGFsbGluZyB1cGRhdGVz ST                              # Installing updates
OSC 7501 ; state=done:app=brew:msg=VXBncmFkZWQgMTIgcGFja2FnZXM= ST                             # Upgraded 12 packages
```

Every report repeats `app` because a report replaces its record
completely. The `done` record survives the shell prompt that follows.

### Several records

```
OSC 7501 ; state=working:app=deploy:msg=RGVwbG95aW5nIHYyLjQuMQ== ST                                               # Deploying v2.4.1
OSC 7501 ; state=working:id=us-east:title=VVMgRWFzdA==:progress=40:msg=UHVzaGluZyBpbWFnZQ== ST                    # US East / Pushing image
OSC 7501 ; state=blocked:kind=permission:id=eu-west:title=RVUgV2VzdA==:msg=QXBwcm92ZSBkZXBsb3kgdG8gZXUtd2VzdCAocHJvZHVjdGlvbik/ ST  # EU West / Approve deploy to eu-west (production)?
OSC 7501 ; state=done:id=us-east:title=VVMgRWFzdA==:msg=SGVhbHRoeQ== ST                                           # US East / Healthy
OSC 7501 ; state=clear:id=us-east ST
OSC 7501 ; state=clear:id=eu-west ST
OSC 7501 ; state=done:app=deploy:msg=RGVwbG95ZWQgdG8gMyByZWdpb25z ST                                              # Deployed to 3 regions
```

The region records take `app=deploy` from the root when displayed. While
`eu-west` is blocked the root still says `working`. Both are true, and
the terminal decides which to show.

### A shell function

```sh
status() {
  printf '\e]7501;state=%s:msg=%s\e\\' "$1" "$(printf '%s' "$2" | base64 | tr -d '\n')"
}
status working "Syncing photos"
rsync -a ~/Photos backup:/photos && status done "Photos synced" || status error "rsync failed"
```

## Revision history

| Date       | Revision | Changes |
| ---------- | -------- | ------- |
| 2026-10-10 | 0.4      | Feature detection reply may list supported states and kinds. Say why control characters are refused and what `app` is for. Remove terminfo. |
| 2026-10-07 | 0.3      | Absent `progress` means indeterminate. Programs may send reports without feature detection. |
| 2026-10-06 | 0.2      | Clarify MUST NOT and apply keywords in more places. |
| 2026-09-28 | 0.1      | Initial draft. |

---
title: "CLI Commands"
canonical_url: https://www.superlogical.com/rex/docs/reference/cli
---

# CLI Commands

This page lists every public `rex` command and flag. See [The CLI](https://www.superlogical.com/rex/docs/automate/cli.md) for how to use them. Anything tagged [Snapshot](https://www.superlogical.com/rex/docs/linux/installation.md#channels) is on the snapshot channel and not yet in preview.

<div class="reference-page commands" hidden></div>

## Common Commands

### `rex new [LABEL] [-- COMMAND...]`

Create a session.

| Flag | Description |
| --- | --- |
| `-c, --cwd string` | Working directory for the terminal. |
| `--keep-open` | Keep the terminal open after its command exits. |
| `--shell SHELL` | Run COMMAND through SHELL's login environment (none starts it directly). |
| `--interactive` | Also read the shell's interactive startup files, such as ~/.zshrc. |
| `--window string` | Label for the first window. |
| `--json` | Output the created session as JSON. |

### `rex ls`

List sessions.

| Flag | Description |
| --- | --- |
| `--json` | Output sessions as JSON. |
| `-q, --quiet` | Print IDs only, one per line. |
| `--short` | Show short IDs (`--short=false` shows full IDs). Defaults to `true`. |

### `rex run [flags] -- COMMAND...`

Run a command in a new block.

Run a command in a new terminal block.

COMMAND runs through your login shell's environment but not its interactive configuration, so profile variables and PATH apply and aliases do not. Use `--interactive` to read that configuration too, such as ~/.zshrc, for a program whose directory is added to PATH there. Use `--shell` to pick another shell, or `--shell=none` to start COMMAND directly.

By default, Rex places the block in a new window. Use `--split` to place it beside an existing block. Use `--focus=false` to preserve the active window. With `--wait`, Rex writes the creation result, then waits and returns the terminal child's exit status.

| Flag | Description |
| --- | --- |
| `--split DIRECTION` | Place the new block in DIRECTION relative to the anchor (right, left, above, or below). |
| `--ratio PCT` | Give the new block PCT percent of the split. Defaults to `50`. |
| `-b, --block BLOCK` | Use BLOCK as the split anchor (defaults to focused). |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |
| `-c, --cwd string` | Working directory for the terminal. |
| `--keep-open` | Keep the terminal open after its command exits. |
| `--shell SHELL` | Run COMMAND through SHELL's login environment (none starts it directly). |
| `--interactive` | Also read the shell's interactive startup files, such as ~/.zshrc. |
| `--label TEXT` | Set the new block label to TEXT. |
| `--focus` | Focus the new block. Defaults to `true`. |
| `--json` | Output the creation result as JSON. |
| `--wait` | Wait and return the terminal child's exit status. |

```sh
rex run -- htop
rex run --wait -- go test ./...
rex run --split=below --ratio 30 -- tail -f app.log
rex run -b editor --split=left -- lazygit
rex run -s build -- make
rex run --interactive -- opencode
rex run --shell=none -- /opt/tool/bin/x
```

### `rex split [flags] [-- COMMAND...]`

Split the focused block.

Create a terminal block for a shell or command.

A COMMAND runs through your login shell's environment but not its interactive configuration. Use `--interactive` to read that configuration too, `--shell` to pick another shell, or `--shell=none` to start COMMAND directly.

By default, Rex splits the focused block and places the new block on its right. Use `-b` BLOCK to select another anchor or `--split` to choose another direction. Use `--focus=false` to preserve the current focus. With `--wait`, Rex writes the creation result, then waits and returns the terminal child's exit status. Use `rex run` to place the block in a new window.

| Flag | Description |
| --- | --- |
| `--split DIRECTION` | Place the new block in DIRECTION relative to the anchor (right, left, above, or below). Defaults to `right`. |
| `--ratio PCT` | Give the new block PCT percent of the split. Defaults to `50`. |
| `-b, --block BLOCK` | Use BLOCK as the split anchor (defaults to focused). |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |
| `-c, --cwd string` | Working directory for the terminal. |
| `--keep-open` | Keep the terminal open after its command exits. |
| `--shell SHELL` | Run COMMAND through SHELL's login environment (none starts it directly). |
| `--interactive` | Also read the shell's interactive startup files, such as ~/.zshrc. |
| `--label TEXT` | Set the new block label to TEXT. |
| `--focus` | Focus the new block. Defaults to `true`. |
| `--json` | Output the creation result as JSON. |
| `--wait` | Wait and return the terminal child's exit status. |

```sh
rex split
rex split --split=below
rex split -b editor --split=left -- lazygit
rex split --wait -- make
rex split -- htop
```

### `rex do (ACTION | SCRIPT | -e CODE) [KEY=VALUE...]`

Perform an action or run a Lua script.

Perform an action by name, or run a Lua script.

A name without a dot is an action init.lua defines with rex.action. A name that starts with a client prefix, such as pane.split, is performed by the targeted client. A name that starts with a block type's creator name, such as com.superlogical.terminal.reset, is a block action or method. Run "rex actions" to list them.

To run a Lua script instead, give its path. The first argument is a script path when it contains a slash (or a backslash on Windows) or ends in .lua. Use - to read the script from stdin, or `-e` to give it on the command line. A script can call the same API as init.lua. Its return value prints the same way as an action's result. In a script, rex.invoke runs the actions the script defines and asks the server to run any others.

A script can also call the hosts that "rex hosts" lists. rex.servers() returns their labels, and rex.server(LABEL) returns the same API helpers as rex, with calls going to that host. `rex do` connects to a host on the script's first call to it, and stays connected until the script ends. `--server` chooses only the server that rex itself calls.

With `--action` NAME, `rex do` runs the script, then runs the action the script defines as NAME and prints that action's result. Pairs are typed against that action's schema. Use this to try an action in init.lua before the server reloads it.

A script that registers rex.on handlers keeps running after its last line, and runs its handlers as events arrive, one at a time. It hears the session that `-s`, `-b`, or the environment names, or every session if none is named. `--all` makes it hear every session, including ones created later. While it runs, rex.session.attach and rex.session.detach change what it hears.

Such a script runs until it calls rex.stop(CODE), and `rex do` exits with CODE, or 0 if it is left out. The script also stops, with status 0, when `--for` DURATION has passed, or when the last session it hears is destroyed, unless it hears every session. Ctrl-C stops it with status 130. A failed handler is reported and the script keeps going. With `--fail-fast`, the first failure exits with status 1.

Any script can pause. rex.sleep(SECONDS) waits for time to pass, and rex.wait(EVENT, FILTER, TIMEOUT) waits for an event, such as the exit of a command the script started. Nothing else in the script runs meanwhile. `--for` does not interrupt a pause at the top level of the script.

Arguments are KEY=VALUE pairs typed against the action's schema. Use `--args` to pass a JSON object instead, or @FILE to read one, or - for stdin. A script reads its arguments from rex.args. Scripts have no schema, so each value's type comes from how it is written: n=2 is a number and name=x is a string.

A client action needs a client. Use `-C` to name one, or REX_CLIENT. With neither, the app client is used when exactly one is connected. An init.lua action's queued client actions go to the same client, and so do a script's calls to rex.client.queue.

| Flag | Description |
| --- | --- |
| `--args JSON` | Pass the arguments as a JSON object (@FILE, or - for stdin). |
| `-s, --session string` | Scope to session SESSION. |
| `-b, --block BLOCK` | Apply the action to BLOCK (default: the focused block). |
| `--origin ORIGIN` | Report the origin as ORIGIN: key, palette, api, or cli. Defaults to `cli`. |
| `-e, --eval CODE` | Run CODE as a Lua script. |
| `-a, --action NAME` | Run the action NAME that the script defines. |
| `--all` | Listen for events from every session. |
| `--for DURATION` | Stop listening for events after DURATION. |
| `--fail-fast` | Exit at the first rex.on handler that fails. |
| `--json` | Print the result as JSON. |

```sh
rex do dev_layout branch=main
rex -C studio do pane.split direction=right
rex do com.superlogical.terminal.reset
rex -C studio do client.tab.goto index=2 --json
rex do ./layout.lua branch=main
rex do - < script.lua
rex do -e 'return rex.call("session.list")' --json
rex do notify.lua --args '{"title":"done"}'
rex do watch.lua --all --for 10m
rex do ~/.config/rex/init.lua --action dev_layout branch=main
```

### `rex send [TEXT]`

Send input to a terminal

| Flag | Description |
| --- | --- |
| `-b, --block string` | Send to block BLOCK. |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |

```sh
rex send 'make test'
rex send -b logs $'\003'
printf 'y\n' | rex send
rex send -s build -w 2 'ls'
```

### `rex send-key KEY...`

Send key presses to a terminal

Each KEY uses Rex's key binding syntax. Join modifiers with +, as in ctrl+c or shift+left. A bracketed name such as [Slash] selects a physical key. By default, Rex sends a press followed by a release for each key. Use `--action` to send one action instead.

Rex interprets printable keys using a US keyboard layout because send-key does not receive physical keyboard input. The generated text and physical key may differ on other layouts. Use `rex send` when exact text matters.

| Flag | Description |
| --- | --- |
| `-b, --block string` | Send to block BLOCK. |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |
| `--action ACTION` | Send one ACTION for each key: press, release, or repeat. |

```sh
rex send-key enter
rex send-key ctrl+c
rex send 'make test' && rex send-key enter
rex send-key -b logs ctrl+c
rex send-key shift+left shift+left
rex send-key --action release ctrl+[KeyQ]
```

### `rex capture`

Print a terminal's screen and scrollback.

Print the screen and scrollback of a terminal block.

When `--block` is omitted, Rex uses REX_BLOCK if it names a live block. Otherwise, it uses the focused block of the default window. Text is the default format. HTML preserves rendered styling. VT preserves terminal control sequences. Output always ends with a newline.

A terminal keeps about 64 MiB of scrollback. That is roughly 100,000 lines of plain text at 80 columns, and fewer in a wider terminal or with styled output. Once the limit is reached, the oldest lines are dropped.

| Flag | Description |
| --- | --- |
| `-b, --block BLOCK` | Select block BLOCK. |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |
| `--format FORMAT` | Output FORMAT: text, html, or vt. Defaults to `text`. |
| `--trim` | Remove trailing blank lines. |
| `--unwrap` | Join soft-wrapped lines. |

```sh
rex capture
rex capture -b logs --trim > screen.txt
rex capture --format=html > screen.html
rex capture -s build -w 2
```

### `rex kill SESSION...`

Kill sessions.

### `rex forward`

Forward TCP ports.

### `rex forward new (-L|-R) [bind_address:]port:host:hostport...`

Forward ports until interrupted.

Forward TCP connections through a Rex server, like ssh `-L` and ssh `-R`.

`-L` ("client's port; server's network") [bind_address:]port:host:hostport listens on this machine on &lt;port&gt;, and connects each local connection to &lt;host&gt;:&lt;hostport&gt; from the server.

`-R` ("server's port; client's network") [bind_address:]port:host:hostport listens on the server on &lt;port&gt;, and connects each remote connection to &lt;host&gt;:&lt;hostport&gt; from this machine.

The bind address defaults to 127.0.0.1; use * for every interface. Port 0 picks a free port. Give `-L` and `-R` as many times as you like to create multiple forwards. They will all be managed by this process and have the same lifetime.

Each forward is a tunnel block in the session chosen with `--session`, or the default session. `rex forward new` runs in the foreground until interrupted, then closes its blocks. It prints nothing while it works unless `-v` is given.

When the connection to the server drops, `rex forward new` reconnects and carries on with the same blocks. TCP connections that were open when it dropped are reset.

A forward whose block is closed, for example with `rex forward close`, stops. `rex forward new` exits once it has no forwards left.

| Flag | Description |
| --- | --- |
| `-L, --local stringArray` | Listen here on port and connect to host:hostport from the server. |
| `-R, --remote stringArray` | Listen on the server on port and connect to host:hostport from here. |
| `-s, --session string` | Keep the forwards' blocks in session SESSION. |

### `rex forward open [flags] (URL | [host:]port)`

Forward a local port to a server-side target and open it.

Forward a port on this machine to a target that the server can reach, and open it. This is `rex forward new` `-L` for the common case of one forward.

The target is a URL, such as http://localhost:8080/app or postgres://db/app, or host:port, or a port, which means that port on the server's localhost. A target without a scheme is taken to be http. A URL without a port uses the usual port for its scheme, if known.

The forward listens on `--bind` and `--port`, by default 127.0.0.1 and a free port. Once it listens, `rex forward open` prints the target's URL with its host and port changed to where the forward listens, and opens that URL. An http or https URL goes to $BROWSER if it is set: a list of commands separated by colons, in which %s stands for the URL. Any other URL, or one $BROWSER cannot open, goes to the system's handler for its scheme. `--no-open` only prints it.

A browser addresses what it sends to localhost and the forward's port rather than to the target's own name, so a site that checks the Host header, or that serves https with a certificate for its own name, may not work through the forward.

`rex forward open` runs in the foreground like `rex forward new`, until it is interrupted or the forward is closed with `rex forward close`.

| Flag | Description |
| --- | --- |
| `-b, --bind ADDRESS` | Listen on ADDRESS; * means every interface. Defaults to `127.0.0.1`. |
| `-p, --port PORT` | Listen on PORT; 0 picks a free port. |
| `-s, --session string` | Keep the forward's block in session SESSION. |
| `--no-open` | Print the URL without opening it. |

### `rex forward share [flags] (URL | [host:]port)`

Forward a server port to a target on this machine's side.

Forward a port on the server to a target that this machine can reach. This is `rex forward new` `-R` for the common case of one forward.

The target is a URL, such as http://localhost:3000, or host:port, or a port, which means that port on this machine's localhost. URLs without a port are read as for `rex forward open`.

The forward listens on the server on `--bind` and `--port`, by default 127.0.0.1 and a free port. Use `--bind` '*' to let other machines connect to it. Once it listens, `rex forward share` prints where: the target's URL with its host and port changed, if the target is a URL, or else host:port. With `--bind` '*' the host is the server's, as rex reached it.

Clients address what they send to the server rather than to the target's own name, so a site that checks the Host header, or that serves https with a certificate for its own name, may not work through the forward.

`rex forward share` runs in the foreground like `rex forward new`, until it is interrupted or the forward is closed with `rex forward close`.

| Flag | Description |
| --- | --- |
| `-b, --bind ADDRESS` | Listen on ADDRESS; * means every interface. Defaults to `127.0.0.1`. |
| `-p, --port PORT` | Listen on PORT; 0 picks a free port. |
| `-s, --session string` | Keep the forward's block in session SESSION. |

### `rex forward ls`

List forwards.

List the forwards in a session: the tunnel blocks that `rex forward new` and other clients create.

STATE is active while the client carrying the forward is connected, away while it is disconnected but may come back, and orphaned once nothing carries it, for example after its `rex forward new` was killed. An orphaned forward that listens on the server still accepts connections, which then time out, so `rex forward ls` warns about orphans; close them with `rex forward close`.

DIRECTION shows where the forward listens and where it connects from: client -&gt; server is a `-L` forward, and server -&gt; client is a `-R` one. A field the client did not report shows -.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |
| `-A, --all` | List forwards in every session. |
| `--json` | Output forwards as JSON. |
| `-q, --quiet` | Print IDs only, one per line. |
| `--short` | Show short IDs (`--short=false` shows full IDs). Defaults to `true`. |

### `rex forward close [FORWARD...]`

Close forwards by closing their tunnel blocks. Give the forwards' IDs, as rex forward ls shows them, or close every orphaned forward in the session with `--orphaned`, or every forward in it with `--all`.

A `rex forward new` that is carrying a closed forward stops it, and exits once it has no forwards left.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |
| `--orphaned` | Close every orphaned forward in the session. |
| `--all` | Close every forward in the session. |

## Layout

### `rex focus TARGET|DIRECTION`

Focus a block or window.

The directions left, right, up, and down move between blocks by position. Next and prev move through the blocks in layout order. Focus stops at the edge of the layout or the end of the order. Use `--wrap` to continue from the other side.

A target can be a block or window ID, label, or ID suffix. A positive integer selects a window by its one-based position. If a target matches both a block and a window, use a full ID to select the intended resource.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |
| `--wrap` | Continue from the other side at the edge. |

```sh
rex focus left
rex focus left --wrap
rex focus next --wrap
rex focus 2
rex focus logs
rex focus -s build editor
```

### `rex zoom [BLOCK]`

Zoom a block to fill its window.

Zoom changes only how the block is displayed. It does not change the underlying window layout. Without `--on` or `--off`, the command toggles the current zoom state. Use `--on` or `--off` to set the state explicitly.

BLOCK may be passed as an argument or with `--block`. If BLOCK is omitted, Rex uses REX_BLOCK when it identifies a live block. Otherwise, Rex uses the focused block in the default window.

| Flag | Description |
| --- | --- |
| `-b, --block BLOCK` | Select block BLOCK. |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |
| `--on` | Enable zoom. |
| `--off` | Disable zoom. |

```sh
rex zoom
rex zoom --off
rex zoom -b logs --on
```

### `rex move [BLOCK] DIRECTION ANCHOR`

Move a block next to another.

Move a block next to an anchor block.

DIRECTION selects BLOCK's new position relative to ANCHOR. It must be right, left, above, or below. The two blocks may be in different windows, but both must be visible and belong to the same session.

If BLOCK is omitted, Rex uses REX_BLOCK when it identifies a visible block. Otherwise, Rex uses the focused block in the default window. The `--ratio` flag sets the percentage of the resulting split assigned to BLOCK. PCT must be between 1 and 99.

If a block label is also a direction word, identify the block by its full block ID or a hexadecimal ID suffix.

| Flag | Description |
| --- | --- |
| `--ratio PCT` | Give the moved block PCT percent of the split. Defaults to `50`. |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |

```sh
rex move below editor
rex move logs right editor --ratio 30
rex move c2a9f3 above 1bc2a9 -s work
```

### `rex swap BLOCK OTHER`

Swap two blocks.

Swap the layout positions of two visible blocks.

The blocks may be in different windows, but they must belong to the same session. Rex resolves both block arguments before changing the layout. The surrounding layout is unchanged.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |

```sh
rex swap editor logs
rex swap c2a9f3 1bc2a9
```

### `rex resize [BLOCK]`

Resize a block.

## Resources

### `rex session`

Manage sessions.

### `rex session new [LABEL] [-- COMMAND...]`

Create a session.

| Flag | Description |
| --- | --- |
| `-c, --cwd string` | Working directory for the terminal. |
| `--keep-open` | Keep the terminal open after its command exits. |
| `--shell SHELL` | Run COMMAND through SHELL's login environment (none starts it directly). |
| `--interactive` | Also read the shell's interactive startup files, such as ~/.zshrc. |
| `--window string` | Label for the first window. |
| `--json` | Output the created session as JSON. |

### `rex session ls`

List sessions.

| Flag | Description |
| --- | --- |
| `--json` | Output sessions as JSON. |
| `-q, --quiet` | Print IDs only, one per line. |
| `--short` | Show short IDs (`--short=false` shows full IDs). Defaults to `true`. |

### `rex session inspect [SESSION]`

Inspect a session.

| Flag | Description |
| --- | --- |
| `--json` | Output session information as JSON. |

### `rex session rename [SESSION] LABEL`

Rename a session.

### `rex session move [SESSION] (--before S | --after S | --index N)`

Move a session to a new place in the session order.

The session order is the order that session ls prints. It also decides which session a number names, so after a move, "rex session inspect 1" shows whichever session is now first.

Exactly one of `--before`, `--after`, or `--index` is required. Positions count from one. An index past the end moves the session last. Moving a session to where it already is does nothing and succeeds.

If SESSION is omitted, Rex uses REX_SESSION when it is set. Otherwise, Rex uses the default session.

| Flag | Description |
| --- | --- |
| `--before S` | Move immediately before session S. |
| `--after S` | Move immediately after session S. |
| `--index N` | Move to one-based position N. |

```sh
rex session move logs --before work
rex session move 3 --after 1
rex session move --index 1
rex session move build --index 99
```

### `rex session kill SESSION...`

Kill sessions.

### `rex window`

Manage windows.

### `rex window new [-s SESSION] [LABEL] [-- COMMAND...]`

Create a window in the selected session. The new window becomes active by default. Use `--focus=false` to keep the active window and focused block unchanged. If the session has no active window, the new window becomes active even with `--focus=false.`

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |
| `-c, --cwd string` | Working directory for the terminal. |
| `--keep-open` | Keep the terminal open after its command exits. |
| `--shell SHELL` | Run COMMAND through SHELL's login environment (none starts it directly). |
| `--interactive` | Also read the shell's interactive startup files, such as ~/.zshrc. |
| `--focus` | Make the new window active (`--focus=false` leaves the current window active). Defaults to `true`. |
| `--json` | Output the created window as JSON. |

```sh
rex window new
rex window new logs -- tail -f app.log
rex window new logs --focus=false -- tail -f app.log
rex window new -s build tests
```

### `rex window ls`

List windows.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |
| `-A, --all` | List windows in every session. |
| `--json` | Output windows as JSON. |
| `-q, --quiet` | Print IDs only, one per line. |
| `--short` | Show short IDs (`--short=false` shows full IDs). Defaults to `true`. |

### `rex window inspect [WINDOW]`

Inspect a window.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |
| `--json` | Output window information as JSON. |

### `rex window focus [WINDOW|next|prev]`

Focus a window by ID, label, position, or ID suffix.

If WINDOW is omitted, focus the window containing REX_BLOCK when available. Otherwise, focus the active window in the default session. Use next or prev to move through window order.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |
| `--wrap` | Wrap next and prev at the ends. |

### `rex window move [WINDOW] (--before W | --after W | --index N) [-s SESSION]`

Move a window within its session's tab order.

Exactly one of `--before`, `--after`, or `--index` is required. Window positions are numbered from one in the same order shown by window ls. An index past the end moves the window last.

If WINDOW is omitted, Rex uses the window containing REX_BLOCK when available. Otherwise, Rex uses the active window in the default session.

| Flag | Description |
| --- | --- |
| `--before W` | Move immediately before window W. |
| `--after W` | Move immediately after window W. |
| `--index N` | Move to one-based position N. |
| `-s, --session string` | Scope to session SESSION. |

```sh
rex window move logs --before editor
rex window move 3 --after 1
rex window move --index 1
rex window move -s work build --index 99
```

### `rex window rename [WINDOW] LABEL`

Rename a window.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |

### `rex window close WINDOW...`

Close windows.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |

### `rex block`

Manage blocks.

### `rex block new FLAVOR`

Create a block.

### `rex block ls`

List blocks.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |
| `-A, --all` | List blocks in every session. |
| `--json` | Output blocks as JSON. |
| `-q, --quiet` | Print IDs only, one per line. |
| `--short` | Show short IDs (`--short=false` shows full IDs). Defaults to `true`. |

### `rex block inspect [BLOCK]`

Inspect a block.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |
| `--json` | Output block information as JSON. |

### `rex block focus [BLOCK]`

Focus a block by ID, label, or ID suffix.

If BLOCK is omitted, focus the block named by REX_BLOCK when available. Otherwise, focus the current block in the default window.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |

### `rex block types`

List block types.

| Flag | Description |
| --- | --- |
| `--json` | Output block types as JSON. |

### `rex block send [TEXT]`

Send input to a terminal

| Flag | Description |
| --- | --- |
| `-b, --block string` | Send to block BLOCK. |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |

```sh
rex send 'make test'
rex send -b logs $'\003'
printf 'y\n' | rex send
rex send -s build -w 2 'ls'
```

### `rex block send-key KEY...`

Send key presses to a terminal

Each KEY uses Rex's key binding syntax. Join modifiers with +, as in ctrl+c or shift+left. A bracketed name such as [Slash] selects a physical key. By default, Rex sends a press followed by a release for each key. Use `--action` to send one action instead.

Rex interprets printable keys using a US keyboard layout because send-key does not receive physical keyboard input. The generated text and physical key may differ on other layouts. Use `rex send` when exact text matters.

| Flag | Description |
| --- | --- |
| `-b, --block string` | Send to block BLOCK. |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |
| `--action ACTION` | Send one ACTION for each key: press, release, or repeat. |

```sh
rex send-key enter
rex send-key ctrl+c
rex send 'make test' && rex send-key enter
rex send-key -b logs ctrl+c
rex send-key shift+left shift+left
rex send-key --action release ctrl+[KeyQ]
```

### `rex block capture`

Print a terminal's screen and scrollback.

Print the screen and scrollback of a terminal block.

When `--block` is omitted, Rex uses REX_BLOCK if it names a live block. Otherwise, it uses the focused block of the default window. Text is the default format. HTML preserves rendered styling. VT preserves terminal control sequences. Output always ends with a newline.

A terminal keeps about 64 MiB of scrollback. That is roughly 100,000 lines of plain text at 80 columns, and fewer in a wider terminal or with styled output. Once the limit is reached, the oldest lines are dropped.

| Flag | Description |
| --- | --- |
| `-b, --block BLOCK` | Select block BLOCK. |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |
| `--format FORMAT` | Output FORMAT: text, html, or vt. Defaults to `text`. |
| `--trim` | Remove trailing blank lines. |
| `--unwrap` | Join soft-wrapped lines. |

```sh
rex block capture
rex block capture -b logs --trim > screen.txt
rex block capture --format=html > screen.html
rex block capture -s build -w 2
```

### `rex block zoom [BLOCK]`

Zoom a block to fill its window.

Zoom changes only how the block is displayed. It does not change the underlying window layout. Without `--on` or `--off`, the command toggles the current zoom state. Use `--on` or `--off` to set the state explicitly.

BLOCK may be passed as an argument or with `--block`. If BLOCK is omitted, Rex uses REX_BLOCK when it identifies a live block. Otherwise, Rex uses the focused block in the default window.

| Flag | Description |
| --- | --- |
| `-b, --block BLOCK` | Select block BLOCK. |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |
| `--on` | Enable zoom. |
| `--off` | Disable zoom. |

```sh
rex block zoom
rex block zoom --off
rex block zoom -b logs --on
```

### `rex block move [BLOCK] DIRECTION ANCHOR`

Move a block next to another.

Move a block next to an anchor block.

DIRECTION selects BLOCK's new position relative to ANCHOR. It must be right, left, above, or below. The two blocks may be in different windows, but both must be visible and belong to the same session.

If BLOCK is omitted, Rex uses REX_BLOCK when it identifies a visible block. Otherwise, Rex uses the focused block in the default window. The `--ratio` flag sets the percentage of the resulting split assigned to BLOCK. PCT must be between 1 and 99.

If a block label is also a direction word, identify the block by its full block ID or a hexadecimal ID suffix.

| Flag | Description |
| --- | --- |
| `--ratio PCT` | Give the moved block PCT percent of the split. Defaults to `50`. |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |

```sh
rex block move below editor
rex block move logs right editor --ratio 30
rex block move c2a9f3 above 1bc2a9 -s work
```

### `rex block swap BLOCK OTHER`

Swap two blocks.

Swap the layout positions of two visible blocks.

The blocks may be in different windows, but they must belong to the same session. Rex resolves both block arguments before changing the layout. The surrounding layout is unchanged.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |

```sh
rex block swap editor logs
rex block swap c2a9f3 1bc2a9
```

### `rex block stream BLOCK [DATA_REF]`

Stream block data.

### `rex block call METHOD [ARGS]`

Call a block method.

Call one method exposed by a block.

METHOD is the block method name. ARGS must be a JSON object. Pass the object directly, use @path to read it from a file, or use - to read it from stdin. When ARGS is omitted, the request sends no arguments.

When `--block` is omitted, Rex uses REX_BLOCK if it names a live block. Otherwise, it uses the focused block of the default window.

Method results are JSON. Output written to a terminal uses two-space indentation. Redirected output stays on one line for use in scripts. Use `rex block inspect` to list methods and `rex block inspect` `--json` to view their schemas.

| Flag | Description |
| --- | --- |
| `-b, --block BLOCK` | Select block BLOCK. |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |

```sh
rex block call process
rex block call -b c2a9f3 write '{"data":"ls\n"}'
rex block call -b logs format @request.json
echo '{"format":"text","trim":true}' | rex block call format -
```

### `rex block rename [BLOCK] LABEL`

Rename a block.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |

### `rex block close BLOCK...`

Close blocks.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |

### `rex client`

Manage clients.

Inspect the clients connected to the server.

A client is one program connected to the server, such as Rex.app or this command. Each client says what it is when it connects, and a person may give it a label.

### `rex client ls`

List clients.

List the clients connected to the server.

Each client says what it is when it connects. The label is the name a person gave it, and two clients may share one. Command line clients such as this one are hidden unless `--all` is given. With `--json`, the output is the server's full reply, including command line clients. Use `-s` to list only the clients attached to one session.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |
| `--all` | Include command line clients. |
| `--json` | Output clients as JSON. |
| `-q, --quiet` | Print IDs only, one per line. |
| `--short` | Show short IDs (`--short=false` shows full IDs). Defaults to `true`. |

```sh
rex client ls
rex client ls -s work
rex client ls --all
rex client ls -q
```

### `rex client inspect [CLIENT]`

Inspect a client.

Show one client and the actions it offers.

CLIENT is a client ID, label, or ID suffix. With no CLIENT, `-C` or REX_CLIENT names the client, and otherwise the app client is used when exactly one is connected.

| Flag | Description |
| --- | --- |
| `--json` | Output the client and its actions as JSON. |

```sh
rex client inspect studio
rex client inspect 3f9a
rex client inspect --json
```

## Automation

### `rex events`

Stream server events as they occur.

Without `--session`, events follows session creation, removal, state changes, and server connectivity for the whole server. Use `--session` to follow one session, including custom events emitted by its blocks. Use `--all` to follow every server event, including clients joining, leaving, and changing.

The default format writes one human-readable line per event. With `--json`, each line contains the same event JSON object sent by the server, compacted onto one line. Output is flushed after every event so pipelines can process events as they arrive.

The command runs until interrupted or the server disconnects. `--timeout` limits connecting and starting event delivery. It does not limit how long events are streamed.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |
| `--all` | Follow every server event, including clients joining, leaving, and changing. |
| `--json` | Output events as newline-delimited JSON. |

```sh
rex events
rex events -s build
rex events --all
rex events --json | jq -c 'select(.type == "session_created")'
```

### `rex wait TARGET`

Wait for a block, window, or session to end.

For a terminal block, wait returns the child process's exit status. It does not wait for the block itself to close, so it also works with terminals created with `--keep-open`. Waiting for any other block, a window, or a session succeeds when that resource closes or is destroyed. If a terminal closes before reporting a child exit, wait returns 1. A block that closes before wait starts watching it also returns 1, because Rex can no longer tell what kind of block it was.

By default, wait runs until the target ends or the server disconnects. Use `--for` to limit the wait. The global `--timeout` still limits connections and individual API requests, but it does not limit event delivery. With `--json`, the command writes the payload that ended the wait as one line of JSON.

If Rex cannot determine a terminal child's exit status, wait returns 1. Statuses above 255 are reduced modulo 256 to match shell behavior.

Targets may be IDs, labels, ID suffixes, or a window's one-based position. If a target matches more than one resource kind, use a full ID to select the intended resource.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |
| `-w, --window string` | Scope to window WINDOW. |
| `--for DURATION` | Give up after DURATION. |
| `--json` | Output the terminating payload as JSON. |

```sh
rex wait c2a9f3
rex wait build
rex wait 2 -s work
rex wait --for 10m $B || echo timed out
```

### `rex api`

Inspect and call the server API.

Inspect and call methods exposed by the server's control API.

Use call to run a method by name. Use list to discover the methods exposed by the connected server, including methods provided by block creators. Use describe to read one method's schemas, examples, and validation rules.

```sh
rex api call server.status
rex api call -s work session.view
rex api list
rex api describe session.new_split
```

### `rex api call METHOD [ARGS]`

Call a server API method.

Call one method exposed by the server's control API.

METHOD is the API method name, such as server.status. ARGS must be a JSON object. Pass the object directly, use @path to read it from a file, or use - to read it from stdin. When ARGS is omitted, the request sends no arguments.

Methods that take a session_id require the connection to attach to that session. Use `--session` to resolve a session by ID or label and attach before the request. The command adds session_id to ARGS when it is missing. If ARGS already contains session_id, the value is preserved and must match the selected session. Without `--session`, a top-level session_id in ARGS is used for the attachment.

Method results are JSON. Output written to a terminal uses two-space indentation. Redirected output stays on one line for use in scripts.

| Flag | Description |
| --- | --- |
| `-s, --session string` | Scope to session SESSION. |

```sh
rex api call server.status
rex api call -s work session.view | jq '.windows | length'
rex api call session.create @layout.json
echo '{"label":"x"}' | rex api call session.create -
```

### `rex api list`

List server API methods.

List methods exposed by the connected server.

The default output has separate tables for regular API methods and methods provided by block creators. Use `--quiet` to print only method names, one per line. Use `--json` to print the complete help document returned by the server.

| Flag | Description |
| --- | --- |
| `-q, --quiet` | Print method names only. |
| `--json` | Output the API help document as JSON. |

### `rex api describe METHOD`

Describe a server API method.

Describe one method exposed by the connected server.

The default output includes the method's description and any available schemas, examples, and validation rules. Use `--json` to print the method's entry from the server's help document.

| Flag | Description |
| --- | --- |
| `--json` | Output the method description as JSON. |

### `rex actions`

List the actions a client or the server offers.

List the actions that a client or the server offers.

With `-C` or REX_CLIENT, this lists the actions that client offers, as it most recently reported them to the server. Otherwise it lists the actions the server offers: those that init.lua defines with rex.action, followed by those of every block type. A key binding can name any action in either list.

The ARGS column lists each action's arguments. An argument followed by ? is optional.

| Flag | Description |
| --- | --- |
| `--json` | Output actions as JSON. |

```sh
rex actions
rex -C studio actions
rex actions --json
```

## Server

### `rex server`

Manage the Rex server.

### `rex server run`

Run a foreground server.

Run a Rex server in the foreground until it is stopped.

Ctrl+C, SIGTERM, and `rex server stop` end the server after it saves its sessions.

On Unix, what SIGHUP does depends on how the server runs. A server in a terminal saves and stops, because closing a terminal sends SIGHUP. A server with no terminal, such as one that systemd, launchd, or a container runtime runs, logs the signal and keeps running. A server started under nohup keeps ignoring the signal. A server that `rex server start` launched reopens its log file instead. See `rex server logs` `--help`.

If nothing reads the server's output any more, for example because tee exited when its terminal closed or an ssh connection dropped, the server keeps running and its log lines are lost. Stop it with `rex server stop`.

| Flag | Description |
| --- | --- |
| `--listen ENDPOINT` | Listen on ENDPOINT (default: the local Rex Unix socket, or the Rex named pipe on Windows). |
| `--extra-listen ENDPOINT` | Also listen on unix, http, tailnet, tailnet-serve, or login-server ENDPOINT (repeatable, but not tailnet-serve or login-server on Windows). |
| `--socket-mode MODE` | Set Unix socket permissions to MODE, owner and group bits only (default 0600, not available on Windows). |
| `--allow-anonymous` | Admit anonymous callers on plain TCP listeners (every connection gets full access). |
| `--tailnet-allow-identified` | Admit every identified caller through the host's tailscaled (default: node owner). |
| `--tailscaled-socket PATH` | Use the tailscaled LocalAPI socket at PATH (default: platform socket). |
| `--tailscale-allow-tailnet-members` | Allow any tailnet member permitted by ACLs to reach Rex. |
| `--exit-empty` | Stop when no sessions or control connections remain. |
| `--websocket-origin-pattern PATTERN` | Allow browser WebSockets from cross-origin PATTERN (repeatable). |
| `--block-dir DIR` | Discover executable block plugins in DIR (repeatable). |
| `--block-plugin-reload` | Reload block plugins after source changes. |
| `--state-dir DIR` | Store persistent server state below DIR. |
| `--restore` | Restore the saved sessions (false starts without them and saves nothing). Defaults to `true`. |
| `--config PATH` | Read the config file at PATH (default: REX_CONFIG, else rex/init.lua in the config directory). |
| `--max-sessions N` | Refuse new sessions once the server holds N (0 for no limit). Defaults to `64`. |
| `--max-blocks N` | Refuse new blocks once the server holds N across all sessions (0 for no limit). Defaults to `1024`. |
| `--max-windows N` | Refuse new windows once a session holds N (0 for no limit). Defaults to `64`. |
| `--max-panes N` | Refuse new panes once a window holds N (0 for no limit). Defaults to `64`. |
| `--tailscale` | Also serve the API over the tailnet as an embedded tsnet node. |
| `--tailscale-hostname NAME` | Tailnet device NAME for `--tailscale`. Defaults to `rex`. |
| `--tailscale-control-url URL` | Register the tailnet node with the control server at URL, such as Headscale. |
| `--tailscale-auth-key-file FILE` | Enroll `--tailscale` with the one-time auth key read from FILE ('-' for stdin). |
| `--tailscale-advertise-tag TAG` | Advertise ACL TAG (tag:name) when `--tailscale` enrolls the node (repeatable). |
| `--log-format FORMAT` | Write server logs as FORMAT: text or json. Defaults to `text`. |

### `rex server start`

Start a persistent server.

Start is idempotent. Start options apply only when a new server is launched. An existing server retains its active configuration until explicitly restarted.

The log size limit applies only to the file created for a detached server. Rex clears that file before a write would exceed `--log-max-size`.

| Flag | Description |
| --- | --- |
| `--extra-listen ENDPOINT` | Also listen on unix, http, tailnet, tailnet-serve, or login-server ENDPOINT (repeatable, but not tailnet-serve or login-server on Windows). |
| `--socket-mode MODE` | Set Unix socket permissions to MODE, owner and group bits only (default 0600, not available on Windows). |
| `--allow-anonymous` | Admit anonymous callers on plain TCP listeners (every connection gets full access). |
| `--tailnet-allow-identified` | Admit every identified caller through the host's tailscaled (default: node owner). |
| `--tailscaled-socket PATH` | Use the tailscaled LocalAPI socket at PATH (default: platform socket). |
| `--websocket-origin-pattern PATTERN` | Allow browser WebSockets from cross-origin PATTERN (repeatable). |
| `--block-dir DIR` | Discover executable block plugins in DIR (repeatable). |
| `--block-plugin-reload` | Reload block plugins after source changes. |
| `--state-dir DIR` | Store persistent server state below DIR. |
| `--restore` | Restore the saved sessions (false starts without them and saves nothing). Defaults to `true`. |
| `--config PATH` | Read the config file at PATH (default: REX_CONFIG, else rex/init.lua in the config directory). |
| `--max-sessions N` | Refuse new sessions once the server holds N (0 for no limit). Defaults to `64`. |
| `--max-blocks N` | Refuse new blocks once the server holds N across all sessions (0 for no limit). Defaults to `1024`. |
| `--max-windows N` | Refuse new windows once a session holds N (0 for no limit). Defaults to `64`. |
| `--max-panes N` | Refuse new panes once a window holds N (0 for no limit). Defaults to `64`. |
| `--tailscale-allow-tailnet-members` | Allow any tailnet member permitted by ACLs to reach Rex. |
| `--tailscale` | Also serve the API over the tailnet as an embedded tsnet node. |
| `--tailscale-hostname NAME` | Tailnet device NAME for `--tailscale`. Defaults to `rex`. |
| `--tailscale-control-url URL` | Register the tailnet node with the control server at URL, such as Headscale. |
| `--tailscale-auth-key-file FILE` | Enroll `--tailscale` with the one-time auth key read from FILE. |
| `--tailscale-advertise-tag TAG` | Advertise ACL TAG (tag:name) when `--tailscale` enrolls the node (repeatable). |
| `--log-format FORMAT` | Write server logs as FORMAT: text or json. Defaults to `text`. |
| `--log-max-size SIZE` | Limit the active detached log to SIZE by clearing old contents (0 disables). Defaults to `50MiB`. |
| `--env NAME` | Pass environment variable NAME or NAME=VALUE to a new server (repeatable). |
| `--json` | Output server information as JSON. |

### `rex server status`

Show server status.

| Flag | Description |
| --- | --- |
| `--json` | Output server information as JSON. |

### `rex server logs`

Show detached server logs.

Show logs captured for a server launched by `rex server start`.

On Windows, the server that `rex server install` runs as a Windows Service writes to this log too, so this command shows its output as well. As with `rex server start`, Rex clears that log before it grows past 50MiB.

Servers started by other service managers, such as systemd or launchd, log through that manager instead. For a personal server run by the systemd user unit that the Linux packages and the home-manager module install, run journalctl `--user` `-u` rex. For the shared server that the Linux packages and the NixOS module install, run journalctl `-u` rex.service.

On Unix, rename/create rotation must send SIGHUP to the server after creating the replacement file. Only a server that `rex server start` launched reopens its log on SIGHUP. See `rex server run` `--help` for what the signal does to other servers. Windows uses copy-truncate or `--log-max-size`. `--follow` continues across log rotation.

| Flag | Description |
| --- | --- |
| `--follow` | Follow appended log output until the server stops. |
| `--state-dir DIR` | Read server logs below DIR. |
| `-n, --lines N` | Show the newest N log lines. Defaults to `200`. |
| `--all` | Show the entire available detached log. |

### `rex server tailscale`

Manage the server's Tailscale node.

### `rex server tailscale on`

Serve the API over the tailnet.

On is idempotent and runtime-only: a restarted server returns to its start flags. To keep Tailscale on, start the server with `--tailscale`. On Windows, "rex server install -- --tailscale" does that each time the server starts. A node enabled for the first time begins Tailscale's normal authentication flow; the login URL appears in the server log and in `rex server status`.

A one-time auth key enrolls a new node without that browser flow: pass `--auth-key-file` with a path, or '-' to read the key from stdin. There is deliberately no way to pass the key itself on the command line, where it would be visible in the process list. The key is needed only for a node's first enrollment — the enrolled identity is kept on the server — and is ignored while the node is already enabled. ACL tags given with `--advertise-tag` are applied the same way: only when this call enrolls the node for the first time.

`--control-url` registers a node this call starts with another control server, such as Headscale. Each control server keeps its own saved login, so a node moved to a new one signs in again. To move a node that is already on, turn it off first. The server's `--tailscale-control-url` is used when this flag is left out.

| Flag | Description |
| --- | --- |
| `--hostname NAME` | Tailnet device NAME when this call starts the node (defaults to the server's configured name). |
| `--allow-tailnet-members` | Allow any caller admitted by the tailnet instead of only this node's owner. |
| `--auth-key-file FILE` | Enroll a new node with the one-time tailscale auth key read from FILE ('-' for stdin). |
| `--advertise-tag TAG` | Advertise ACL TAG (tag:name) when this call enrolls a new node (repeatable). |
| `--control-url URL` | Register a node this call starts with the control server at URL, such as Headscale. |
| `--json` | Output the resulting state as JSON. |

### `rex server tailscale off`

Stop serving the API over the tailnet.

Running 'off' more than once succeeds. Tailnet connections close while sessions keep running. Keeps you signed in. The next 'on' reconnects without signing in. Use 'logout' to forget this device.

| Flag | Description |
| --- | --- |
| `--json` | Output the resulting state as JSON. |

### `rex server tailscale logout`

Sign out of Tailscale and stop tailnet serving.

Sign out of the enabled Tailscale node and stop tailnet serving. The next 'on' must sign in again. If already off, this leaves the saved login unchanged.

| Flag | Description |
| --- | --- |
| `--json` | Output the resulting state as JSON. |

### `rex server tailscale login`

Request a Tailscale sign-in link.

Request a sign-in link for the enabled Tailscale node. Tailscale can take a few seconds to produce the link. If none is shown, run the command again or check `rex server status`.

| Flag | Description |
| --- | --- |
| `--json` | Output the resulting state as JSON. |

### `rex server tailscale rename NAME`

Rename the enabled Tailscale node without restarting or signing in again. NAME must be one DNS label of 1 to 63 ASCII letters, digits, or hyphens, with no leading or trailing hyphen. The new DNS name may appear after this command returns.

| Flag | Description |
| --- | --- |
| `--json` | Output the resulting state as JSON. |

### `rex server tailscale pair`

Show a QR code to pair another device.

Show the server's tailnet address as a QR code to scan with Rex on another device.

The code holds only the address, not a credential. The other device must be on the same tailnet. Tailscale must be on and its HTTPS address ready.

| Flag | Description |
| --- | --- |
| `--json` | Output the address as JSON. |

### `rex server stop`

Stop the server.

Stopping the server ends every program running in its sessions.

With `--if-idle`, the server stops only if it has no sessions and no other client is connected. Otherwise it keeps running, and the command says what is using it and exits with status 1. A client that connects at the same moment is refused, so it never starts work that the stop would end.

With `--discard`, the command also deletes the server's saved sessions once it has stopped, so the next start begins with none. If no server is running, it only deletes them. If the stop is refused, nothing is deleted. Pass `--state-dir` if the server was started with one.

`--if-idle` needs a server recent enough to support it. With an older server, the command leaves the server running and exits with status 4. Without `--if-idle`, `rex server stop` works with a server of any version.

| Flag | Description |
| --- | --- |
| `--discard` | Delete the saved sessions once the server has stopped. |
| `--if-idle` | Stop only if the server has no sessions and no other connected clients. |
| `--state-dir DIR` | Find the saved sessions for `--discard` below DIR. |

### `rex server install [flags] [ssh://[USER@]HOST[:PORT]] [-- SERVER-FLAG...]`

Start your server at boot and keep it running.

Set up your personal Rex server to start on its own. By default, it starts when the machine boots and keeps running while you are logged out.

On Linux, this enables rex.socket, a systemd user unit that starts the server the first time a rex command connects. It also turns on lingering for your account with "loginctl enable-linger". Lingering keeps your systemd user manager, and the server with it, running while you are logged out. No sudo is needed. If you run the rex that a Rex package installed, the package's units are enabled as they are. For any other rex, copies of the units that run that binary are written to ~/.config/systemd/user. Each command run and each file written is printed first.

With `--user`, the server starts when you log in and stops when you log out, and lingering is left as it is.

If a rex command already started a server, that server keeps its sessions. An idle server is stopped so that the unit or service takes over right away. A server with sessions keeps running, and the unit or service starts at the next boot or login instead.

If the system does not let you turn on lingering, the server still starts when you log in, and the command says how an administrator can turn lingering on. If ~/.config/systemd/user already has a rex.service or rex.socket that rex did not write, such as one from the home-manager module, the command leaves it alone and fails.

On Windows, this creates a Windows Service that runs "rex server run" as your account. It starts at boot, shortly after the other automatic services, and keeps running while nobody is signed in and after you sign out. Creating it needs administrator rights, so run the command from a terminal started with "Run as administrator". The command also grants your account the right to log on as a service, which Windows needs to start the service as you, and registers "Rex Server" as a source of the Event Log. It deletes the sign-in entry that `--user` adds, because only one of the two can run your server. Each change is printed first. The server and every program it starts run without administrator rights, as they would after a normal sign-in, even if your account is an administrator.

The command asks for your Windows password, because Windows needs it to start the service as your account while you are not signed in. Windows keeps it with the service. Rex does not keep it. Use the password of your Windows account, not a Windows Hello PIN. If you sign in with a Microsoft account, use the password of that Microsoft account. Without a terminal, the command reads the password from its standard input. If your password changes, run the command again, or the service cannot start at the next boot.

With `--user` on Windows, no administrator rights are needed. The command adds a value named "Rex Server" to HKCU\Software\Microsoft\Windows\CurrentVersion\Run, which runs "rex server start" each time you sign in. It also starts a server now if none is running, and leaves a running server alone. The server stops when you sign out of Windows, and starts again the next time you sign in. If the value is turned off in the Startup tab of Task Manager, the command says so. While the service exists, the command fails. Run "rex server uninstall" first.

On Windows, you can give the server flags after --. The server gets them each time it starts, so use them for settings that must last when the server restarts. For example, "rex server tailscale on" turns Tailscale on only until the server stops. This turns it on at every start:

    `rex server install` -- `--tailscale`

The service runs "rex server run" with the flags, and the "Rex Server" value that `--user` adds runs "rex server start" with them. The command refuses flags that the server would refuse, and then changes nothing. It also refuses `--listen`, because the server must stay on the named pipe where rex commands look for it. Use `--extra-listen` to add another endpoint.

Running the command again without -- keeps the server flags that are already set. Give new flags after -- to replace them, or -- on its own to remove them. A server that is already running keeps its old flags until it restarts.

On Linux, systemd starts the server, so the command takes no server flags there. Set them in a drop-in file instead, with "systemctl `--user` edit rex.service".

Running the command again is safe. Run it again after moving rex.

Linux with systemd and Windows are supported so far. Elsewhere, "rex server start" starts a server that runs until the machine restarts.

With an ssh:// endpoint, the command sets up your server on another machine that you can log in to with ssh, and installs Rex there first. It runs the Rex installer there with `--user`, which puts rex in ~/.local/bin and needs no sudo. As root, or with `--package`, it installs the Rex package for the whole machine instead, through sudo. Then it runs "rex server install" there, connects to the new server through ssh, and adds the machine to your hosts, so that "rex `-S` LABEL" reaches it. Only Linux machines are supported so far.

Each command that runs on the other machine is printed first, and the command asks before each change. `--yes` answers yes to every question. Without a terminal to ask on, `--yes` is required.

| Flag | Description |
| --- | --- |
| `--user` | Start the server when you sign in and stop it when you sign out, instead of at boot. |
| `--json` | Output what the command did as JSON. |
| `--package` | Over ssh, install the Rex package for the whole machine through sudo. |
| `--label LABEL` | Over ssh, add the machine to your hosts as LABEL (default its host name). |
| `--channel NAME` | Over ssh, install from the release channel NAME: stable, preview, or snapshot. |
| `--yes` | Over ssh, answer yes to every question. |

### `rex server uninstall [ssh://[USER@]HOST[:PORT]]`

Stop starting your server on its own.

Undo "rex server install".

On Linux, this disables and stops rex.socket and rex.service, and removes the copies of them that `rex server install` wrote to ~/.config/systemd/user. Stopping the server ends every program running in its sessions. Units that a package installed stay in place, disabled. If ~/.config/systemd/user has a rex.service or rex.socket that rex did not write, such as one from the home-manager module, nothing is disabled or removed.

Lingering stays on, because other services of your account may need it. Turn it off with "loginctl disable-linger".

On Windows, this stops and deletes the Windows Service that `rex server install` created, which ends every program running in its sessions. It also unregisters the "Rex Server" source of the Event Log, unless the service of another account still uses it. Removing the service needs a terminal started with "Run as administrator". Your account keeps the right to log on as a service, because other services may need it.

It also deletes the value named "Rex Server" that `rex server install` `--user` added to HKCU\Software\Microsoft\Windows\CurrentVersion\Run, which needs no administrator rights. A server that the value started keeps running, along with its sessions. Stop it with "rex server stop".

`--user` is accepted so that both commands take the same flags. Uninstall does the same with or without it.

With an ssh:// endpoint, the command runs "rex server uninstall" on that machine instead, after asking. Rex stays installed there, and the machine stays in your hosts. The command says how to remove each.

| Flag | Description |
| --- | --- |
| `--user` | Undo a `rex server install` `--user`. |
| `--json` | Output what the command did as JSON. |
| `--yes` | Over ssh, answer yes to every question. |

### `rex config`

Work with Rex's config file.

Rex reads one configuration file per account, init.lua. It binds keys with rex.bind and takes them away with rex.unbind.

The file is REX_CONFIG when that variable is set. Otherwise it is rex/init.lua in the account's configuration directory, which is $XDG_CONFIG_HOME/rex or ~/.config/rex on macOS and Linux, and %AppData%\rex on Windows.

### `rex config check [PATH]`

Load the config file and report what it found.

Load the config file and report where it is, whether it is there, how many keys it binds and unbinds, how many modes it uses, how many actions it defines, and everything loading it had to complain about. Without PATH the account's own init.lua is used.

Only loading happens here. Some mistakes take more than the file to see, such as binding an action that neither this file nor the client has, or unbinding a key that nothing was bound to. Those depend on which client is asking, and "rex keymap" reports them.

The file is loaded by this rex binary, so it is checked against the functions and fields this version of Rex knows. A server running a different version may read it differently. "rex keymap" asks the running server instead.

The exit status is 1 when any complaint is an error.

| Flag | Description |
| --- | --- |
| `--json` | Output the result as JSON. |

### `rex config reload`

Make the server read the config file again and tell every client connected to it that their keys may have changed.

The generation counts the times this server has read the file. A client compares it against its own to notice that its keymap is out of date. It advances on every read, including one that failed.

A read that fails keeps the keys of the last one that worked. Only the file's own complaints are reported here. For the ones that take a client's own bindings to find, use "rex keymap".

This runs on the server's own machine, over its local socket. Whoever can edit the file is already there, and a remote caller is refused.

The exit status is 1 when any complaint is an error.

| Flag | Description |
| --- | --- |
| `--json` | Output the result as JSON. |

### `rex keymap`

Show what each key does, as the server resolves it. The answer is the config file's bindings stacked over the bindings the asking client ships with, and under the shortcuts recorded in that client's own settings.

Without `--request` this command sends no layers and declares no actions of its own. The output is then the config file's bindings alone, and a binding that names an action is not checked against any client's list of actions. Pass a client's own request to see the keymap that client receives. If that request sets server_actions, the output lists the same actions the client receives.

Bindings are listed with the top of each key's stack first, which is the order a client tries them in. A binding in a mode is listed as the config file writes it, such as resize/left, and applies only while a client has that mode active. When there are modes, a table of them follows the bindings. The exit status is 1 when any complaint is an error.

| Flag | Description |
| --- | --- |
| `--request FILE` | Send the client.keymap request in FILE ("-" reads stdin). |
| `--json` | Output the response as JSON. |

```sh
rex keymap
rex keymap --request client.json
rex keymap --json | jq '.bindings[] | select(.keys[0] == "cmd+d")'
```

### `rex login-server`

Run the multi-account login server.

The login server runs as root. It identifies each caller, selects an operating system account through the accounts file, and starts or reuses a Rex server for that account. It proxies each accepted connection to the selected server. Clients select an account with https://ACCOUNT@host:port. Tailnet listeners admit every identified caller because the accounts file controls authorization. Local users use sudo to request another account. Send SIGHUP to reload the accounts file. Reloading closes revoked connections but leaves account servers and sessions running.

| Flag | Description |
| --- | --- |
| `--listen ENDPOINT` | Listen on the root-only Unix ENDPOINT (default: unix://RUNTIME-ROOT/login-server.sock). |
| `--extra-listen ENDPOINT` | Also listen on unix, http, tailnet, or tailnet-serve ENDPOINT (repeatable). |
| `--allow-anonymous` | Admit anonymous callers on plain TCP listeners (every connection gets full access). |
| `--tailscaled-socket PATH` | Use the tailscaled LocalAPI socket at PATH (default: platform socket). |
| `--accounts FILE` | Map callers to accounts with the HuJSON FILE (default /etc/rex/accounts.jsonc, or /usr/local/etc/rex/accounts.jsonc on FreeBSD). |
| `--runtime-root DIR` | Keep the socket and accounts/ below DIR (default: $REX_RUNTIME_DIR, else /run/rex, or /var/run/rex on FreeBSD). |
| `--pam` | Open PAM sessions and run PAM account checks (default true; `--pam=false` uses shadow-based checks and no login session). |
| `--pam-service NAME` | Use the PAM policy for service NAME (default rex-login-server). |
| `--log-format FORMAT` | Write server logs as FORMAT: text or json. Defaults to `text`. |

### `rex home`

Show or choose your home server.

Show or choose your home, the server this device follows for hosts and shortcuts.

Your home is this machine unless you choose another. Any Rex server can be a home. A server is not told that it is someone's home, and admits you the same way either way.

Without a command, connect to your home and show what it reports about you. Your choice is kept in home.json in Rex's state directory. Setting REX_HOME chooses a home for one environment and takes precedence over the file. The `--server` option and REX_SERVER choose the server other commands talk to. They do not change your home.

| Flag | Description |
| --- | --- |
| `--json` | Output the home and your identity as JSON. |

### `rex home set ENDPOINT`

Follow ENDPOINT for hosts and shortcuts.

Make the server at ENDPOINT your home. ENDPOINT takes the same forms as `--server`.

The server is asked for its status first. If it cannot be reached, your home is not changed, so a mistyped address or a server that is down never becomes your home. The home is named by `--label`, or by the endpoint's host or socket name if no label is given.

Your choice is written to home.json in Rex's state directory, along with the host ID the server reports. `rex home` uses the host ID to warn you if the address later leads to a different machine. This command fails while REX_HOME is set, because that variable takes precedence over the file.

| Flag | Description |
| --- | --- |
| `--label NAME` | Call the home NAME in output. |

### `rex home unset`

Follow this machine again.

Forget the home you chose, so this machine's own server is your home again. This removes home.json. It succeeds if no home was chosen.

This command fails while REX_HOME is set, because that variable takes precedence over the file.

### `rex hosts`

List and change the hosts you connect to.

List and change your hosts, the other Rex servers you connect to by label.

Your home keeps your hosts, so every device that follows the same home sees the same list. The hosts of this machine's own server always count too, whatever your home is. If a host of this machine and a host of your home have the same label, this machine's host wins.

A new host goes to your home. A change to a host goes to the server the host came from. If your home is away, the change is held on this machine and sent the next time `rex hosts` reaches your home. Until then, the list shows the hosts your home had when it was last reached.

To use a host, give its label to `--server`, as in "rex `-S` studio session ls".

Without a command, list your hosts. The `--server` option and REX_SERVER do not change which servers this command talks to.

| Flag | Description |
| --- | --- |
| `--json` | Output the hosts as JSON. |

### `rex hosts list`

List your hosts.

List the hosts your home keeps, including changes held for it, and the hosts of this machine's own server.

FROM says which server a host came from, and is left out when your home is this machine. SOURCE is the init.lua line that declares a host, "stored" for a host added with `rex hosts`, or "held" for a host not yet sent to your home.

If your home is away, the list shows the hosts it had when it was last reached, and says how long ago that was. Warnings go to standard error.

| Flag | Description |
| --- | --- |
| `--json` | Output the hosts as JSON. |

### `rex hosts add LABEL ENDPOINT...`

Add a host to your home.

Add a host called LABEL, reached at each ENDPOINT in turn. An endpoint takes the same forms as `--server`.

A socket path, such as unix:///tmp/rex.sock, works only on a device where the path exists, such as the local end of an ssh `-L` forward. Other devices skip it and try the next endpoint.

The host goes to your home, so every device that follows your home sees it. No other host of yours may have the same label, ignoring case.

If your home is away, the host is held on this machine and listed at once. It is sent the next time `rex hosts` reaches your home.

| Flag | Description |
| --- | --- |
| `--icon NAME` | Show the icon NAME next to the host. |

### `rex hosts edit LABEL`

Change a host.

Change the host called LABEL, ignoring case. Give at least one option. `--endpoint` replaces all of the host's endpoints, and may be given more than once.

The change goes to the server the host came from: your home, or this machine. Editing a host declared in init.lua stores a host that takes the declaration's place. The line stays in the file, but no longer has an effect.

If the host is your home's and your home is away, the change is held on this machine and sent the next time `rex hosts` reaches your home.

| Flag | Description |
| --- | --- |
| `--label NEW` | Rename the host to NEW. |
| `--endpoint ENDPOINT` | Reach the host at ENDPOINT (repeatable, replaces all endpoints). |
| `--icon NAME` | Show the icon NAME next to the host (empty removes it). |

### `rex hosts remove LABEL`

Remove a host.

Remove the host called LABEL, ignoring case, from the server it came from: your home, or this machine.

Removing a host declared in init.lua hides it. The line stays in the file, but no longer has an effect.

If the host is your home's and your home is away, the removal is held on this machine and sent the next time `rex hosts` reaches your home.

### `rex hosts check [LABEL...]`

Check which hosts can be reached from here.

Try to reach each of your hosts, or only the hosts called LABEL, ignoring case. All hosts are tried at once, and each one has up to `--timeout` to answer.

STATUS is one of:

  reachable                a Rex server answered at ENDPOINT   not reachable from here  nothing answered, but the host may work from another network or device   down                     a machine answered, but not a Rex server

DETAIL says why a host could not be reached. For a reachable host, it says when the address now leads to a different machine than the one you added.

The check succeeds whatever it finds. Commands that connect to a host try the address that answered first.

| Flag | Description |
| --- | --- |
| `--json` | Output the results as JSON. |

### `rex whoami`

Show your identity, server, and Rex location.

| Flag | Description |
| --- | --- |
| `--json` | Output identity, server, and location as JSON. |

## Other

### `rex completion`

Generate shell completion scripts for Rex.

### `rex completion bash`

Generate Bash completion.

Load it with source &lt;(`rex completion bash`), or install it in a bash-completion directory.

### `rex completion zsh`

Generate Zsh completion.

Install it with `rex completion zsh` &gt; `"${fpath[1]}/_rex".`

### `rex completion fish`

Generate Fish completion.

Install it with `rex completion fish` &gt; ~/.config/fish/completions/rex.fish.

### `rex completion powershell`

Generate PowerShell completion.

Add the output of `rex completion powershell` to your PowerShell profile.

### `rex completion nu`

Generate Nushell completion.

Add the output of `rex completion nu` to your config.nu.

### `rex terminfo`

Set up and report Rex's terminal type.

Rex carries its own terminfo entry, xterm-rex, describing what its terminals support.

A terminal claims it wherever this account is set up for it, unless REX_TERM names another entry. Run "rex terminfo setup" once per account, and "rex terminfo status" to see what this machine would decide.

### `rex terminfo status`

Report the TERM a new terminal would be given, and why.

Report the TERM a new terminal would be given, what was examined to decide it, and, when this account is not set up, which command fixes it.

A server claims xterm-rex wherever that is safe and keeps xterm-256color everywhere else. Naming an entry in REX_TERM overrides that and gives every terminal that name, which makes REX_TERM=xterm-256color the off switch. The value "auto" asks for the rule the server already follows.

The report covers the terminfo entry, the ssh client, both guard files, the hosts that have been recorded, and which configuration is responsible for what ssh would actually send.

Nothing of yours is changed and no host is contacted. The one file written is a throwaway copy of Rex's guard, used to ask ssh what that guard would do on its own.

| Flag | Description |
| --- | --- |
| `--json` | Output the report as JSON. |

### `rex terminfo setup`

Install Rex's terminfo entry and ssh guard.

Install xterm-rex where programs find it, and configure ssh so that the name never reaches a host that has never heard of it.

Three things happen. The entry is installed in ~/.terminfo. A guard is written to Rex's configuration directory. One Include line is added at the top of ~/.ssh/config. ssh is then asked what it would send to a host without the entry, and the answer is reported.

If your ssh configuration already includes a guard, as it does when home-manager manages it with Rex's module, the guard and the Include line are skipped, because they would add nothing.

The guard needs OpenSSH 7.8 or later, because an older ssh refuses any configuration that contains it. With an older ssh, only the entry is installed, and the command fails with a message that says why.

With `--system` the entry goes into /usr/share/terminfo and the guard into /etc/ssh/ssh_config.d/50-rex.conf, so that every account on this machine has both, root included. That needs root, and it is what makes programs started through sudo and su find the entry. On FreeBSD, where /etc/ssh/ssh_config reads no other files, it also adds an Include line for the guard's directory to the top of that file.

Run this once per account. Repeating it is harmless.

Two flags are meant for a program that sets an account up on someone's behalf. `--entry-only` installs the entry and leaves ssh configuration alone, which is the half that needs no permission. `--json` reports what the run did as one object instead of the lines a person reads.

Installing the entry is not the whole story. A terminal claims the name once the guard is in place as well, and REX_TERM can name another entry instead. "rex terminfo status" reports what this machine would do.

| Flag | Description |
| --- | --- |
| `--entry-only` | Install the entry alone, for a program that adds the guard later (the name is claimed only once the guard is in place too). |
| `--dry-run` | Report what would change without writing anything. |
| `--system` | Install the entry and the guard for every account on this machine. |
| `--json` | Output what the run did as JSON. |

### `rex terminfo remove`

Remove the ssh guard, leaving the terminfo entry installed.

Remove Rex's ssh guard and the Include line that puts it into effect, so that ssh goes back to forwarding whatever TERM it is given.

With `--system` the machine-wide guard /etc/ssh/ssh_config.d/50-rex.conf goes instead. On FreeBSD, so does the Include line the setup added to /etc/ssh/ssh_config. That needs root.

The terminfo entry stays where it was installed, in both cases. Shells that are already running with TERM=xterm-rex stop working the moment the entry disappears, so there is no command that deletes it. Remove the files by hand if you really want them gone.

`--json` reports what the run did as one object instead of the lines a person reads, in the shape the setup uses.

| Flag | Description |
| --- | --- |
| `--dry-run` | Report what would change without writing anything. |
| `--system` | Remove the guard that every account on this machine shares. |
| `--json` | Output what the run did as JSON. |

### `rex terminfo show`

Print the terminfo source.

Compile it on another machine with: `rex terminfo show` &#124; tic `-x` -

### `rex terminfo host`

Install Rex's terminfo entry on other machines.

Install xterm-rex on a remote host and record that it is there, so that ssh stops announcing xterm-256color to it.

A record is compared, exactly, against the destination as ssh resolves it, so one machine can be recorded under more than one spelling.

### `rex terminfo host install DESTINATION [-- SSH_OPTIONS...]`

Install Rex's terminfo entry on a remote host.

Install xterm-rex on DESTINATION over ssh, and record the host so that ssh stops announcing xterm-256color to it.

The entry's source is piped to the remote tic, which compiles it into ~/.terminfo there. Nothing else on the remote host is touched, and no interactive session is started. Options after -- are passed to ssh, so "-- `-p` 2222" and "-- `-J` jump.example.com" work as usual.

The entry lands in the home directory of the account you connect as, while the record is kept per host. Another account on that host will not have the entry. Use "rex terminfo host forget" to undo a record.

What gets recorded is the spelling you typed, because that is what the guard compares against. When your ssh config rewrites it with HostName, that name is recorded as well, because a machine-wide guard sees the rewritten one. Either way, other spellings of the same machine keep the old terminal type until you install to them too.

### `rex terminfo host forget HOST`

Forget that a host has Rex's terminfo entry.

Remove HOST from the hosts file, so that ssh goes back to announcing xterm-256color to it.

Use this when the entry is gone from that host, or when you connect to it as an account that does not have it.

HOST is the spelling that was recorded. Run "rex terminfo host ls" to see them.

### `rex terminfo host ls`

List the hosts recorded as having Rex's terminfo entry, one per line, in the order they were added.

Each line is compared, exactly, against the destination as ssh resolves it. One machine can appear under more than one spelling, and each spelling is its own record.

| Flag | Description |
| --- | --- |
| `--json` | Output the list as JSON. |

### `rex skill`

Set up agents to use Rex.

Set up agents such as Claude Code and Codex to use Rex.

Rex installs two things into each agent. A skill teaches the agent how to use the rex command. A hook, in agents that support hooks, tells the agent at the start of each session which Rex session and terminal it is running in. Both get their text from the rex command each time the agent uses them, so upgrading Rex also updates what agents are told.

Run "rex skill setup" once per account, and "rex skill status" to see what is installed.

### `rex skill status`

Report whether Rex's skill and hooks are installed.

Report, for each agent Rex knows, whether the agent was found on this machine and whether Rex's skill and hooks are installed in it. Nothing is changed.

Each item is reported as one of:

  current      installed and up to date   outdated     installed by an older Rex, or edited since   missing      not installed   foreign      a file of the same name that Rex did not install   unreadable   the file could not be read   unsupported  the agent cannot hold the item here

| Flag | Description |
| --- | --- |
| `--project` | Report on the current repository instead of your account. |
| `--json` | Print the report as JSON. |

### `rex skill setup`

Install Rex's skill and hooks into agents.

Install Rex's skill and hooks into the agents found on this machine, or into the agents named with `--agent`. If no agent is found, the skill alone goes into the shared ~/.agents/skills directory that most agents read.

The skill goes into each agent's skill directory. Some agents share a directory, and it is written once. Hooks are added to each agent's settings file, and the rest of the file is left as it was. Some agents ask you to approve a new hook before they run it, and the output says how.

Run this once per account. Running it again is safe, and updates anything an older Rex installed.

Pass `--session-naming` to have agents name the session after their task. A new setup leaves that off, and running setup again keeps the choice already installed unless you pass the flag.

If a skill named rex is already installed and Rex did not install it, it is left alone and reported as a conflict. Use `--force` to replace it.

| Flag | Description |
| --- | --- |
| `--agent AGENT` | Use only AGENT, which is claude, codex, opencode or pi (repeatable). |
| `--project` | Set up the current repository instead of your account. |
| `--dry-run` | Report what would change without changing anything. |
| `--force` | Replace a skill named rex that Rex did not install. |
| `--session-naming` | Tell agents to name the session after their task (omit to keep the installed choice). |
| `--json` | Print the result as JSON. |

### `rex skill remove`

Remove Rex's skill and hooks from agents.

Remove Rex's skill and hooks from every agent Rex knows, or from the agents named with `--agent`. Agents that are no longer installed are included, so nothing Rex installed is left behind.

Only files and hooks that Rex installed are removed, including older copies of the skill in other directories the agent reads. Other skills, hooks and settings are left as they were.

| Flag | Description |
| --- | --- |
| `--agent AGENT` | Use only AGENT, which is claude, codex, opencode or pi (repeatable). |
| `--project` | Remove from the current repository instead of your account. |
| `--dry-run` | Report what would change without changing anything. |
| `--json` | Print the result as JSON. |

### `rex skill show`

Print the guide that Rex's skill gives agents.

An agent with the skill installed reads the guide through the skill. An agent without it can run this command to read the same guide.

### `rex version`

Print version information.

| Flag | Description |
| --- | --- |
| `--json` | Output version information as JSON. |

## Global Options

Every command accepts these.

| Flag | Description |
| --- | --- |
| `--autostart` | Start a missing local server. Defaults to `true`. |
| `-C, --client CLIENT` | Act on client CLIENT, given as a label or ID. |
| `--color WHEN` | Set color WHEN to auto, always, or never. Defaults to `auto`. |
| `-S, --server SERVER` | Connect to SERVER, a host's label or a server address (a value with a port is always an address). |
| `--timeout DURATION` | Limit each connection and nonstreaming request to DURATION. Defaults to `10s`. |
| `-v, --verbose count` | Increase diagnostic verbosity (repeatable). |

---
title: "Server API Methods"
canonical_url: https://www.superlogical.com/rex/docs/reference/api
---

# Server API Methods

This page lists every method in the control API. See [Server API](https://www.superlogical.com/rex/docs/automate/server-api.md) for how to call them. Anything tagged [Snapshot](https://www.superlogical.com/rex/docs/linux/installation.md#channels) is on the snapshot channel and not yet in preview.

<div class="reference-page methods" hidden></div>

## block

### `block.close`

Close a block and end the program running in it.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session that holds the block. |
| `block_id` |  | The block to close. It may be a detached block. Send this or window_id, not both. |
| `window_id` |  | The window whose focused block to close. It doesn't have to be the active window. Send this or block_id, not both. |
| `keep_last` |  | If true, fail rather than close the only block left in a window, counting floating layers. Ignored for detached blocks. |

Send exactly one of `block_id` and `window_id`. If you send both, or neither, the call fails with `bad_request`.

The server sends `block_closed` when the block closes. If it was the last block in its window, the window closes too and the server sends `window_closed`.

If `keep_last` is true and the block is the only one left in its window, counting blocks in floating layers, the call fails with `bad_request` and closes nothing.

If the block's creator is still placing it, the call returns at once and the block closes when placement finishes.

On one connection, `block.close` waits for earlier requests that can change focus, such as `session.new_split` and `session.focus_block`, to finish. A close by `window_id` therefore closes the block those requests left focused.

If the session doesn't exist, the call fails with `session_not_found`. If `block_id` or `window_id` names something that isn't in the session, or the window has no focused block, it fails with `not_found`.

### `block.event`

Send an input event, such as a key press or a paste, to a block.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session that holds the block. |
| `block_id` | Yes | The block to send the event to. |
| `event` | Yes | The event to send: its type, such as paste, and its payload. |

A client may send only `key`, `paste`, `mouse`, `wheel`, and `focus` events. Every other event type is sent only by the server. A `paste` event's `payload` is `{"text": "..."}`, and a `focus` event's `payload` is `{"action": "in"}` or `{"action": "out"}`.

The event goes straight to the block, so Lua key bindings never see it. To give key bindings the first chance at a key, use `client.key` instead.

On one connection, `block.event` calls run one at a time, in order with `client.key`, so the block receives events in the order you send them.

The call returns once the block has received the event, which may be before the block has finished acting on it.

The call fails with `bad_request` if `event` is missing or its `payload` has the wrong shape for its type. It also fails with `bad_request` if `event.type` is empty, names an event only the server sends, or names an event this server doesn't know.

If the session doesn't exist, the call fails with `session_not_found`, and if `block_id` names a block that isn't in the session, it fails with `not_found`.

### `block.method`

Call a method that a block provides and return its result.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session that holds the block. |
| `block_id` | Yes | The block to call the method on. |
| `method` | Yes | The block method's short name, such as write or format. |
| `args` |  | The block method's own arguments. Leaving it out is the same as an empty object. |

Send the block method's short name, such as `write`, in `method`. `help.all` lists the methods each creator's blocks provide under `block_types`, with each method's `args_schema` and `output_schema`.

Each block method can also be called by its full name, such as `com.superlogical.terminal.write`, with the same `session_id`, `block_id`, and `args`. The full name names the block's creator, so the call works only on a block that creator made. On any other block, it fails with `conflict`.

The result is the block method's own result, as its `output_schema` describes it. `block.method` has no result schema of its own.

`block.method` doesn't wait for earlier requests on the same connection, and it can run at the same time as `client.key`. If a call depends on an earlier request, wait for that request's reply first. To call a block method in order with `client.key`, such as from a key binding, use `client.action`.

If `block_id` is missing, the call fails with `bad_request`. If the session doesn't exist, it fails with `session_not_found`. If `block_id` names a block that isn't in the session, it fails with `not_found`, and if the block has no method named `method`, it fails with `method_not_found`. If `args` is missing a required field or has a value of the wrong type, it fails with `bad_request`.

## block_creator

### `block_creator.list`

List the creators the server can make blocks with, and their flavors.

Creators are sorted by `name`.

To create a block, pass a flavor's `flavor` to `session.new_block` or in the `block` of a layout, with `options` that match its `options_schema`.

`api.methods` lists the methods that every block from the creator provides. Call one as `<creator>.<method>`, such as `com.superlogical.terminal.title`, with `session_id`, `block_id`, and its arguments under `args`.

A method's `actions` are things a person can do with a block, such as choosing an item in Rex.app's command palette or pressing a key. Performing one calls the method with the action's `args`, which are fixed values, not a schema like the `args` that `client.keymap` lists for an action.

## client

### `client.action`

Perform an action from the configuration file or a block, as this client.

| Field | Required | Description |
| --- | --- | --- |
| `action` | Yes | The full name of the action, such as dev_layout or com.superlogical.terminal.reset. client.keymap lists them. |
| `args` |  | The action's arguments, as a JSON object. Leave it out if there are none, and always for an action a creator declares. |
| `origin` |  | What caused the request: key, palette, or api (the default). The action reads it as ctx.origin. Other values count as api, but script makes the call fail with bad_request. |
| `session_id` |  | The session the action applies to. Required for a block action. A configuration file action may leave it out, and then has no default session. |
| `block_id` |  | The block the action applies to. Defaults to the session's focused block, unless server is set. Requires session_id. |
| `servers` |  | Other servers this client is connected to, which a configuration file action can call with rex.server(name). Don't list the server you're sending this to. |
| `server` |  | The name, from servers, of the server that holds session_id and block_id. Leave it out when they're on the server you're sending this to. |

`action` decides what runs. A name without a dot, such as `dev_layout`, is an action that the configuration file defines with `rex.action`. A name with a dot, such as `com.superlogical.terminal.reset`, is an action or method of a block, named after its creator. `client.keymap` lists every name in `actions`.

A block action, such as `com.superlogical.terminal.reset`, has fixed arguments, so a request for one with `args` fails with `bad_request`. A block method, such as `com.superlogical.terminal.write`, takes its arguments from `args`.

If `args` isn't an object, the call fails with `bad_request`. `null` counts as leaving it out.

The client's own actions, such as `pane.split`, run in the client and can't be sent here. A request for one, or for any name the server doesn't know, fails with `not_found`.

On each connection, `client.key`, `client.action`, `block.event`, and the methods that change focus or the layout run one at a time, in the order they arrive. For an action bound to a key, use this method rather than calling the block method by name, such as `com.superlogical.terminal.write`, so the action takes effect after the keys typed before it.

When `session_id` is set, the connection must be attached to that session, with `session.attach`, `session.bootstrap`, or `session.attach_all`, or the call fails with `conflict`. If `session_id` names no session, the call fails with `session_not_found`. Neither check applies when `server` is set.

Send `session_id` with any block action or method, and whenever you send `block_id`, or the call fails with `bad_request`. A block action or method runs on `block_id`, or on the session's focused block if you leave that out. If the session has no focused block, the call fails with `conflict`.

If the block isn't the kind the action or method belongs to, the call fails with `conflict`. If `block_id` names a block that isn't in the session, it fails with `not_found`.

An action from the configuration file may leave out `session_id`. It then has no `ctx.session_id`, and its `rex.session` calls have no default session. With `session_id`, it sees `block_id`, or the focused block, as `ctx.block_id`.

If `origin` is `script`, the call fails with `bad_request`, because only `rex.invoke` may report that origin.

An action from the configuration file may run for 30 seconds, including time spent waiting on other servers. If it runs longer, the call fails with `timeout`. If the file is reloaded while it runs, the call fails with `conflict`.

If an action from the configuration file raises an error, the call fails with a message that starts with where it happened, such as `init.lua:4: attempt to index a nil value`. However the action fails, part of it may already have run.

Each entry in `servers` needs a unique, non-empty `name` and a non-empty `id`, and `server` must name one of them. Otherwise the call fails with `bad_request`. The server doesn't interpret either value.

Set `server` when the session belongs to one of `servers`. The IDs then go to the action unchecked, and the focused block isn't filled in, so send `block_id` yourself. The action's `rex.session`, `rex.block.call`, and `rex.call` calls go to that server.

When `server` is set, any name with a dot fails with `bad_request`, because a block action must run on the server that holds the block. Send it to that server instead.

When the action calls another server, this connection receives a `client_call_requested` event. Make the call there and report the outcome with `client.reply`. If no reply comes within 10 seconds, or the reply is an error, the action's call fails and the action keeps running.

### `client.inspect`

Get one connected client's details, including the actions it offers.

| Field | Required | Description |
| --- | --- | --- |
| `client_id` | Yes | The client to describe. It can be any client, not only the one calling. |

If `client_id` is missing or isn't a client ID, the call fails with `bad_request`. If it names no connected client, including one that has left, the call fails with `not_found`.

To ask the client to perform one of the actions in `actions`, call `client.perform`.

### `client.key`

Send a key to a block, giving Lua key bindings the first chance at it.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session the key is typed in. |
| `key` | Yes | The key event, as the keyboard reported it. |
| `block_id` |  | The block to send the key to. Defaults to the session's focused block. |
| `prefix` |  | The keys pressed before key when key ends a key sequence, in order. For ctrl+b then x, send x as key and ctrl+b here. |

The connection must be attached to `session_id`, with `session.attach`, `session.bootstrap`, or `session.attach_all`, or the call fails with `conflict`. If `session_id` names no session, the call fails with `session_not_found`.

Without `block_id`, the key goes to the session's focused block. The server reads it once, before any Lua function runs, so a function that moves focus doesn't change where the key goes. Either way, Lua functions see the block the key goes to as `ctx.block_id`.

On each connection, `client.key`, `client.action`, `block.event`, and the methods that change focus or the layout run one at a time, in the order they arrive. That keeps keys in the order you send them.

The key goes first to the Lua functions bound to it in this client's scripts, then to those in the configuration file. If none of them takes the key, it goes to the block.

Only a key bound to a Lua function reaches Lua, and only when it's pressed or repeating. Every other key goes straight to the block. `client.keymap` reports the bound keys as bindings for `client.key.forward`.

If no Lua function takes the key and the session has no focused block, the key goes nowhere. The call still succeeds, with `handled` set to false.

To send a key sequence, wait until it's complete. Then send its last key as `key` and the keys before it in `prefix`, such as `["ctrl+b"]`. The server doesn't remember keys between requests.

A key with a `prefix` reaches only the functions bound to that exact sequence. A key without one reaches only the functions bound to that key alone.

If no function takes a key sequence, only `key` goes on to the block. The keys in `prefix` never do.

A Lua function bound to a key may run for 5 seconds. If it raises an error or runs longer, the call fails and the key doesn't reach the block.

If an element of `prefix` isn't a key the server can read, the call fails with `bad_request`, and the message names the element.

If no Lua function takes the key and `block_id` names a block that isn't in the session, the call fails with `not_found`.

### `client.keymap`

Get what each key does for this client. The result combines the client's own bindings with the configuration file and the client's Lua scripts.

| Field | Required | Description |
| --- | --- | --- |
| `actions` |  | The actions the client performs itself. Leave it out to skip the checks for unknown actions and wrong arguments. The server keeps the list for client.inspect and client.perform. |
| `server_actions` |  | Which of the server's actions the reply lists: all (the default), configured (only the configuration file's), or none. An unknown value means all. |
| `defaults` |  | The client's built-in layers, lowest first. They sit beneath the configuration file. Problems found only in them aren't reported, because the user can't edit them. |
| `overrides` |  | Layers of shortcuts a person recorded in the client, lowest first. They sit above the configuration file and this client's scripts. Problems in them come back as warnings. |

Bindings stack in layers, from bottom to top: your `defaults`, then the account's configuration file, then the scripts this client loaded with `client.set_script`, then your `overrides`.

A layer's `name` becomes the `source` of its bindings and of warnings about it. If a name is empty, contains a colon, or is used by another layer in either list, the call fails with `bad_request`.

The keymap is always the calling client's, so a request that includes `client_id` fails with `bad_request`. So does a top-level `bindings` list, which older servers accepted. Send those bindings as a layer in `defaults` instead.

A key sequence such as ctrl+b followed by c is `["ctrl+b", "c"]` in `keys`. The configuration file writes it as `ctrl+b>c`, but the result always lists the keys separately. The server doesn't split an element such as `ctrl+b>c`. It treats it as a key it can't read.

Keys the server can read come back in their standard spelling. `cmd+d` and `cmd+[KeyD]` are still different keys: the first follows the keyboard layout and the second names a physical key.

A key spelling the server can't read is kept as written, in either list, so a newer client keeps its keys. Only a removal with the same spelling can take it off. A sequence with any such key is kept the same way.

A sequence sits on the stack of its first key, in order among that key's other bindings. If the top binding a client can perform is a sequence, the client waits for the next key. The server keeps no state between keys, so waiting is the client's job.

A key bound to a Lua function, in the configuration file or this client's scripts, comes back as a binding for `client.key.forward`. To perform it, send the key to `client.key`. Treat it as always available: the bindings below it on that key never run, even when no function takes the key.

For a sequence bound to a function, send its last key to `client.key` with the earlier keys in `prefix`. The binding ends the stack only for that exact sequence, so other sequences that start with the same key still work.

A key can't both run a binding of its own and start a sequence, so the higher of the two wins. The server keeps both and adds one warning per key, with the `source`, `action`, and `key` of the binding that can't run. It warns even about a binding in `defaults`, unless the one above it is in `defaults` too.

A removal matches keys in full. Removing `["ctrl+b"]` leaves every sequence that starts with ctrl+b, and removing `["ctrl+b", "c"]` leaves ctrl+b.

A removal that removes nothing gets a warning, unless it's in `defaults`, names a key the server can't read, or blocks a key in a mode.

A binding or removal with a `mode` belongs to that mode, a set of bindings the client uses only while the mode is active. If a mode name contains anything but lowercase letters, digits, `_`, and `-`, or is `root`, the call fails with `bad_request`.

Bindings in a mode stack and are checked like any others. A key in a mode never hides the same key outside it, and a removal affects only its own mode. The configuration file writes a key in a mode as `resize/left`.

`modes` lists a mode if any layer binds a key in it, blocks a key in it, or declares it with `rex.mode`.

A removal in a mode that names no `action` always blocks its key, so the key is listed in the mode's `blocked`, whether or not anything was removed. A removal that names an action never blocks.

A removal with a `mode` and no `keys` removes the whole mode: its bindings, its blocked keys, and its declarations in the layers beneath. If it also names an `action`, the call fails with `bad_request`.

Above a removed mode, a layer that binds, blocks, or declares a key in the mode brings the mode back, with only what that layer and the ones above it say. Otherwise the mode isn't in `modes`.

The client decides which mode is active, with its own `client.mode.enter`, `client.mode.exit`, and `client.mode.exit_all` actions. The server keeps no mode state.

The server warns about a `client.mode.enter` binding whose `name` argument isn't a mode in the keymap, and about a declared mode with nothing bound in it.

An empty request returns the bindings of the configuration file and this client's scripts, without checking for unknown actions or wrong arguments.

When `actions` isn't empty, the server adds its own actions before checking: those the configuration file defines with `rex.action`, every block method, and every block action. A binding to one of these is never reported as unknown.

A binding with wrong arguments is kept, with a warning. A binding to a block method must pass arguments that match the method's input schema, and passing none counts as `{}`. A binding to a block action, or to a method with no input schema, must pass no arguments.

A binding to an action from the configuration file must match the `args` schema the action declares, if it declares one. The client's own actions aren't checked, because the client checks their arguments itself.

`actions` in the result lists the configuration file's actions in the order it defines them, then each creator's methods and block actions, sorted by creator and then by method name. It doesn't repeat the actions you sent.

A block method in `actions` has no `title`, and its `args` is its input schema. Bind it, but don't show it in a menu. A block action has a `title` and no `args`.

`server_actions` changes only what `actions` lists in the result, not which actions bindings are checked against.

To show a diagnostic next to the binding it's about, match its `action`, `key`, and `mode` to the binding's.

A client can keep using a stored result while its request is unchanged and `config.instance_id` and `config.hash` both match. Call this method again after every `client.set_script` call, and after every `keymap_changed` event whose `hash` differs from the stored one.

### `client.list`

List the clients connected to the server, in the order they connected.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` |  | List only the clients attached to this session with session.attach or session.bootstrap. |
| `kinds` |  | List only the clients of these kinds, such as app. A client that didn't report a kind matches none. |

With no arguments, the result lists every connected client, including short-lived ones such as the `rex` CLI.

`session_id` counts only clients attached to that session with `session.attach` or `session.bootstrap`. A connection that only called `session.attach_all` doesn't count. Any client may use this filter, attached or not.

In `kinds`, a kind this server doesn't know counts as `other`. A client that didn't report a kind matches no filter, not even `other`.

If a client's connection drops without closing, the client stays in the list for up to 60 seconds, with `connection_state` set to `lost`, so it can reconnect. A client that wasn't attached to anything and had no scripts leaves at once.

To keep a copy of this list up to date, call `session.attach_all` with `event_scope` set to `all`. The connection then receives `client_joined`, `client_left`, and `client_changed` events.

### `client.perform`

Ask another client to perform one of its actions and wait for its answer.

| Field | Required | Description |
| --- | --- | --- |
| `client_id` | Yes | The client to ask. To find one by its label, call target.resolve with kinds set to client. |
| `action` | Yes | The name of an action the client declared, such as pane.split. client.inspect lists them. |
| `args` |  | The action's arguments, as a JSON object. Leave it out if there are none. |
| `origin` |  | What caused the request, such as key, palette, api, or cli. The client receives it unchanged. Empty means api. |
| `session_id` |  | The session the action applies to, if any. The client receives it without the server checking it. |
| `block_id` |  | The block to act on instead of the client's focused block, if any. The client receives it without the server checking it. |
| `timeout_ms` |  | How long to wait for the answer, in milliseconds, once the client is sent the request. Defaults to 10000. Values over 30000 count as 30000. |

The server sends the named client a `client_perform_requested` event and waits for it to answer with `client.reply`.

The server tells the named client which client asked, based on the connection the request arrived on, so a client can't claim to be another.

The connection doesn't need to be attached to `session_id`, because the server only passes it on to the client.

Requests to one client wait in line, and the client answers them one at a time in the order they arrived. `timeout_ms` counts from when a request reaches the front of the line.

A request without `client_id` or `action`, with a negative `timeout_ms`, or with `args` that aren't an object or don't match the action's schema fails with `bad_request`. If `client_id` names no connected client, the call fails with `not_found`.

If the client has listed its actions and `action` isn't one of them, the call fails with `not_supported`. A client that listed none is asked anyway, and it may answer `not_supported` itself.

A client that declines answers `refused`, and the call fails with that code and a message that says why.

If the client is reconnecting, already has 16 requests waiting for it, or disconnects while the request waits, the call fails with `client_unavailable`.

If the client doesn't answer in time, the call fails with `timeout`. The client isn't told, so it may still perform the action later.

When a request ends, the server sends a `client_performed` event with the outcome to connections that called `session.attach_all` with `event_scope` set to `all`. A request that names no connected client sends no event.

### `client.reply`

Answer a client_call_requested or client_perform_requested event.

| Field | Required | Description |
| --- | --- | --- |
| `call_id` | Yes | The call_id of the event you're answering. |
| `result` |  | The call's result, or what the action returned. Use null if there is none. Set this or error, not both. |
| `error` |  | Why the call failed or the action wasn't performed. Set this or result, not both. |

A `client_call_requested` event asks this client to make one call on another server, for a `client.action` request that is still running. Reply to every one, even when the call fails. Send the event's `call_id`, on the connection that received the event.

A `client_perform_requested` event asks this client to perform one of its own actions for another client's `client.perform` request. Reply the same way. `result` is what the action returned, or `null`.

A reply that sets both `result` and `error`, or neither, fails with `bad_request`. For a call with no result, send `"result": null`.

For a call that failed, use the code and message the other server returned, or explain why you couldn't make the call. The waiting action sees only the message.

For an action you didn't perform, set `error.code` to `not_supported`, `refused`, or `internal`. The client that sent `client.perform` gets that code unchanged. An empty code is sent as `internal`.

A reply runs as soon as it arrives. It doesn't wait behind this connection's earlier `client.key` and `client.action` requests, because a `client.action` request that is waiting for the reply holds that line.

A `call_id` that nothing is waiting for fails with `not_found`. That includes a call that was already answered and one that timed out.

### `client.set_label`

Set or remove this client's label.

| Field | Required | Description |
| --- | --- | --- |
| `label` | Yes | The label to set, such as studio. An empty string removes it. At most 64 bytes, with no control characters. |

The label always belongs to the client that sends the request. A request that includes `client_id` fails with `bad_request`.

The server removes whitespace from both ends of `label` before it checks the label, so a label of only spaces removes the current one.

A label longer than 64 bytes, or one that isn't valid UTF-8 or contains a control character, fails with `bad_request`. The message says which rule it breaks.

Labels don't have to be unique. Two clients with the same label are told apart by their IDs.

Labels are for people to read. The server never uses one to decide what a client may do.

When the label changes, the server sends a `client_changed` event to connections that called `session.attach_all` with `event_scope` set to `all`.

A client can also send a label when it connects, in the `client` field of its `hello` message. The same rules apply, but the server fixes the label instead of failing: it cuts a label longer than 64 bytes down to size and drops one that is otherwise invalid.

### `client.set_script`

Load Lua scripts for this client, replacing any it loaded before.

| Field | Required | Description |
| --- | --- | --- |
| `scripts` | Yes | Every Lua source to load, run in order. They replace all the scripts loaded before. An empty list unloads them. |

The scripts belong to this client and apply in every session it's attached to. A request that includes `session_id` or `client_id` fails with `bad_request`.

If a script has a syntax error, raises an error, or runs longer than 5 seconds while it loads, the call fails with `bad_request`. The scripts loaded before keep running.

To see every key and key sequence the scripts bound to a function, call `client.keymap`. It reports each one as a binding for `client.key.forward`.

The keys that reach the scripts are fixed when they load. A function that a script binds later, while it runs, receives a key only if a function was already bound to that key or sequence when the scripts loaded.

A successful call can still return `diagnostics`, even with severity `error`. An error marks a part of a script the server skipped, such as a binding to a key it can't read. The rest of the scripts still load.

Loading scripts changes this client's keymap without a `keymap_changed` event. Call `client.keymap` again after each call to this method.

## data

### `data.connect`

Get a one-time ticket for opening a data connection to a block. A data connection streams a block's content as it changes, such as a terminal's output.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session that holds the block. The connection must be attached to it with session.attach or session.bootstrap. |
| `block_id` | Yes | The block that provides the data. |
| `data_ref_id` | Yes | Which of the block's data sources to open, such as terminal. |
| `subprotocol` |  | The subprotocol the data connection will use, such as rex.data.terminal.v1. If set, the ticket works only on a connection that uses it. |

The connection must be attached to `session_id` with `session.attach` or `session.bootstrap`. `session.attach_all` isn't enough. If the connection isn't attached, the call fails with `conflict`.

To use the ticket, open a WebSocket to `/api/data` on the server and send the ticket in the HELLO message. A ticket works once, even if the handshake fails, and expires after `expires_in_ms`.

If another user presents the ticket, the data connection gets REJECT reason 0x01, as it would for an unknown ticket. The ticket is then used up, so your own data connection can't use it either.

The server checks only that `data_ref_id` is well formed. If the block has no data by that name, the server closes the data connection with WebSocket status 1008 after the handshake.

Set `subprotocol` to the subprotocol the data connection will use, such as `rex.data.terminal.v1`. The server then refuses a data connection that uses a different one with REJECT reason 0x07, before any data is sent.

If `subprotocol` is left out, the block checks the subprotocol after the handshake and closes the data connection with status 1008 if it doesn't support it.

The call fails with `bad_request` if `data_ref_id` or `subprotocol` is malformed, and with `not_found` if the block isn't in the session or has no data to offer.

## help

### `help.all`

Describe every method the server provides, including block methods.

The result has two lists. `methods` describes each method with its arguments, result, examples, and notes. `block_types` lists each creator whose blocks provide methods, with its flavors and those methods.

The result describes the server you are connected to, so it always matches that server's version.

Over HTTP, `GET /api/help` returns the same document with an added `http` field, and `/llms.txt` shows it as Markdown.

## hosts

### `hosts.list`

List the server's hosts: the other Rex servers you connect to by label.

Every Rex server keeps a host list. Each of your devices reads its hosts from the one server it uses as its home, so they all see the same list.

The list holds the hosts declared with `rex.host` in the server's configuration file, and the hosts that clients added with `hosts.put`. A declared host has a `source`, such as `init.lua:4`. A host added with `hosts.put` has none.

Each host comes back exactly as it was stored, including fields this server doesn't know. A client that sends a host back with `hosts.put` must keep those fields.

A declared host with a mistake in it, or whose label another host already uses, is left out of `hosts`. A warning in `diagnostics` names the line that declared it.

A server started without a state directory can't store hosts, so it lists only the declared ones.

If a hand edit broke the file that stores the hosts, the call fails with `conflict` and a message that names the file.

### `hosts.put`

Add a host to the server's host list, or replace the host with the same ID.

| Field | Required | Description |
| --- | --- | --- |
| `host` | Yes | The host to add, or to store in place of the host with the same id. |

The device that adds a host chooses its `id`, so sending the same request again is safe and adds nothing. If `host` has no `id`, the server creates one.

If `host` matches the stored host in every field except `updated_at`, nothing changes and the result holds the stored host.

`label` can't be empty, can't start or end with white space, and has at most 64 characters. No other host in the list may have the same label, ignoring case. If one does, the call fails with `conflict` and a message that names that host and, for a declared host, the line that declared it.

`endpoints` needs at least one endpoint, each in a form that the `-S` option of `rex` accepts. Unix sockets and named pipes aren't allowed, because their paths work only on their own machine.

To change a declared host, put a host with its `id`. The server never writes the configuration file. It stores the host you send, which takes the place of the declared one. A put also brings back a host that `hosts.remove` removed.

The server sets `updated_at` and drops `source`. It stores every other field as sent, including fields it doesn't know.

When the put changes the list, every connected client receives a `hosts_changed` event.

If `host` fails a check, the call fails with `bad_request` and a message that names the field. A server started without a state directory can't store hosts, and fails with `not_supported`. A hand edit that broke the file that stores the hosts makes the call fail with `conflict`.

The file that stores the hosts can hold at most 1 MiB, which is room for thousands of hosts. A put that would make it larger fails with `too_large`.

### `hosts.remove`

Remove a host from the server's host list.

| Field | Required | Description |
| --- | --- | --- |
| `id` | Yes | The host to remove. |

Removing a host that isn't in the list isn't an error. `removed` is false, nothing changes, and no event is sent, so calling it again is safe. The same happens if `id` is left out.

The server never writes the configuration file, so removing a declared host hides it. Removing a stored host that took the place of a declared one removes both. Either way, only a later `hosts.put` with the same `id` brings the host back.

When a host is removed, every connected client receives a `hosts_changed` event.

If `id` isn't a valid host ID, the call fails with `bad_request`. A server started without a state directory can't store hosts, and fails with `not_supported`. A hand edit that broke the file that stores the hosts makes the call fail with `conflict`.

## server

### `server.apple.grant_file_access`

Let the server, and every program it runs, read files a Mac app was given.

| Field | Required | Description |
| --- | --- | --- |
| `files` | Yes | The files to make readable, in the order they were dropped or pasted. At most 256. |

A Mac app calls this when a file is dropped on or pasted into a terminal. macOS lets the app open the file, but not always the server, which macOS treats as a different app.

For each file, the server first tries to open `path`. It uses `token` only if it can't, so most files, such as those in a project, come back `readable` and their tokens go unused.

A file the server can't open doesn't fail the call. A relative path is always `denied`.

Access lasts until the server exits. If a file comes back `denied`, or with an `access` value the client doesn't know, copy the file somewhere the server can read instead.

A request that names more than 256 files fails with `bad_request`.

The server accepts this method only over its local Unix socket, or its named pipe on Windows. Over any other connection, such as TCP or Tailscale, the call fails with status 403.

### `server.apple.privacy_status`

Report the macOS privacy permissions the server and its programs have.

macOS decides privacy permissions for the app it holds responsible for a process. Every shell and program the server runs belongs to the server, so they all get the permissions listed here, whatever the app that shows their terminals was allowed.

Only the server can read its own permissions. A Mac app calls this to show them, and calls it again after the person changes one in System Settings.

Reading the permissions never makes macOS ask the person anything.

`responsible_path` is the executable macOS holds responsible for the server. The permissions belong to the app that contains it. When the server was started in the foreground from a terminal app, that is the terminal app.

Skip a `service` you don't know, and treat a `status` you don't know as `unknown`. A server that isn't running on macOS lists no services.

The server accepts this method only over its local Unix socket, or its named pipe on Windows. Over any other connection, such as TCP or Tailscale, the call fails with status 403.

### `server.config_reload`

Reload the configuration file and tell every client to fetch its keymap again.

The server reads the same configuration file it read when it started, usually `init.lua`, along with the modules that file loads with `require`.

The next `client.keymap` result reports the same `generation` in `config.generation`, unless the file is reloaded again first.

A mistake that stops the file from loading, such as a Lua syntax error, doesn't fail the call. The server keeps the bindings, actions, and hosts from the last load that worked, and `diagnostics` reports the mistake.

If the file no longer exists, the reload removes the bindings, actions, and hosts it used to declare. That isn't an error.

`diagnostics` lists only problems in the file itself. A problem that depends on a client, such as a binding to an action that client doesn't have, is reported by `client.keymap` instead.

After the load, every connected client receives a `keymap_changed` event with the new `generation` and `hash`. A client can keep its keymap if its last `client.keymap` result has the same `hash` and came from the same server process, that is, the same `instance_id`.

If the reload changes the host list, such as by adding or removing a `rex.host` declaration, every connected client also receives a `hosts_changed` event.

The server accepts this method only over its local Unix socket, or its named pipe on Windows. Over any other connection, such as TCP or Tailscale, the call fails with status 403.

### `server.status`

Get the server's identity, session count, configuration, and Tailscale status.

Two servers report the same `host_id` only when they see the same files, as two servers run by one user on one machine usually do. If the values differ, or either one is missing, assume the servers don't share files. The value means nothing else.

`config` describes the last load of the configuration file. Its `generation` and `hash` match the `config` of a `client.keymap` result taken at the same moment.

A client that saved a keymap can compare `config.hash` and `instance_id` with the values it saved. If either differs, call `client.keymap` again.

Later changes to `tailscale` arrive as `tailscale_status_changed` events, which only connections that called `session.attach_all` or `session.bootstrap` receive.

### `server.stop`

Shut the server down, or with if_idle, only if nothing is using it.

| Field | Required | Description |
| --- | --- | --- |
| `if_idle` |  | Stop only if the server has no sessions and no other client is connected. A server that is in use refuses with conflict and keeps running. |

Only a client on the server's own machine, connected over its Unix socket or named pipe, can call this method.

Without `if_idle`, the server stops no matter what it is doing, and every program running in its sessions ends.

With `if_idle`, the server stops only if it has no sessions, including any still being created or closed, and no client is connected except the caller. Otherwise it keeps running and the call fails with `conflict`. The error message says what is using the server.

The server checks and stops in one step. A client that connects just after the check is refused.

The server replies before it shuts down. Close the connection after reading the reply. The server waits up to a second for the connection to close before it stops.

A server running inside another program, with no process of its own, fails the call with `not_supported`.

The server accepts this method only over its local Unix socket, or its named pipe on Windows. Over any other connection, such as TCP or Tailscale, the call fails with status 403.

### `server.tailscale.disable`

Turn off built-in Tailscale, keeping its sign-in for the next enable.

Turning off built-in Tailscale closes every connection that came in over the tailnet. Sessions keep running.

The device keeps its sign-in, so the next `server.tailscale.enable` reconnects without signing in. To sign out instead, call `server.tailscale.logout`.

If built-in Tailscale is already off, the call changes nothing and returns the current status, so calling it again is safe.

If the server can't run built-in Tailscale, the call fails with `not_supported`.

The server accepts this method only over its local Unix socket, or its named pipe on Windows. Over any other connection, such as TCP or Tailscale, the call fails with status 403.

### `server.tailscale.enable`

Turn on built-in Tailscale so devices on your tailnet can reach the server.

| Field | Required | Description |
| --- | --- | --- |
| `hostname` |  | The device name to use if this call starts the device, such as studio. If left out, the server uses its configured name, or rex. |
| `allow_tailnet_members` | Yes | True lets in anyone your tailnet's access rules allow. False lets in only the Tailscale user who signed the device in. Applies on every call, even when the device is already on. |
| `auth_key` |  | A Tailscale auth key that signs the device in without a browser. Used only when this call starts the device. Never logged or saved. |
| `advertise_tags` |  | Tailscale tags, such as tag:rex, for the device to sign in with. Used only when this call starts the device. Needs allow_tailnet_members set to true. |
| `control_url` |  | The control server to sign in with if this call starts the device, such as https://headscale.example.com. If left out, the server uses its configured control server, or Tailscale's own. |

Built-in Tailscale is a copy of Tailscale inside Rex. It makes the server join your tailnet as a device of its own, so the machine doesn't need Tailscale installed.

If built-in Tailscale is already on, the call only applies `allow_tailnet_members`, without restarting the device or dropping connections. It ignores `hostname`, `auth_key`, and `advertise_tags`.

The first time the device starts, it must sign in to Tailscale. With a valid `auth_key`, it signs in without a browser. Otherwise `state` becomes `needs_login`, and the sign-in link appears in `auth_url` and in the server's log.

The device keeps its sign-in, so later calls need no `auth_key`. The server never logs, saves, or returns the key.

`advertise_tags` signs the device in as a tagged device, which has no owner, so it needs `allow_tailnet_members` set to true. The auth key or OAuth client that signs the device in must be allowed to grant the tags.

The call returns once the device has started, before it has finished connecting. The server is reachable over the tailnet only once `https.state` is `available`. Follow the progress with `tailscale_status_changed` events, on a connection that called `session.attach_all` or `session.bootstrap`.

If the HTTPS listener fails later, built-in Tailscale turns off: `state` becomes `disabled` and `error` says why.

When the call starts the device, it fails with `bad_request` for a tag that isn't a valid Tailscale tag, and for tags with `allow_tailnet_members` set to false.

While the server is shutting down, the call fails with `conflict`.

If the server can't run built-in Tailscale, the call fails with `not_supported`.

The server accepts this method only over its local Unix socket, or its named pipe on Windows. Over any other connection, such as TCP or Tailscale, the call fails with status 403.

### `server.tailscale.login`

Ask Tailscale for a sign-in link for the server's Tailscale device.

The call starts the sign-in and returns without waiting for the link. If `auth_url` is empty, the link arrives later in a `tailscale_status_changed` event, which only connections that called `session.attach_all` or `session.bootstrap` receive. `server.status` also shows it once it is ready.

Calling it again while a sign-in is already under way is safe and doesn't start another one.

If built-in Tailscale is off, the call fails with `conflict`. Turn it on with `server.tailscale.enable` first.

If the request fails, or Tailscale doesn't accept it within 10 seconds, the call fails. The server keeps trying in the background.

If the server can't run built-in Tailscale, the call fails with `not_supported`.

The server accepts this method only over its local Unix socket, or its named pipe on Windows. Over any other connection, such as TCP or Tailscale, the call fails with status 403.

### `server.tailscale.logout`

Sign the server out of Tailscale and turn off built-in Tailscale.

After signing out, built-in Tailscale turns off. The next `server.tailscale.enable` must sign in again.

If built-in Tailscale is off, the call changes nothing and the saved sign-in stays. Turn it on first to sign out.

If signing out fails, or Tailscale doesn't answer within 10 seconds, the call fails and built-in Tailscale stays on, so you can try again.

If the server can't run built-in Tailscale, the call fails with `not_supported`.

The server accepts this method only over its local Unix socket, or its named pipe on Windows. Over any other connection, such as TCP or Tailscale, the call fails with status 403.

### `server.tailscale.rename`

Change the server's Tailscale device name without signing in again.

| Field | Required | Description |
| --- | --- | --- |
| `hostname` | Yes | The new device name, such as studio: 1 to 63 ASCII letters, digits, and hyphens, not starting or ending with a hyphen. |

The device keeps its identity and its sign-in. Until the server exits, a later `server.tailscale.enable` without a `hostname` uses the new name too.

`dns_name` in the result may still show the old name. The new one arrives in a `tailscale_status_changed` event, on a connection that called `session.attach_all` or `session.bootstrap`.

If built-in Tailscale is off, the call fails with `conflict`. To choose the name then, pass `hostname` to `server.tailscale.enable` instead.

If `hostname` isn't a valid device name, the call fails with `bad_request`.

If the server can't run built-in Tailscale, the call fails with `not_supported`.

The server accepts this method only over its local Unix socket, or its named pipe on Windows. Over any other connection, such as TCP or Tailscale, the call fails with status 403.

### `server.version`

Get the version of Rex that the server is running and how it was built.

A missing `api_revision` or `api_min_revision` means 0. Servers from before these fields existed leave both out.

### `server.whoami`

Get how the server identified you, and the account the server runs as.

Which fields `principal` has depends on its `kind`. A `peer` has `uid` and `gid`, and usually `pid`. A `windows_peer` has `sid`, and usually `pid`. A `tailscale` client has `user_id`, `login_name`, and `node`, plus `tags` if its device is tagged. An `anonymous` client has none of these.

The server identifies a client when its connection opens. `principal` stays the same for as long as the connection lasts.

`principal` describes a decision the server already made when it let the client in. Use it to show who is connected, not to decide what a client may do.

## session

### `session.attach`

Attach to a session: start receiving its events and get its current view.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to attach to. |

The connection starts receiving every event from the session, including `block_event`, before the server reads `view`. Apply `view` first, then each event whose `seq` is higher than the response's `seq`, even one that arrived before the response.

Most methods that take a `session_id` work only on a connection attached to that session, with this method, `session.bootstrap`, or `session.attach_all`. On a connection that isn't attached, they fail with `conflict`.

Attaching makes the connection's client one of the session's clients. The server sends `client_connected`, and `session.list_clients` lists the client.

`data.connect`, `session.own_block`, and `session.disown_block` need this kind of attachment. Attaching with `session.attach_all` isn't enough for them.

Calling it again for the same session is safe. It returns the same `client_id` and a fresh `view`.

If `session_id` names no session, the call fails with `session_not_found`.

### `session.attach_all`

Start receiving events from the whole server and get the list of sessions.

| Field | Required | Description |
| --- | --- | --- |
| `event_scope` | Yes | Which events to receive. 'session' sends every session's events except block_event, plus session_created, session_destroyed, session_moved, and tailscale_status_changed. 'all' also sends block_event and the client events, such as client_joined. |

Use `session` unless you need `block_event` or the client events. With `all`, the client events are `client_joined`, `client_left`, `client_changed`, `client_performed`, and `client_script_events_dropped`. Any value other than `session` or `all` fails with `bad_request`.

The connection still receives every event from a session it attached to with `session.attach`, whatever the scope.

The connection doesn't join any session. It can call methods on every session, but it isn't listed in `session.list_clients` and can't call `data.connect` or own blocks. To join a session too, call `session.attach`, or use `session.bootstrap` to do both in one call.

Calling it again replaces the scope with the new `event_scope`.

The session list is a snapshot taken after events start. Apply each event whose `seq` is higher than the response message's `seq` on top of it.

Events arrive in order, and their `seq` has no gaps. If the connection falls more than 8 MiB of events behind, the server closes it rather than skip events. Open a new connection to get a fresh snapshot.

### `session.bootstrap`

Set up a new connection in one call: receive events, list sessions, and attach.

| Field | Required | Description |
| --- | --- | --- |
| `event_scope` | Yes | Which events to receive. 'session' sends every session's events except block_event, plus session_created, session_destroyed, session_moved, and tailscale_status_changed. 'all' also sends block_event and the client events, such as client_joined. |
| `preferred_session_id` |  | The session to attach to, if it exists. Otherwise the server attaches the first session. Ignored when attach_session_ids is set. |
| `attach_session_ids` |  | The sessions to attach to, in order. Unknown and repeated IDs are skipped, and an empty list attaches none. Leave it out to let the server pick one. |
| `data_tickets` |  | The kinds of block to issue data tickets for. The response has a ticket for each visible block in an attached session that matches one. At most 8 entries. |

Send it as the first request on a new connection. Apply its result first, then each event whose `seq` is higher than the response's `seq`, even one that arrived before the response.

`event_scope` works as it does for `session.attach_all`, and the connection can then call methods on any session. A value other than `session` or `all` fails with `bad_request`.

If `attach_session_ids` is left out, the server attaches one session and returns it in `selected_session`, which is null when the server has no sessions.

If `attach_session_ids` is set, even to an empty array, the server returns the sessions it attached in `attached_sessions` and leaves `selected_session` null.

A session in `attach_session_ids` that is destroyed during the call is left out of `attached_sessions`. If the session chosen for `selected_session` is destroyed during the call, the call fails with `session_not_found`. Any other failure to attach also fails the whole call.

If the connection falls more than 8 MiB of events behind, the server closes it rather than skip events. Open a new connection and call `session.bootstrap` again.

If the call fails, close the connection and open a new one before you try again. Part of the setup may already have taken effect.

### `session.close_layer`

Close a floating layer and every block in it.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `layer_id` | Yes | The floating layer to close. Every block in it closes too. |

`layer_id` must name a floating layer. Naming the window's layout, the layer of kind `tiled` in `session.view`, fails the call with `bad_request`. Close the window with `session.close_window` instead.

The server closes every block in the layer, which ends the programs running in them. The call returns once the blocks have closed.

If the layer had focus, focus returns to the layer that had it before, at the block last focused there. Otherwise focus doesn't change.

If the layer held the window's last blocks, the server closes the window too.

If the session doesn't exist, the call fails with `session_not_found`. If `layer_id` names no layer in the session, it fails with `not_found`.

### `session.close_window`

Close a window and every block in it.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `window_id` |  | The window to close. Leave it out to close the active window. Every block in it closes too. |

The server closes every block in the window, which ends the programs running in them. The call returns once the blocks have closed.

If the window was active, the window after it becomes active, or the one before it if it was last.

If the session doesn't exist, the call fails with `session_not_found`. If `window_id` names a window that isn't in the session, it fails with `not_found`. A window leaves the session as soon as a close starts, so closing the same window twice also fails with `not_found`.

If `window_id` is left out and the session has no windows, the call fails with `conflict`.

### `session.create`

Create a session, optionally with its first windows already in it.

| Field | Required | Description |
| --- | --- | --- |
| `label` |  | The session's label, up to 128 characters. Leave it out to let the server choose one, such as quiet-meadow. |
| `initial_windows` |  | Windows to create with the session, in order. The last one becomes the active window. If any of them fails, the server creates no session. |

Every argument is optional. With none, the server creates a session with no windows.

If `label` is left out or empty, the server picks one that no other session has, such as `quiet-meadow`.

The server sends `session_created` only once every window in `initial_windows` is ready, so no client sees a half-built session. If any window fails, the server closes the session and its blocks, and the call fails.

The calling client becomes the owner of every block in the initial windows.

The call doesn't attach the connection to the new session. Call `session.attach` to receive its events.

If a layout is malformed, the call fails with `bad_request` before any block is created. A flavor that no creator provides fails with `not_found`.

If the server already has as many sessions as it allows, the call fails with `conflict`.

### `session.describe_block`

Get a block's details and the methods, events, and actions it provides.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `block_id` | Yes | The block to describe. It may be a detached block. |

`api` lists the methods and events the block provides, in the same form as `block_creator.list`. Call a method as `<creator>.<method>` with `session_id`, `block_id`, and its arguments under `args`.

If `block_id` names a block that isn't in the session, or one that is closing, the call fails with `not_found`.

### `session.destroy`

Destroy a session and close every block in it.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to destroy. Every block in it closes. |

The server closes every window and block in the session. Closing a terminal hangs up the program running in it, as closing a terminal window does.

The call returns once the session has closed. `session_destroyed` is the last event about the session, though a `client_script_events_dropped` event for it can still follow.

The connection must be attached to the session, with `session.attach`, `session.bootstrap`, or `session.attach_all`, or the call fails with `conflict`.

If `session_id` names no session, the call fails with `session_not_found`.

### `session.detach`

Detach the connection from a session. The connection stays open and the session keeps running.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to detach from. |

The connection's other attachments, and any events it receives through `session.attach_all`, continue as before.

The client leaves the session. The server sends `client_disconnected`, and each block the client owned passes to the next client in line for it, if there is one.

If the connection isn't attached to the session, the call succeeds and changes nothing, even if the session doesn't exist.

A `session.attach` for the same session that is still in progress isn't canceled. This call succeeds without detaching, and the attach completes.

If the connection attaches to the session again later, its client keeps the same `client_id`.

### `session.detach_block`

Take a block out of its window without closing it. The block keeps running.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `block_id` | Yes | The block to detach. It keeps running but leaves its window. |

`session.list_blocks` lists the block with `placement` set to `detached`, and `session.move_block` places it in a window again.

Detaching a block that is already detached succeeds and changes nothing.

If the block was the last one in its window, the window closes too. If it was the only block in a floating layer, the layer closes.

If the block had focus, another block in its window gets focus.

To close the block instead, call `block.close`.

If `block_id` names a block that isn't in the session, the call fails with `not_found`.

### `session.disown_block`

Give up the calling client's claim on blocks. The result lists each block's owner.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `block_ids` |  | The blocks to give up. Leave it out or empty to give up every block in the session. |

The calling client must be attached to the session, with `session.attach` or `session.bootstrap`. A connection that only called `session.attach_all` fails with `bad_request`.

A block the caller owned passes to the next connected client in line for it, or is left with no owner. For a block the caller doesn't own, the caller only leaves the line.

`blocks` lists each block's owner after the call, in the order of `block_ids`, with a repeated ID listed once. When you give up every block, they are sorted by `block_id`.

Every change of owner sends `block_owner_changed`. If a block in a window changes owner, the call also sends one `session_view_changed`.

If any ID in `block_ids` isn't a block in the session, the call fails with `bad_request` and changes nothing.

### `session.float_block`

Move a block out of its window's layout into a new floating layer.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `block_id` | Yes | The block to float. It must be in its window's layout, not already in a floating layer. |
| `bounds` | Yes | Where the new floating layer sits, as [x, y, w, h] fractions of the window's width and height. It must fit inside the window. |

`bounds` is measured from the window's top-left corner. Its width and height must be greater than 0.

The new layer is in the block's own window, in front of every other floating layer there.

Focus and the active window don't change. If the window was zoomed, the zoom ends.

Floating the last block of a window's layout leaves the layout empty. The window stays open, and its layer of kind `tiled` in `session.view` keeps the same `layer_id`.

If the session doesn't exist, the call fails with `session_not_found`. If `block_id` names a block that isn't in any window, including a detached block, it fails with `not_found`, and bad `bounds` fail with `bad_request`.

If the block is already in a floating layer, the call fails with `bad_request`.

### `session.focus_block`

Focus a block, and make its window the active window if it isn't.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `block_id` | Yes | The block to focus. It must be in a window, which becomes the active window. |

The block can be in the window's layout or in a floating layer. If it isn't already its window's focused block, any zoom in that window ends.

If the block already has focus in the active window, the call changes nothing.

If the session doesn't exist, the call fails with `session_not_found`. If `block_id` names a block that isn't in any window, including a detached block, it fails with `not_found`.

### `session.focus_direction`

Move focus to the nearest block to the left, right, above, or below.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `direction` | Yes | Which way to move focus from the focused block in the active window. |
| `wrap` |  | Whether to continue from the opposite edge when no block lies that way. Defaults to false. Older servers ignore it and always wrap. |

The search starts at the focused block of the active window. It covers every block in that window, floating layers included, by where each appears on screen. When two blocks are equally close, the one in front wins.

If no block lies in `direction`, focus doesn't change and the call still succeeds. Moving focus ends any zoom in the window.

If the session doesn't exist, the call fails with `session_not_found`. If the session has no windows, it fails with `conflict`.

### `session.focus_next_pane`

Move focus to the next block in the active window.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `wrap` |  | Whether to go from the last block back to the first. Defaults to false. |

Blocks are in layout order: the window's layout first, reading each split's `before` side ahead of its `after` side, then each floating layer from back to front.

If there's no next block, focus doesn't change and the call still succeeds. Moving focus ends any zoom in the window.

If the session doesn't exist, the call fails with `session_not_found`. If the session has no windows, it fails with `conflict`.

### `session.focus_next_window`

Make the next window the active window.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `wrap` |  | Whether to go from the last window back to the first. Defaults to false. |

The window order is the order of `windows` in `session.view`.

Each window keeps its own focused block, which then receives typing.

If there's no next window, nothing changes and the call still succeeds.

If the session doesn't exist, the call fails with `session_not_found`. If the session has no windows, it fails with `conflict`.

### `session.focus_previous_pane`

Move focus to the previous block in the active window.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `wrap` |  | Whether to go from the first block back to the last. Defaults to false. |

Blocks are in layout order: the window's layout first, reading each split's `before` side ahead of its `after` side, then each floating layer from back to front.

If there's no previous block, focus doesn't change and the call still succeeds. Moving focus ends any zoom in the window.

If the session doesn't exist, the call fails with `session_not_found`. If the session has no windows, it fails with `conflict`.

### `session.focus_previous_window`

Make the previous window the active window.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `wrap` |  | Whether to go from the first window back to the last. Defaults to false. |

The window order is the order of `windows` in `session.view`.

Each window keeps its own focused block, which then receives typing.

If there's no previous window, nothing changes and the call still succeeds.

If the session doesn't exist, the call fails with `session_not_found`. If the session has no windows, it fails with `conflict`.

### `session.focus_window`

Make a window the active window. Focus goes to the block it last had focused.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `window_id` | Yes | The window to make active. Focus goes to the block that was focused in it. |

If the window is already active, the call changes nothing.

If the session doesn't exist, the call fails with `session_not_found`. If `window_id` names a window that isn't in the session, it fails with `not_found`.

### `session.help`

Describe the methods this session accepts and the methods its blocks provide.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |

The result has the same form as `help.all`, with an added `http` field. It describes only the methods the session answers itself, such as `session.view` and `session.new_split`, and the block methods of the creators that session can use.

Most clients call `help.all` instead, which describes every method the server provides.

### `session.list`

List the sessions on the server.

Sessions are listed in the order they were created, until `session.move` changes that order. `session.attach_all` and `session.bootstrap` list them in the same order.

### `session.list_blocks`

List every block in the session, including detached blocks.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |

Treat a `placement` you don't recognize like `detached`. The block exists and you can call its methods, but don't assume where it is shown.

Blocks are sorted by `block_id`.

To see where each block sits in its window and which one has focus, call `session.view`.

### `session.list_clients`

List the clients attached to the session.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |

Only clients attached with `session.attach` or `session.bootstrap` are listed. A connection that only called `session.attach_all` isn't. To list every connected client, call `client.list`.

### `session.lower_layer`

Move a floating layer to the back, or just below another floating layer.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `layer_id` | Yes | The floating layer to move back. |
| `relative_layer_id` |  | Another floating layer in the same window to place it just below. Leave it out to move it behind every other floating layer. |

`layer_id` must name a floating layer. Naming the window's layout, the layer of kind `tiled` in `session.view`, fails the call with `bad_request`.

A floating layer never goes behind the window's layout. At the back, it sits behind every other floating layer and in front of the layout.

If the layer is already in place, the call changes nothing. Focus and the layer's bounds never change.

If the session doesn't exist, the call fails with `session_not_found`. If `layer_id` names no layer in the session, it fails with `not_found`, and if `relative_layer_id` is the same as `layer_id`, it fails with `bad_request`.

If `relative_layer_id` names a layer of kind `tiled`, or a layer in another window, the call fails with `bad_request`. If it names no layer in the session, it fails with `not_found`.

### `session.move`

Move a session to another place in the order that session.list returns.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to move. |
| `before_session_id` |  | The session to place it just before. Leave it out to move the session to the end. |

The order is the same for every client. Each move that changes it sends `session_moved` with the same `session_id` and `before_session_id`.

Moving a session to where it already is succeeds and changes nothing.

The connection must be attached to the session it moves, with `session.attach`, `session.bootstrap`, or `session.attach_all`, or the call fails with `conflict`.

If `before_session_id` names the session being moved, the call fails with `bad_request`.

If either ID names no session, the call fails with `session_not_found`, and the message says which one.

### `session.move_block`

Move a block, or place a detached one, in a new split beside another block.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `block_id` | Yes | The block to move. It may be detached or placed in any window. |
| `anchor_block_id` | Yes | The block to place it beside. It must be placed in a window. |
| `direction` |  | How to divide the anchor's space: 'horizontal' for side by side or 'vertical' for one above the other. Defaults to horizontal. |
| `side` |  | Where the moved block goes: 'before' for the left or top part, or 'after' for the right or bottom part. Defaults to after. |
| `ratio` |  | The fraction of the space that the before part gets, greater than 0 and less than 1. Defaults to 0.5. |

The block and the anchor can be in different windows or layers.

If the block had focus in its window, focus moves with it and its new window becomes active. A detached block doesn't take focus.

Zoom ends in every window the move changes.

If the move empties a floating layer, the server removes it. If it empties a whole window, the server closes the window. A window's layout that empties while the window still has floating layers stays, with the same `layer_id`.

Apart from the two splits named in the result, every split keeps its ID.

If the session doesn't exist, the call fails with `session_not_found`. If `block_id` and `anchor_block_id` are the same, or `ratio` is out of range, it fails with `bad_request`. If either block isn't in the session, or the anchor isn't in a window, it fails with `not_found`.

If the block is detached or in another window, and the anchor's window already has as many blocks as it allows, the call fails with `conflict`.

### `session.move_window`

Move a window to another place in the session's window order.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `window_id` | Yes | The window to move. |
| `before_window_id` |  | The window to place it just before. Leave it out to move the window to the end. |

The window order is the order of `windows` in `session.view` and of the tabs in Rex.app. `session.focus_next_window` and `session.focus_previous_window` follow it.

The active window, focus, and the windows' blocks don't change. If the window is already in place, the call changes nothing.

If the session doesn't exist, the call fails with `session_not_found`. If either window isn't in the session, it fails with `not_found`, and if `before_window_id` is the same as `window_id`, it fails with `bad_request`.

### `session.new_block`

Create a block without placing it in a window. Place it later with session.move_block.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `flavor` | Yes | The flavor of block to create, such as com.superlogical.terminal.shell. block_creator.list lists them. |
| `options` |  | Settings for the block's creator, in the shape of the flavor's options_schema from block_creator.list. |
| `label` |  | The block's label, up to 128 characters. Defaults to the flavor's short name, such as shell. |

The server passes `options` to the flavor's creator unchanged.

The block starts detached: it runs, sends events, and answers method calls, but no window shows it. `session.move_block` can place it beside a block that is in a window.

The calling client becomes the block's owner.

A malformed `flavor` or `label` fails with `bad_request`. A flavor that no creator provides fails with `not_found`.

If the server already has as many blocks as it allows, the call fails with `conflict`.

### `session.new_layer`

Create blocks in a new floating layer at the front of a window.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `window_id` |  | The window to add the floating layer to. Leave it out to use the active window. |
| `bounds` | Yes | Where the new floating layer sits, as [x, y, w, h] fractions of the window's width and height. It must fit inside the window. |
| `layout` | Yes | The blocks to create in the floating layer and how to arrange them. Each node holds either block, with a flavor and optional options and label, or split, with direction, before, after, and an optional ratio. |
| `focus` |  | Whether to make the window active and focus the first new block. Defaults to false. |

`bounds` is measured from the window's top-left corner. Its width and height must be greater than 0.

A `block` node in `layout` starts one new block from a `flavor`, such as `com.superlogical.terminal.shell`. A `split` node divides its space in two and holds another node in each of `before` and `after`.

In a `split` node, `direction` is `horizontal` to put `before` on the left of `after`, or `vertical` to put it on top. `ratio` is the fraction of the space that `before` gets, greater than 0 and less than 1. It defaults to 0.5.

`layout` always starts new blocks. To move a block that is already in a window's layout into a floating layer, use `session.float_block`.

The new layer starts in front of every other floating layer in the window.

Either the whole layout is created or none of it is. If a block fails to start, the server closes the blocks it already started, and the call fails.

If the session doesn't exist, the call fails with `session_not_found`. If `window_id` names a window that isn't in the session, or no creator provides a `flavor`, it fails with `not_found`. Bad `bounds`, or a malformed `layout` or `flavor`, fail with `bad_request`.

If `window_id` is left out and the session has no windows, the call fails with `conflict`.

If the call would go past a limit on blocks per window or blocks on the server, it fails with `conflict`.

### `session.new_split`

Create blocks in a new split beside an existing block or split.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `anchor_id` |  | The block or split whose space to split. A block ID splits that one block. A split ID puts the new blocks beside that split and everything under it. Leave it out to split the focused block of window_id, or of the active window. Don't send it with window_id. |
| `window_id` |  | The window whose focused block to split. Send it only when anchor_id is left out. |
| `direction` | Yes | How to divide the anchor's space: 'horizontal' for side by side or 'vertical' for one above the other. |
| `side` | Yes | Where the new blocks go: 'before' for the left or top part, or 'after' for the right or bottom part. |
| `ratio` |  | The fraction of the space that the before part gets, greater than 0 and less than 1. Defaults to 0.5. |
| `layout` | Yes | The blocks to create beside the anchor and how to arrange them. Each node holds either block, with a flavor and optional options and label, or split, with direction, before, after, and an optional ratio. |
| `focus` |  | Whether to make the anchor's window active and focus the first new block. Defaults to false. |

The server picks the block to split before it starts the new blocks, so a focus change while they start doesn't move the split.

A `block` node in `layout` starts one new block from a `flavor`, such as `com.superlogical.terminal.shell`. A `split` node divides its space in two and holds another node in each of `before` and `after`.

In a `split` node, `direction` is `horizontal` to put `before` on the left of `after`, or `vertical` to put it on top. `ratio` is the fraction of the space that `before` gets, greater than 0 and less than 1. It defaults to 0.5.

Any zoom in the window ends, whether or not `focus` is set.

If `anchor_id` is a split ID, the new split takes that split's place. The old split, with everything under it, goes on one side and `layout` on the other. Anchoring a layer's top split makes the new blocks span the whole layer. A layer with one block has no split, so anchor its block instead.

Splits that already existed keep their IDs.

Either the whole layout is created or none of it is. If a block fails to start, the server closes the blocks it already started, and the call fails.

If the session doesn't exist, the call fails with `session_not_found`. If `window_id` names a window that isn't in the session, or no creator provides a `flavor`, it fails with `not_found`. Sending both `anchor_id` and `window_id`, a bad `ratio`, or a malformed `layout` or `flavor` fail with `bad_request`.

If `anchor_id` names a block that isn't in a window, including a detached block, or a split that isn't in any window, the call fails with `not_found`. If both `anchor_id` and `window_id` are left out and the session has no windows, it fails with `conflict`.

If the call would go past a limit on blocks per window or blocks on the server, it fails with `conflict`.

### `session.new_window`

Create a window and the blocks in its layout. It becomes the active window by default.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `window_label` |  | The window's label, up to 128 characters. Leave it out for a label made from the window's ID, such as Window 89abcdef. |
| `layout` |  | The blocks to create in the window and how to arrange them. Required. Each node holds either block, with a flavor and optional options and label, or split, with direction, before, after, and an optional ratio. |
| `focus` |  | Whether to make the new window the active window. Defaults to true. A session with no active window always makes the new one active. |

A `block` node in `layout` starts one new block from a `flavor`, such as `com.superlogical.terminal.shell`. A `split` node divides its space in two and holds another node in each of `before` and `after`.

In a `split` node, `direction` is `horizontal` to put `before` on the left of `after`, or `vertical` to put it on top. `ratio` is the fraction of the space that `before` gets, greater than 0 and less than 1. It defaults to 0.5.

`layout` always starts new blocks. To place a detached block that already exists, use `session.move_block`.

In the new window, the first block in `block_ids` has focus.

Either the whole layout is created or none of it is. If a block fails to start, the server closes the blocks it already started, and the call fails.

If the session doesn't exist, the call fails with `session_not_found`. If `layout` is missing or malformed, or a `flavor` is malformed, it fails with `bad_request`, and if no creator provides a `flavor`, it fails with `not_found`.

If the call would go past a limit on windows per session, blocks per window, or blocks on the server, it fails with `conflict`.

### `session.own_block`

Claim blocks for the calling client. The result lists each block's owner.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `block_ids` |  | The blocks to claim. Leave it out or empty to claim every block in the session. |
| `mode` |  | How to claim them. 'take' makes the calling client the owner right away. 'request' makes it the owner only of blocks that have none, and it may get the rest when their owner gives them up or leaves. Defaults to take. |

When the clients attached to a session want different things for a block, such as a terminal's size, the block follows its owner. Owning a block gives no other rights over it.

The calling client must be attached to the session, with `session.attach` or `session.bootstrap`. A connection that only called `session.attach_all` fails with `bad_request`.

Either mode puts the caller in line for the block. When the owner gives it up, detaches, or loses its connection, the block passes to the connected client in line that claimed it most recently, with any `take` ahead of every `request`.

An owner whose connection drops gets each of its blocks back when it reconnects, unless that block's owner has changed again since.

`blocks` lists each block's owner after the call, in the order of `block_ids`, with a repeated ID listed once. When you claim every block, they are sorted by `block_id`.

Every change of owner sends `block_owner_changed`. If a block in a window changes owner, the call also sends one `session_view_changed`, because `session.view` shows owners.

A `mode` other than `take` or `request` fails with `bad_request`.

If any ID in `block_ids` isn't a block in the session, the call fails with `bad_request` and changes nothing.

### `session.raise_layer`

Move a floating layer to the front, or just above another floating layer.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `layer_id` | Yes | The floating layer to move forward. |
| `relative_layer_id` |  | Another floating layer in the same window to place it just above. Leave it out to move it in front of every other floating layer. |

`layer_id` must name a floating layer. Naming the window's layout, the layer of kind `tiled` in `session.view`, fails the call with `bad_request`.

If the layer is already in place, the call changes nothing. Focus and the layer's bounds never change.

If the session doesn't exist, the call fails with `session_not_found`. If `layer_id` names no layer in the session, it fails with `not_found`, and if `relative_layer_id` is the same as `layer_id`, it fails with `bad_request`.

If `relative_layer_id` names a layer of kind `tiled`, or a layer in another window, the call fails with `bad_request`. If it names no layer in the session, it fails with `not_found`.

### `session.resize_layer`

Move or resize a floating layer.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `layer_id` | Yes | The floating layer to move or resize. |
| `bounds` | Yes | The layer's new position and size, as [x, y, w, h] fractions of the window's width and height. It must fit inside the window. |

`layer_id` must name a floating layer. Naming the window's layout, the layer of kind `tiled` in `session.view`, fails the call with `bad_request`.

`bounds` is measured from the window's top-left corner. Its width and height must be greater than 0.

Focus and the order of floating layers don't change. Setting the bounds the layer already has changes nothing.

If the session doesn't exist, the call fails with `session_not_found`. If `layer_id` names no layer in the session, it fails with `not_found`, and bad `bounds` fail with `bad_request`.

### `session.resize_pane`

Move the border between two blocks to change their sizes.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `direction` | Yes | The kind of split between the two blocks: 'horizontal' for a border between left and right, or 'vertical' for one between top and bottom. |
| `before_block_id` | Yes | A block on the left of the border, or above it when direction is vertical. |
| `after_block_id` | Yes | A block on the right of the border, or below it when direction is vertical. |
| `ratio` | Yes | The fraction of the split's space that the before part gets, greater than 0 and less than 1. |

The two blocks pick the border. The server finds the innermost split in `direction` that has `before_block_id` on its left or top side and `after_block_id` on its right or bottom side. The blocks don't need to touch.

`ratio` has no default. Unlike in `session.new_split`, 0 doesn't mean an even split.

The blocks can be in any window, active or not. Focus doesn't change.

The call always raises `revision` and sends `session_view_changed`, even if the border was already where you asked.

If the session doesn't exist, the call fails with `session_not_found`. If the two block IDs are the same, or `ratio` is out of range, it fails with `bad_request`, and if either block isn't in any window, it fails with `not_found`.

If the blocks are in different windows or layers, or no split between them matches, the call fails with `bad_request`.

### `session.set_block_label`

Set a block's label.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `block_id` |  | The block to relabel. Required. |
| `label` |  | The new label, up to 128 characters. An empty or missing label restores a default made from the block's ID. |

`block_id` is required. Leaving it out fails with `bad_request`.

The server trims whitespace from both ends of `label`. An empty `label` restores a default made from the block's ID, such as `Block 89abcdef`, rather than the flavor name a new block starts with.

A label longer than 128 characters, or one that contains a control character, fails with `bad_request`.

Relabeling a block that is in a window sends `session_view_changed`. A detached block isn't part of the view, so relabeling it sends no event and leaves `revision` unchanged.

If `block_id` names a block that isn't in the session, the call fails with `not_found`.

### `session.set_label`

Set the session's label.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `label` |  | The new label, up to 128 characters. An empty or missing label restores the label the server chose when it created the session. |

The server trims whitespace from both ends of `label`. An empty `label` restores the label the server chose when it created the session, such as `quiet-meadow`. It does so even if the session was created with a label.

A label longer than 128 characters, or one that contains a control character, fails with `bad_request`.

A new label sends `session_label_changed`, then `session_view_changed`. Setting the label the session already has changes nothing.

### `session.set_layer_state`

Minimize a floating layer or return it to normal, optionally raising and focusing it.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `layer_id` | Yes | The layer to change. |
| `state` | Yes | The state to set: 'normal' or 'minimized'. |
| `focus` |  | Whether to also raise the layer, focus the block last focused in it, and make its window active, all in one change. Needs state to be 'normal' and the layer to have a block. Defaults to false. |

Only a floating layer can be minimized. Minimizing the window's layout, the layer of kind `tiled` in `session.view`, fails with `bad_request`. Setting it to `normal` succeeds and changes nothing.

With `focus` set to `true`, the layer is also raised, the block last focused in it takes focus, or its first block, and its window becomes active, all in one revision. This also works on a layer that is already normal and on the tiled layer. `focus` with `minimized`, or on a layer with no blocks, fails with `bad_request` and changes nothing.

`state` must be `normal` or `minimized`. Any other value fails with `bad_request`.

A minimized layer stays in `session.view` with `state` set to `minimized`, its blocks keep running, and it keeps its bounds and its place in the stack. `state` is left out for a normal layer.

Minimizing the layer that has focus moves focus to the layer used before it, or to the nearest visible pane, and clears zoom. Returning a layer to normal leaves focus where it is. Setting the state the layer already has changes nothing.

A window must keep a visible pane, so minimizing the layer that holds its last visible panes fails with `bad_request`.

If the session doesn't exist, the call fails with `session_not_found`. If `layer_id` names no layer in the session, it fails with `not_found`.

### `session.set_window_label`

Set a window's label.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `window_id` | Yes | The window to relabel. |
| `label` |  | The new label, up to 128 characters. An empty or missing label restores a default made from the window's ID. |

The server trims whitespace from both ends of `label`. An empty `label` restores a default made from the window's ID, such as `Window 89abcdef`.

A label longer than 128 characters, or one that contains a control character, fails with `bad_request`.

A new label sends `window_label_changed`, then `session_view_changed`. Setting the label the window already has changes nothing.

If `window_id` names a window that isn't in the session, the call fails with `not_found`.

### `session.swap_blocks`

Swap the places of two blocks.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `block_id` | Yes | One of the two blocks to swap. |
| `other_block_id` | Yes | The other block. The order of the two doesn't matter. |

Both blocks must be in a window. They can be in different windows or layers.

Only the two blocks move. Splits, their sizes and IDs, floating layers, and the window order stay the same.

If one of the blocks has focus in the active window, focus follows it to its new place, and that place's window becomes active.

If the session doesn't exist, the call fails with `session_not_found`. If `block_id` and `other_block_id` are the same, it fails with `bad_request`. If either block isn't in any window, including a detached block, it fails with `not_found`.

### `session.view`

Get the session's windows, their layouts, and their blocks in one snapshot.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |

`revision` starts at zero for a new session and goes up by one each time the view changes. Compare revisions only within one session, and keep the view with the highest revision.

A `session_view_changed` event carries only the new revision, not the view. These events can arrive out of revision order, so call `session.view` only when an event's revision is higher than the one in the view you hold.

`window_created`, `window_closed`, `window_label_changed`, `active_window_changed`, and `session_label_changed` arrive before the `session_view_changed` event for the same change.

The view holds only blocks that are in a window. `session.list_blocks` also lists detached blocks.

The view describes structure, not content. To read what a block shows, call one of its own methods, such as `com.superlogical.terminal.format`.

### `session.zoom_block`

Zoom a block to fill its window, or undo the zoom.

| Field | Required | Description |
| --- | --- | --- |
| `session_id` | Yes | The session to act on. |
| `block_id` |  | The block to zoom. It takes focus, and its window becomes active. Leave it out to use the focused block of window_id, or of the active window. |
| `window_id` |  | A window whose focused block to zoom. The active window doesn't change. Send it only when block_id is left out. |
| `zoom` |  | Set true to zoom or false to restore the layout. Leave it out to switch between the two. |

With `block_id`, the server first focuses the block and makes its window active, as `session.focus_block` does. With `window_id`, focus and the active window don't change.

Zoom changes only what is shown. The layout stays the same, and the zoom ends when focus moves to another block in the window.

If the session doesn't exist, the call fails with `session_not_found`. If `block_id` names a block that isn't in any window, or `window_id` names a window that isn't in the session, it fails with `not_found`. Sending both fails with `bad_request`.

If both `block_id` and `window_id` are left out and the session has no windows, the call fails with `conflict`.

## target

### `target.complete`

List the labels, window positions, and short IDs that start with a prefix. Use it to complete a name as someone types it.

| Field | Required | Description |
| --- | --- | --- |
| `kinds` | Yes | What to complete: session, window, block, or client, with the same rules as in target.resolve. Completions come back in this order. |
| `prefix` | Yes | The text typed so far. It matches the start of labels (ignoring case), window positions, and short IDs. An empty prefix matches everything. |
| `context` |  | Where the caller is. Only session_id is used: without scope.session, windows and blocks come from that session. |
| `scope` |  | Limits completion to one session, or block completion to one window, as in target.resolve. |

`kinds`, `scope`, and `context.session_id` choose what to search the same way as in `target.resolve`, and the call fails with `bad_request` in the same cases.

`prefix` matches the start of labels, ignoring case, and label completions keep the label's own case. White space around `prefix` is ignored. A control character inside it, such as a tab, fails the call with `bad_request`.

Windows also match by position, so the prefix `1` matches windows 1, 10, 11, and so on. An empty prefix matches every position.

An empty prefix, or one made only of hexadecimal digits, also matches the start of short IDs. A short ID is the last six hexadecimal digits of an ID, such as `12bef5`, and `target.resolve` accepts it as the end of an ID.

One thing can appear more than once, such as a window whose label and short ID both start with the prefix.

Completions are grouped by kind, in the order of `kinds`. Within each kind, labels come first, sorted ignoring case, then window positions in order, then short IDs in sorted order.

If `scope.session` or `scope.window` matches nothing or several things, `completions` is empty. The call doesn't fail.

Detached blocks are included unless `scope.window` is set.

To complete a client, set `kinds` to `["client"]`. Clients complete by label and short ID, and each target's `where` names the program, such as `Rex.app 1.6.0`.

### `target.resolve`

Look up sessions, windows, blocks, or clients by ID, label, or position. Each query gets its own result, which says whether it matched one thing, nothing, or several.

| Field | Required | Description |
| --- | --- | --- |
| `kinds` | Yes | What each query may name: session, window, block, or client. List each kind once, and list client only on its own. |
| `queries` | Yes | The names to look up. Each is a full ID, a label, a window position, the end of an ID, or an empty string for the default. |
| `context` |  | Where the caller is, such as the terminal a script runs in. IDs in it that no longer exist are ignored. |
| `scope` |  | Limits the search to one session, or a block search to one window. Not allowed when kinds includes session. |

List at least one kind in `kinds`, each only once, and `client` only on its own, or the call fails with `bad_request`. A kind the server doesn't know fails the same way, which can happen when the server is older than your client.

Send at least one query, or the call fails with `bad_request`. Each query gets one entry in `results`, in the same order. A query that matches nothing or several things doesn't fail the call: its `status` is `not_found` or `ambiguous`.

The server tries each query in four steps and stops at the first step that matches anything: full ID, label (ignoring case), window position, then the end of an ID (4 to 32 hexadecimal digits). So a window labeled `2` wins over the second window. If the winning step matches more than one thing, the result is `ambiguous`.

A query that starts like a full ID, such as `block:`, is only tried as a full ID. Its result is `not_found` if the ID is malformed, if `kinds` doesn't include that kind, or if nothing has that ID.

Only windows have positions. `1` is the first window in its session, in the order Rex.app shows them as tabs. When several sessions are searched, a position can match a window in each one.

When `kinds` lists more than one kind, one query can match different kinds of things, such as a session and a window with the same label. The result is then `ambiguous`, and each candidate's `kind` says what it is.

Without `scope.session`, windows and blocks are searched only in the session named by `context.session_id`. If that's left out or the session no longer exists, every session is searched. This applies to full IDs too, so the full ID of a block in another session is `not_found`.

If a scope matches nothing or several things, every result gets that `not_found` or `ambiguous` status, and its `reason` says which scope failed. For an ambiguous scope, `candidates` lists what the scope could mean.

An empty query, or one of only spaces, asks for a default, such as the block a script runs in. When `kinds` lists several kinds, an empty query gets the default for the most specific one: block, then window, then session.

An empty session query selects `context.session_id`, or the only session if there's just one. With no sessions it's `not_found`, and with several it's `ambiguous`.

An empty window query selects the window that holds `context.block_id`, or else the active window of the session being searched. If several sessions are searched, it's `ambiguous` even with `context.block_id` set, and `candidates` lists the sessions to choose from.

An empty block query selects `context.block_id` if that block is among those searched. Otherwise it selects the focused block of the window an empty window query would choose. With `scope.window`, it ignores `context.block_id` and selects that window's focused block.

Block searches include detached blocks, which are running but aren't placed in any window. `scope.window` leaves them out. A block's `where` reads `window 2` or `detached`. When several sessions are searched, it names the session too, as in `window 2 of work`, and a window's `where` reads `in work`.

To look up a connected client, such as Rex.app, set `kinds` to `["client"]`. A client query matches a full ID, a label, or the end of an ID, since clients have no positions. Every connected client is searched, unless `scope.session` limits the search to clients attached to that session.

An empty client query selects `context.client_id` if that client is among those searched. Otherwise it selects the only client that reported its kind as `app` when it connected, such as Rex.app. With no app it's `not_found`, and with several it's `ambiguous`.

`scope.window` is looked up among the sessions being searched. The call fails with `bad_request` if `kinds` includes `session` and `scope` is sent, even as an empty object, or if `scope.window` is set and `kinds` isn't just `block`. A query or scope that contains a control character, such as a newline, fails the same way.

---
title: "Mac Actions"
canonical_url: https://www.superlogical.com/rex/docs/reference/mac-actions
---

# Mac Actions

This page lists every action in the Rex for Mac command palette. Use the name to [bind a key](https://www.superlogical.com/rex/docs/customize/keyboard-shortcuts.md) or to run it with `rex do`. Anything tagged [Bleeding Edge](https://www.superlogical.com/rex/docs/reference/mac-settings.md#setting-app-version-update-channel) is on that update channel and not yet in Preview.

<div class="reference-page actions" hidden></div>

## Window

| Action | Name | Default Shortcut |
| --- | --- | --- |
| <span id="action-client-window-new" data-entry="New Window"></span>New Window | `client.window.new` <span hidden>create workspace window</span> |  <span hidden>Default shortcut:</span>`cmd+n` |
| <span id="action-client-window-close" data-entry="Close Window"></span>Close Window | `client.window.close` <span hidden>close workspace window</span> |  <span hidden>Default shortcut:</span>`shift+cmd+w` |
| <span id="action-client-window-fullscreen-toggle" data-entry="Toggle Full Screen"></span>Toggle Full Screen | `client.window.fullscreen.toggle` <span hidden>full screen fullscreen enter exit window</span> |  <span hidden>Default shortcut:</span>`ctrl+cmd+f` |
| <span id="action-client-close" data-entry="Close"></span>Close Closes the focused pane, else the selected tab, else the window. | `client.close` <span hidden>close pane tab window focused</span> |  <span hidden>Default shortcut:</span>`cmd+w` |

## App

| Action | Name | Default Shortcut |
| --- | --- | --- |
| <span id="action-client-palette-toggle" data-entry="Command Palette…"></span>Command Palette… | `client.palette.toggle` <span hidden>command palette search commands actions</span> |  <span hidden>Default shortcut:</span>`shift+cmd+p` |
| <span id="action-client-settings" data-entry="Settings…"></span>Settings… | `client.settings` <span hidden>preferences settings options configure</span> |  <span hidden>Default shortcut:</span>`cmd+,` |
| <span id="action-client-diagnostics" data-entry="Rex Diagnostics"></span>Rex Diagnostics | `client.diagnostics` <span hidden>diagnostics logs debug troubleshoot server report</span> |  <span hidden>Default shortcut:</span>`shift+cmd+,` |
| <span id="action-client-onboarding" data-entry="Open Onboarding Session…"></span>Open Onboarding Session… | `client.onboarding` <span hidden>onboarding welcome setup get started tour walkthrough migrate import keybindings theme appearance ghostty tmux zellij agent</span> |  |
| <span id="action-client-permissions" data-entry="Grant System Permissions…"></span>Grant System Permissions… | `client.permissions` <span hidden>permissions privacy security full disk access screen recording system audio grant allow system settings tcc</span> |  |
| <span id="action-client-visibility-toggle" data-entry="Toggle Visibility"></span>Toggle Visibility Shows Rex from any app, or hides it when it's in front. | `client.visibility.toggle` <span hidden>show hide rex global hotkey summon bring to front focus toggle_visibility</span> |  |
| <span id="action-client-version-copy" data-entry="Copy Version Info"></span>Copy Version Info Copies the app and server versions to the clipboard. | `client.version.copy` <span hidden>version build number copy clipboard about app server release</span> |  |
| <span id="action-client-debug-copy" data-entry="Copy Debug Info"></span>Copy Debug Info Copies a report of the app, system, server, and host state to the clipboard. | `client.debug.copy` <span hidden>debug debugging info report copy clipboard diagnostics troubleshoot support bug commit build environment</span> |  |
| <span id="action-client-pair-device" data-entry="Pair Device with QR Code…"></span>Pair Device with QR Code… | `client.pair_device` <span hidden>pair device qr code scan iphone ipad ios mobile tailscale connect link</span> |  |
| <span id="action-client-update-accept" data-entry="Install Update"></span>Install Update The menu shows the current update's own title; this is the static fallback. | `client.update.accept` <span hidden>update install retry app application version upgrade sparkle</span> |  <span hidden>Default shortcut:</span>`shift+cmd+u` |
| <span id="action-client-update-accept-when-idle" data-entry="Relaunch when Idle"></span>Relaunch when Idle Relaunches into a downloaded update once Rex's windows are out of sight. | `client.update.accept_when_idle` <span hidden>update install relaunch restart idle later automatic</span> |  <span hidden>Default shortcut:</span>`alt+shift+cmd+u` |
| <span id="action-client-update-check" data-entry="Check for Updates in Background"></span>Check for Updates in Background Checks for a Rex update without showing anything, and returns what it found. | `client.update.check` <span hidden>update check background quiet silent app application version sparkle</span> |  |
| <span id="action-client-update-status" data-entry="Report Update Status"></span>Report Update Status Returns the running version, the update settings, and the updater's state. | `client.update.status` <span hidden>update status version build channel feed state report sparkle</span> |  |
| <span id="action-client-update-install" data-entry="Install Update and Relaunch"></span>Install Update and Relaunch Downloads the available Rex update if needed, installs it, and relaunches without asking. | `client.update.install` <span hidden>update install download relaunch restart upgrade app application sparkle unattended</span> |  |
| <span id="action-client-update-set-automation" data-entry="Set Update Automation"></span>Set Update Automation Chooses whether Rex only offers updates, downloads them, or installs them. | `client.update.set_automation` <span hidden>update automatic automation download install notify setting preference</span> |  |
| <span id="action-client-update-set-channel" data-entry="Set Update Channel"></span>Set Update Channel Chooses the release channel Rex updates from. | `client.update.set_channel` <span hidden>update channel feed preview snapshot bleeding edge release setting preference</span> |  |
| <span id="action-client-notice-dismiss" data-entry="Dismiss Notice"></span>Dismiss Notice | `client.notice.dismiss` <span hidden>dismiss notice banner alert hide</span> |  <span hidden>Default shortcut:</span>`shift+cmd+.` |
| <span id="action-client-shortcuts-edit" data-entry="Change Shortcut…"></span>Change Shortcut… | `client.shortcuts.edit` <span hidden>keyboard key binding bindings keybinding keybindings rebind record hotkey shortcut</span> |  |
| <span id="action-client-agents-setup" data-entry="Install Rex for Agents"></span>Install Rex for Agents Installs the Rex skill and hooks for Claude Code, Codex, and other agents. | `client.agents.setup` <span hidden>`rex skill` install agent agents coding ai claude code codex cursor copilot gemini hook hooks setup</span> |  |
| <span id="action-client-server-restart" data-entry="Restart Server…"></span>Restart Server… Stops the local Rex server and starts it again. | `client.server.restart` <span hidden>restart reload relaunch reboot stop start server rex daemon process local</span> |  |

## Session

| Action | Name | Default Shortcut |
| --- | --- | --- |
| <span id="action-session-new" data-entry="New Session"></span>New Session | `session.new` <span hidden>create session add</span> |  <span hidden>Default shortcut:</span>`shift+cmd+n` |
| <span id="action-session-switch" data-entry="Change Session…"></span>Change Session… | `session.switch` <span hidden>session switch jump go</span> |  <span hidden>Default shortcut:</span>`cmd+k` |
| <span id="action-session-select" data-entry="Go to Session"></span>Go to Session | `session.select` <span hidden>session select show jump go identifier id</span> |  |
| <span id="action-session-next" data-entry="Next Session"></span>Next Session | `session.next` <span hidden>cycle session forward</span> |  |
| <span id="action-session-previous" data-entry="Previous Session"></span>Previous Session | `session.previous` <span hidden>cycle session back</span> |  |
| <span id="action-session-rename" data-entry="Rename Session"></span>Rename Session | `session.rename` <span hidden>rename session name title</span> |  |
| <span id="action-session-close" data-entry="Close Session"></span>Close Session | `session.close` <span hidden>close kill destroy delete session</span> |  |
| <span id="action-session-refresh" data-entry="Refresh Sessions"></span>Refresh Sessions | `session.refresh` <span hidden>reload sync sessions</span> |  <span hidden>Default shortcut:</span>`cmd+r` |
| <span id="action-session-collapse" data-entry="Collapse Session"></span>Collapse Session | `session.collapse` <span hidden>collapse fold hide session sidebar tabs</span> |  |
| <span id="action-session-expand" data-entry="Expand Session"></span>Expand Session | `session.expand` <span hidden>expand unfold show open session sidebar tabs</span> |  |
| <span id="action-session-collapse-others" data-entry="Collapse Other Sessions"></span>Collapse Other Sessions | `session.collapse_others` <span hidden>collapse fold hide other all sessions sidebar focus</span> |  |

## Host

| Action | Name | Default Shortcut |
| --- | --- | --- |
| <span id="action-client-host-switch" data-entry="Switch Host"></span>Switch Host | `client.host.switch` <span hidden>host remote machine server switch go</span> |  |
| <span id="action-client-host-add" data-entry="Add Remote Host…"></span>Add Remote Host… | `client.host.add` <span hidden>host remote add server machine url http https connect</span> |  |
| <span id="action-client-host-remove" data-entry="Remove Host…"></span>Remove Host… | `client.host.remove` <span hidden>remove forget delete disconnect host remote server machine</span> |  |
| <span id="action-client-host-forward-port" data-entry="Forward Port…"></span>Forward Port… <span class="row-tags"><span class="api-tag channel">[Bleeding Edge](https://www.superlogical.com/rex/docs/reference/mac-settings.md#setting-app-version-update-channel)</span></span> | `client.host.forward_port` <span hidden>port forward forwarding tunnel localhost open browser ssh `-L` local remote host</span> |  |

## Tab

| Action | Name | Default Shortcut |
| --- | --- | --- |
| <span id="action-client-tab-new" data-entry="New Tab"></span>New Tab | `client.tab.new` <span hidden>create tab window add</span> |  <span hidden>Default shortcut:</span>`cmd+t` |
| <span id="action-client-tab-close" data-entry="Close Tab"></span>Close Tab | `client.tab.close` <span hidden>close tab window</span> |  <span hidden>Default shortcut:</span>`alt+cmd+w` |
| <span id="action-client-tab-close-others" data-entry="Close Other Tabs"></span>Close Other Tabs | `client.tab.close_others` <span hidden>close other tabs windows kill only keep</span> |  |
| <span id="action-client-tab-rename" data-entry="Rename Tab"></span>Rename Tab | `client.tab.rename` <span hidden>rename tab window name title</span> |  |
| <span id="action-client-tab-next" data-entry="Show Next Tab"></span>Show Next Tab | `client.tab.next` <span hidden>cycle tab window forward</span> |  <span hidden>Default shortcut:</span>`ctrl+tab`, `shift+cmd+]` |
| <span id="action-client-tab-previous" data-entry="Show Previous Tab"></span>Show Previous Tab | `client.tab.previous` <span hidden>cycle tab window back</span> |  <span hidden>Default shortcut:</span>`ctrl+shift+tab`, `shift+cmd+[` |
| <span id="action-client-tab-goto" data-entry="Go to Tab"></span>Go to Tab | `client.tab.goto` <span hidden>tab window select number index jump go</span> |  <span hidden>Default shortcut:</span>`cmd+[Digit1]`, `cmd+[Digit2]`, `cmd+[Digit3]`, `cmd+[Digit4]`, `cmd+[Digit5]`, `cmd+[Digit6]`, `cmd+[Digit7]`, `cmd+[Digit8]`, `cmd+[Digit9]` |
| <span id="action-client-tab-move" data-entry="Move Tab"></span>Move Tab | `client.tab.move` <span hidden>move tab window reorder</span> |  |
| <span id="action-client-tab-move-backward" data-entry="Move Tab Backward"></span>Move Tab Backward | `client.tab.move.backward` <span hidden>move tab window reorder left up earlier</span> |  <span hidden>Default shortcut:</span>`ctrl+shift+pageup` |
| <span id="action-client-tab-move-forward" data-entry="Move Tab Forward"></span>Move Tab Forward | `client.tab.move.forward` <span hidden>move tab window reorder right down later</span> |  <span hidden>Default shortcut:</span>`ctrl+shift+pagedown` |
| <span id="action-client-tab-move-to-new-window" data-entry="Move Tab to New Window"></span>Move Tab to New Window | `client.tab.move_to_new_window` <span hidden>move tab new window tear off detach separate pop out own</span> |  |

## Pane

| Action | Name | Default Shortcut |
| --- | --- | --- |
| <span id="action-pane-split" data-entry="Split Pane"></span>Split Pane | `pane.split` <span hidden>pane split terminal divide new</span> |  |
| <span id="action-pane-split-left" data-entry="Split Pane Left"></span>Split Pane Left | `pane.split.left` <span hidden>pane split terminal divide new left</span> |  <span hidden>Default shortcut:</span>`shift+cmd+left` |
| <span id="action-pane-split-right" data-entry="Split Pane Right"></span>Split Pane Right | `pane.split.right` <span hidden>pane split terminal divide new right</span> |  <span hidden>Default shortcut:</span>`cmd+d`, `shift+cmd+right` |
| <span id="action-pane-split-up" data-entry="Split Pane Up"></span>Split Pane Up | `pane.split.up` <span hidden>pane split terminal divide new above up</span> |  <span hidden>Default shortcut:</span>`shift+cmd+up` |
| <span id="action-pane-split-down" data-entry="Split Pane Down"></span>Split Pane Down | `pane.split.down` <span hidden>pane split terminal divide new below down</span> |  <span hidden>Default shortcut:</span>`shift+cmd+d`, `shift+cmd+down` |
| <span id="action-pane-split-auto" data-entry="Split Pane Best Fit"></span>Split Pane Best Fit | `pane.split.auto` <span hidden>pane split terminal divide new automatic best fit</span> |  <span hidden>Default shortcut:</span>`cmd+enter` |
| <span id="action-pane-focus" data-entry="Focus Pane"></span>Focus Pane | `pane.focus` <span hidden>pane focus navigate</span> |  |
| <span id="action-pane-focus-left" data-entry="Focus Pane Left"></span>Focus Pane Left | `pane.focus.left` <span hidden>pane focus navigate left</span> |  <span hidden>Default shortcut:</span>`alt+cmd+left` |
| <span id="action-pane-focus-right" data-entry="Focus Pane Right"></span>Focus Pane Right | `pane.focus.right` <span hidden>pane focus navigate right</span> |  <span hidden>Default shortcut:</span>`alt+cmd+right` |
| <span id="action-pane-focus-up" data-entry="Focus Pane Up"></span>Focus Pane Up | `pane.focus.up` <span hidden>pane focus navigate up above</span> |  <span hidden>Default shortcut:</span>`alt+cmd+up` |
| <span id="action-pane-focus-down" data-entry="Focus Pane Down"></span>Focus Pane Down | `pane.focus.down` <span hidden>pane focus navigate down below</span> |  <span hidden>Default shortcut:</span>`alt+cmd+down` |
| <span id="action-pane-focus-next" data-entry="Focus Next Pane"></span>Focus Next Pane | `pane.focus_next` <span hidden>pane focus next forward</span> |  <span hidden>Default shortcut:</span>`cmd+]` |
| <span id="action-pane-focus-previous" data-entry="Focus Previous Pane"></span>Focus Previous Pane | `pane.focus_previous` <span hidden>pane focus previous back</span> |  <span hidden>Default shortcut:</span>`cmd+[` |
| <span id="action-pane-move" data-entry="Move Pane"></span>Move Pane | `pane.move` <span hidden>pane move swap rearrange</span> |  |
| <span id="action-pane-move-left" data-entry="Move Pane Left"></span>Move Pane Left | `pane.move.left` <span hidden>pane move swap rearrange left</span> |  <span hidden>Default shortcut:</span>`alt+shift+cmd+left` |
| <span id="action-pane-move-right" data-entry="Move Pane Right"></span>Move Pane Right | `pane.move.right` <span hidden>pane move swap rearrange right</span> |  <span hidden>Default shortcut:</span>`alt+shift+cmd+right` |
| <span id="action-pane-move-up" data-entry="Move Pane Up"></span>Move Pane Up | `pane.move.up` <span hidden>pane move swap rearrange up above</span> |  <span hidden>Default shortcut:</span>`alt+shift+cmd+up` |
| <span id="action-pane-move-down" data-entry="Move Pane Down"></span>Move Pane Down | `pane.move.down` <span hidden>pane move swap rearrange down below</span> |  <span hidden>Default shortcut:</span>`alt+shift+cmd+down` |
| <span id="action-pane-float" data-entry="Float Pane"></span>Float Pane | `pane.float` <span hidden>pane float floating window dock undock detach overlay pop out tile</span> |  <span hidden>Default shortcut:</span>`alt+cmd+enter` |
| <span id="action-pane-new-floating" data-entry="New Floating Pane"></span>New Floating Pane | `pane.new_floating` <span hidden>pane new create open float floating window terminal overlay popup scratchpad</span> |  |
| <span id="action-pane-minimize-floating" data-entry="Minimize Floating Window"></span>Minimize Floating Window | `pane.minimize_floating` <span hidden>pane floating window hide show toggle minimize minimise restore taskbar capsule stash scratchpad</span> |  <span hidden>Default shortcut:</span>`alt+cmd+m` |
| <span id="action-pane-minimize-all-floating" data-entry="Minimize Floating Windows"></span>Minimize Floating Windows | `pane.minimize_all_floating` <span hidden>pane floating windows all hide show toggle minimize minimise restore taskbar capsule stash scratchpad</span> |  <span hidden>Default shortcut:</span>`alt+shift+cmd+enter` |
| <span id="action-pane-zoom" data-entry="Zoom Pane"></span>Zoom Pane | `pane.zoom` <span hidden>pane zoom maximize restore terminal</span> |  <span hidden>Default shortcut:</span>`shift+cmd+enter` |
| <span id="action-pane-close" data-entry="Close Pane"></span>Close Pane | `pane.close` <span hidden>pane close remove terminal</span> |  <span hidden>Default shortcut:</span>`cmd+w` |
| <span id="action-pane-close-others" data-entry="Close Other Panes"></span>Close Other Panes | `pane.close_others` <span hidden>close other panes kill only keep focused</span> |  |
| <span id="action-pane-balance" data-entry="Balance Splits"></span>Balance Splits | `pane.balance` <span hidden>pane split balance equalize equalise evenly distribute resize layout</span> |  |
| <span id="action-pane-resize" data-entry="Resize Pane"></span>Resize Pane | `pane.resize` <span hidden>pane resize grow shrink divider wider narrower taller shorter</span> |  |
| <span id="action-pane-move-to-new-tab" data-entry="Move Pane to New Tab"></span>Move Pane to New Tab | `pane.move_to_new_tab` <span hidden>pane block move new window tab detach split</span> |  |
| <span id="action-pane-move-to-tab" data-entry="Move Pane to Tab…"></span>Move Pane to Tab… | `pane.move_to_tab` <span hidden>pane block move tab window send other existing join</span> |  |
| <span id="action-pane-go-to-directory" data-entry="Go to Directory…"></span>Go to Directory… | `pane.go_to_directory` <span hidden>go to directory folder path cd new tab open navigate</span> |  <span hidden>Default shortcut:</span>`shift+cmd+g` |
| <span id="action-pane-go-to" data-entry="Go to Anything…"></span>Go to Anything… | `pane.go_to` <span hidden>go to anything pane block tab session agent jump switch navigate find search tree overview</span> |  <span hidden>Default shortcut:</span>`cmd+p` |
| <span id="action-pane-view-activity" data-entry="View Activity…"></span>View Activity… | `pane.view_activity` <span hidden>view activity active working blocked failed done agents programs status priority</span> |  |
| <span id="action-pane-insert-path" data-entry="Insert Path…"></span>Insert Path… | `pane.insert_path` <span hidden>insert path file folder directory cursor type paste complete browse</span> |  <span hidden>Default shortcut:</span>`ctrl+cmd+i` |
| <span id="action-pane-copy-id" data-entry="Copy Pane ID"></span>Copy Pane ID Copies the focused pane's block ID to the clipboard. | `pane.copy_id` <span hidden>pane block id identifier copy clipboard target agent capture</span> |  |
| <span id="action-pane-rename" data-entry="Rename Pane"></span>Rename Pane | `pane.rename` <span hidden>rename pane block name title label</span> |  |
| <span id="action-pane-paste" data-entry="Paste into Pane"></span>Paste into Pane Pastes the clipboard into the focused terminal. | `pane.paste` <span hidden>paste clipboard insert text terminal</span> |  |
| <span id="action-pane-copy" data-entry="Copy from Pane"></span>Copy from Pane Copies the focused terminal's selection to the clipboard. | `pane.copy` <span hidden>copy selection clipboard text terminal</span> |  |
| <span id="action-pane-select-all" data-entry="Select All in Pane"></span>Select All in Pane Selects everything in the focused terminal, including its scrollback. | `pane.select_all` <span hidden>select all selection text terminal scrollback</span> |  |
| <span id="action-pane-send-key" data-entry="Send Key"></span>Send Key Sends one key to the focused terminal, as if it had been pressed there. | `pane.send_key` <span hidden>send key terminal press chord binding</span> |  <span hidden>Default shortcut:</span>`cmd+backspace`, `cmd+left`, `cmd+right`, `alt+left`, `alt+right` |
| <span id="action-client-key-forward" data-entry="Send Key to Server"></span>Send Key to Server Sends the key that performed it to the focused terminal, for a script to answer. | `client.key.forward` <span hidden>send key forward server script lua binding</span> |  |
| <span id="action-client-prompt-previous" data-entry="Jump to Previous Prompt"></span>Jump to Previous Prompt | `client.prompt.previous` <span hidden>jump scroll previous prompt command mark shell integration up back history</span> |  <span hidden>Default shortcut:</span>`cmd+up` |
| <span id="action-client-prompt-next" data-entry="Jump to Next Prompt"></span>Jump to Next Prompt | `client.prompt.next` <span hidden>jump scroll next prompt command mark shell integration down forward</span> |  <span hidden>Default shortcut:</span>`cmd+down` |
| <span id="action-client-scroll-page-up" data-entry="Scroll Page Up"></span>Scroll Page Up | `client.scroll.page_up` <span hidden>scroll page up back scrollback history</span> |  |
| <span id="action-client-scroll-page-down" data-entry="Scroll Page Down"></span>Scroll Page Down | `client.scroll.page_down` <span hidden>scroll page down forward scrollback history</span> |  |
| <span id="action-client-quick-look" data-entry="Quick Look"></span>Quick Look Previews the files and web links on screen in Quick Look. | `client.quick_look` <span hidden>quick look preview file image link path url web pointer hover</span> |  <span hidden>Default shortcut:</span>`cmd+y` |

## Keys

| Action | Name | Default Shortcut |
| --- | --- | --- |
| <span id="action-client-mode-enter" data-entry="Enter Mode"></span>Enter Mode Makes the keys a mode binds perform, until the mode is left. | `client.mode.enter` <span hidden>mode enter key table prefix resize modal</span> |  |
| <span id="action-client-mode-exit" data-entry="Exit Mode"></span>Exit Mode | `client.mode.exit` <span hidden>mode exit leave quit key table escape</span> |  |
| <span id="action-client-mode-exit-all" data-entry="Exit All Modes"></span>Exit All Modes | `client.mode.exit_all` <span hidden>mode exit leave quit all key table escape</span> |  |

## Find

| Action | Name | Default Shortcut |
| --- | --- | --- |
| <span id="action-client-find-open" data-entry="Find…"></span>Find… | `client.find.open` <span hidden>find search text scrollback highlight</span> |  <span hidden>Default shortcut:</span>`cmd+f` |
| <span id="action-client-find-next" data-entry="Find Next"></span>Find Next | `client.find.next` <span hidden>find search next forward match</span> |  <span hidden>Default shortcut:</span>`cmd+g` |
| <span id="action-client-find-previous" data-entry="Find Previous"></span>Find Previous | `client.find.previous` <span hidden>find search previous back match</span> |  <span hidden>Default shortcut:</span>`shift+cmd+g` |

## View

| Action | Name | Default Shortcut |
| --- | --- | --- |
| <span id="action-client-font-increase" data-entry="Increase Font Size"></span>Increase Font Size | `client.font.increase` <span hidden>terminal font size larger bigger increase zoom text typography</span> |  <span hidden>Default shortcut:</span>`shift+cmd+=`, `cmd+=` |
| <span id="action-client-font-decrease" data-entry="Decrease Font Size"></span>Decrease Font Size | `client.font.decrease` <span hidden>terminal font size smaller decrease zoom text typography</span> |  <span hidden>Default shortcut:</span>`cmd+-` |
| <span id="action-client-font-reset" data-entry="Reset Font Size"></span>Reset Font Size | `client.font.reset` <span hidden>terminal font size reset default original text typography</span> |  <span hidden>Default shortcut:</span>`cmd+[Digit0]` |
| <span id="action-client-sidebar-toggle" data-entry="Toggle Sidebar"></span>Toggle Sidebar | `client.sidebar.toggle` <span hidden>sidebar collapse expand fold hide show tabs</span> |  <span hidden>Default shortcut:</span>`cmd+/` |
| <span id="action-client-sidebar-activity-toggle" data-entry="Toggle Activity"></span>Toggle Activity | `client.sidebar.activity.toggle` <span hidden>activity mode view filter working waiting attention programs status sidebar tabs</span> |  <span hidden>Default shortcut:</span>`cmd+.` |
| <span id="action-client-sidebar-activity-by-session" data-entry="Show Activity by Session"></span>Show Activity by Session | `client.sidebar.activity.by_session` <span hidden>activity mode view filter group grouping by session sessions sidebar tabs</span> |  |
| <span id="action-client-sidebar-activity-by-priority" data-entry="Show Activity by Priority"></span>Show Activity by Priority | `client.sidebar.activity.by_priority` <span hidden>activity mode view filter group grouping by priority blocked failed done working idle sidebar tabs</span> |  |
| <span id="action-client-sidebar-filter-focus" data-entry="Focus Tab Filter"></span>Focus Tab Filter | `client.sidebar.filter.focus` <span hidden>filter search find type query focus keyboard sidebar activity tabs sessions hosts</span> |  |
| <span id="action-client-tab-style-toggle" data-entry="Toggle Toolbar Layout"></span>Toggle Toolbar Layout | `client.tab_style.toggle` <span hidden>toolbar layout tab style orientation sidebar horizontal vertical view</span> |  <span hidden>Default shortcut:</span>`shift+cmd+s` |
| <span id="action-client-toolbar-reveal" data-entry="Reveal Toolbar"></span>Reveal Toolbar | `client.toolbar.reveal` <span hidden>show reveal peek toolbar tab bar title row hover auto-hide hidden view</span> |  <span hidden>Default shortcut:</span>`alt+cmd+t` |
| <span id="action-client-pane-header-reveal" data-entry="Reveal Pane Header"></span>Reveal Pane Header | `client.pane_header.reveal` <span hidden>show reveal peek pane header title hover hidden focused pane view</span> |  <span hidden>Default shortcut:</span>`alt+shift+cmd+t` |
| <span id="action-client-theme-change" data-entry="Change Theme…"></span>Change Theme… | `client.theme.change` <span hidden>theme colour color scheme palette appearance</span> |  |
| <span id="action-client-theme-import" data-entry="Import Theme File"></span>Import Theme File Imports a Ghostty theme or iTerm color file and selects it. | `client.theme.import` <span hidden>theme import ghostty iterm file colour color scheme palette</span> |  |
| <span id="action-client-appearance-switch-mode" data-entry="Switch Appearance Mode"></span>Switch Appearance Mode | `client.appearance.switch_mode` <span hidden>appearance mode light dark toggle color scheme</span> |  |
| <span id="action-client-config-copy-path" data-entry="Copy Config File Path"></span>Copy Config File Path | `client.config.copy_path` <span hidden>config configuration file path copy clipboard init.lua lua keybindings shortcuts settings location where</span> |  |
| <span id="action-client-config-reload" data-entry="Reload Config"></span>Reload Config | `client.config.reload` <span hidden>config configuration reload reread apply init.lua lua keybindings shortcuts settings</span> |  |

---
title: "Mac Settings"
canonical_url: https://www.superlogical.com/rex/docs/reference/mac-settings
---

# Mac Settings

This page lists every setting in Rex for Mac and where to find it. Anything tagged [Bleeding Edge](https://www.superlogical.com/rex/docs/reference/mac-settings.md#setting-app-version-update-channel) is on that update channel and not yet in Preview.

<div class="reference-page settings" hidden></div>

## Integrations

### Coding Agents

Teach Claude Code, Codex, OpenCode and Pi how to use Rex. In Settings > Integrations > Coding Agents. <span hidden>agents, ai, claude, claude code, codex, opencode, pi, skill, hooks, `rex skill setup`, llm, assistant, integrations</span>

| Setting | Description |
| --- | --- |
| <span id="setting-coding-agents-integrate-with-available-agents" data-entry="Integrate with available agents"></span>Integrate with available agents | Installs a Rex skill and session hooks for supported agents. <span hidden>In Settings &gt; Integrations &gt; Coding Agents.</span> <span hidden>install skill, remove skill, set up agents, uninstall, enable agents, disable agents, update skill</span> |

### Agent Behavior

Choose what Rex tells coding agents running in its terminals. In Settings > Integrations > Agent Behavior. <span hidden>agent instructions, agent rules, session hook</span>

| Setting | Description |
| --- | --- |
| <span id="setting-agent-behavior-let-agents-name-sessions" data-entry="Let agents name sessions"></span>Let agents name sessions | Let agents label the session after the task they're working on. Shown while Integrate with available agents is on. <span hidden>In Settings &gt; Integrations &gt; Agent Behavior.</span> <span hidden>rename session, session label, session name, auto name, title, label sessions</span> |

## Notifications

### In-App Notifications

Choose where floating notifications appear in the window. In Settings > Notifications > In-App Notifications. <span hidden>notifications, notification settings, alerts, banners, notification center, in app notifications, customize notifications</span>

| Setting | Description |
| --- | --- |
| <span id="setting-in-app-notifications-notification-position" data-entry="Notification position"></span>Notification position | Click a position in the window diagram to place notifications at the top or bottom, aligned left, center, or right. <span hidden>In Settings &gt; Notifications &gt; In-App Notifications.</span> <span hidden>notification position, notification placement, banner location, anchor, top left, top center, top right, bottom left, bottom center, bottom right</span> |

### Program Activity

Independently configure tab indicators, system notifications, Dock badges, and sounds for program activity. In Settings > Notifications > Program Activity. <span hidden>program activity, tab activity, activity indicators</span>

| Setting | Description |
| --- | --- |
| <span id="setting-program-activity-show-activity-in-tabs" data-entry="Show activity in tabs"></span>Show activity in tabs | Show working dots, progress pies, and needs-attention dots and messages on tabs when a program in them reports its status. <span hidden>In Settings &gt; Notifications &gt; Program Activity.</span> <span hidden>program status, process status, osc 7501, agent, claude, progress, progress pie, blocked, waiting, question, permission, error, failed, status dot, status badges, notification dot, badge, working, tab status, indicator, needs attention</span> |
| <span id="setting-program-activity-include-idle-processes-in-activity" data-entry="Include idle processes in activity"></span>Include idle processes in activity | Keep tabs whose programs report that they're idle in the Activity filter and popup, even after you've seen them. <span hidden>In Settings &gt; Notifications &gt; Program Activity.</span> <span hidden>idle, idle processes, idle agents, ready, activity filter, activity popup, keep idle, show idle, include idle, always show idle processes in activity, eye</span> |
| <span id="setting-program-activity-show-each-process-in-tabs" data-entry="Show each process in tabs"></span>Show each process in tabs | Split a tab with more than one active pane into a row for each pane in the Activity filter and popup. Clicking a row focuses its pane. <span hidden>In Settings &gt; Notifications &gt; Program Activity.</span> <span hidden>each process, each process in activity, every process, all processes, split processes, split tabs, panes, pane rows, agents, multiple agents, expand tabs, activity filter, activity popup, eye</span> |
| <span id="setting-program-activity-show-system-notifications" data-entry="Show system notifications"></span>Show system notifications | Send a macOS notification when a program reports a status while Rex is in the background, independently of tab indicators. Clicking it opens the program's tab. <span hidden>In Settings &gt; Notifications &gt; Program Activity.</span> <span hidden>program notifications, agent notifications, background notifications, claude notification, notify when done, notify when blocked, notifications without tab indicators</span> |
| <span id="setting-program-activity-badge-dock-icon" data-entry="Badge Dock icon"></span>Badge Dock icon | Show on Rex's Dock icon how many tabs have a program that's blocked, failed, or finished that you haven't seen. <span hidden>In Settings &gt; Notifications &gt; Program Activity.</span> <span hidden>dock badge, badge count, app icon badge, unread count, red badge, badge application icon, dock icon</span> |
| <span id="setting-program-activity-play-sounds" data-entry="Play sounds"></span>Play sounds | Play a sound when a program in a tab is blocked, reports an error, or finishes. <span hidden>In Settings &gt; Notifications &gt; Program Activity.</span> <span hidden>sound, audio, chime, alert sound, beep, bell, audible, monoh, sound effects, program finished, done sound, mute, silence</span> |
| <span id="setting-program-activity-sound-volume" data-entry="Sound volume"></span>Sound volume | Set how loud the blocked, error, and done sounds play, from quieter to louder than the default. <span hidden>In Settings &gt; Notifications &gt; Program Activity.</span> <span hidden>volume, loudness, louder, quieter, too quiet, too loud, sound level, alert volume, notification volume, boost, gain</span> |
| <span id="setting-program-activity-working" data-entry="Working"></span>Working | Enable or disable all activity for working programs, or configure their tab indicators, notifications, Dock badges, and sounds individually. <span hidden>In Settings &gt; Notifications &gt; Program Activity.</span> <span hidden>working status, busy, progress, spinner, running, disable all working, enable working status</span> |
| <span id="setting-program-activity-blocked" data-entry="Blocked"></span>Blocked | Enable or disable all activity for programs waiting for you, or configure their tab indicators, notifications, Dock badges, and sounds individually. <span hidden>In Settings &gt; Notifications &gt; Program Activity.</span> <span hidden>blocked status, waiting, question, permission, approval, sign in, attention sound, blocked notifications, disable all blocked, enable blocked status</span> |
| <span id="setting-program-activity-error" data-entry="Error"></span>Error | Enable or disable all activity for programs reporting errors, or configure their tab indicators, notifications, Dock badges, and sounds individually. <span hidden>In Settings &gt; Notifications &gt; Program Activity.</span> <span hidden>error status, failed, failure, error sound, disable all errors, enable error status</span> |
| <span id="setting-program-activity-done" data-entry="Done"></span>Done | Enable or disable all activity for finished programs, or configure their tab indicators, notifications, Dock badges, and sounds individually. <span hidden>In Settings &gt; Notifications &gt; Program Activity.</span> <span hidden>done status, finished, complete, completed, success sound, finish sound, disable all done, enable done status</span> |

### System Notifications

Choose whether terminal programs can send native macOS notifications, and see the ones Rex sends for Tailscale. In Settings > Notifications > System Notifications. <span hidden>native notifications, macOS notifications, mac notifications, tailscale alerts, terminal notifications</span>

| Setting | Description |
| --- | --- |
| <span id="setting-system-notifications-terminal-notifications" data-entry="Terminal notifications"></span>Terminal notifications | Let programs send native notifications with OSC 9 or OSC 777. <span hidden>In Settings &gt; Notifications &gt; System Notifications.</span> <span hidden>osc 9, osc 777, osc9, osc777, desktop notification, notify-send, escape sequence, program notifications, printf notify</span> |
| <span id="setting-system-notifications-tailscale-sign-in" data-entry="Tailscale sign-in"></span>Tailscale sign-in | A native notification when Rex needs you to sign in to Tailscale. <span hidden>In Settings &gt; Notifications &gt; System Notifications.</span> <span hidden>tailscale login notification, sign in, authentication, account</span> |
| <span id="setting-system-notifications-tailscale-device-approval" data-entry="Tailscale device approval"></span>Tailscale device approval | A native notification when a tailnet administrator must approve this Mac. <span hidden>In Settings &gt; Notifications &gt; System Notifications.</span> <span hidden>device approval notification, approve mac, tailnet administrator, authorization</span> |
| <span id="setting-system-notifications-tailscale-failures" data-entry="Tailscale failures"></span>Tailscale failures | A native notification when Tailscale stops or reports an error. <span hidden>In Settings &gt; Notifications &gt; System Notifications.</span> <span hidden>tailscale failure notification, tailscale stopped, connection error, offline</span> |

## Server

### Rex Server

Manage the local server, compatibility, and diagnostics. In Settings > Server > Rex Server. <span hidden>start, stop, restart, status, version, api, diagnostics</span>

| Setting | Description |
| --- | --- |
| <span id="setting-rex-server-server-status" data-entry="Server status"></span>Server status | See whether the local server is running and for how long, with version and API details in its info popover. <span hidden>In Settings &gt; Server &gt; Rex Server.</span> <span hidden>health, running, stopped, preparing server, failed, error, source checkout, server details, server version, api revision, uptime, running time, how long, started, duration</span> |
| <span id="setting-rex-server-compatibility" data-entry="Compatibility"></span>Compatibility | When an update, compatibility issue, or pending settings change is detected, see the discrepancy and update or restart the server when appropriate. <span hidden>In Settings &gt; Server &gt; Rex Server.</span> <span hidden>incompatible server, restart recommended, update rex app, restart server, upgrade server, newer server, available version, restart required, new features, apply settings, use bundled server, older app, backward compatible, a newer server is available, updating is optional, optional update, new API features are available, newer API features, unavailable features, server settings have changed, cannot connect to this server, this server requires a newer rex app, server and app are incompatible, reconnect, server compatibility, pending changes, update server, restart using included server, restart from source</span> |

### Remote Address

Check serving progress, copy the address, or pair another device. In Settings > Server > Remote Address. <span hidden>tailnet, https, endpoint, connectivity</span>

| Setting | Description |
| --- | --- |
| <span id="setting-remote-address-remote-address" data-entry="Remote Address"></span>Remote Address | Check serving progress and certificate readiness, retry, copy the address, or pair a device using its QR code. <span hidden>In Settings &gt; Server &gt; Remote Address.</span> <span hidden>copy remote address, tailscale connection progress, https, dns, hostname, endpoint, copy, qr, scan, pair, device, iphone, ipad, certificate, waiting for certificate, tls, ssl, secure, starting, stopping, connection progress, retry, connected, disconnected, approval, error, dns recovery</span> |

### Tailscale

Connect Rex to your tailnet and control who can access it. In Settings > Server > Tailscale. <span hidden>network, vpn, remote, secure, sharing</span>

| Setting | Description |
| --- | --- |
| <span id="setting-tailscale-serve-over-tailscale" data-entry="Serve over Tailscale"></span>Serve over Tailscale | Enable or disable access to Rex over your tailnet. <span hidden>In Settings &gt; Server &gt; Tailscale.</span> <span hidden>connect, network, vpn, share, enable, disable</span> |
| <span id="setting-tailscale-preferred-hostname" data-entry="Preferred Hostname"></span>Preferred Hostname | Choose this Mac’s Tailscale hostname while serving. The assigned address may differ. <span hidden>In Settings &gt; Server &gt; Tailscale.</span> <span hidden>hostname, device name, dns, rename, machine, address, assigned address, host id, durable host id, identity</span> |
| <span id="setting-tailscale-limit-access-to-your-tailscale-account" data-entry="Limit access to your Tailscale account"></span>Limit access to your Tailscale account | Restrict callers to your account or allow tailnet members. <span hidden>In Settings &gt; Server &gt; Tailscale.</span> <span hidden>security, authentication, identity, permissions, owner, members, acl</span> |
| <span id="setting-tailscale-tags" data-entry="Tags"></span>Tags | See this device’s Tailscale tags. Sign out and back in to change them. <span hidden>In Settings &gt; Server &gt; Tailscale.</span> <span hidden>tag, tagged, acl tags, advertise tags, tailnet policy, machine identity, owner, access</span> |
| <span id="setting-tailscale-control-server" data-entry="Control Server"></span>Control Server | See which control server this device signs in with. Sign out and back in to change it. <span hidden>In Settings &gt; Server &gt; Tailscale.</span> <span hidden>control url, control plane, coordination server, login server, self-hosted, custom server</span> |
| <span id="setting-tailscale-sign-in-to-tailscale" data-entry="Sign in to Tailscale"></span>Sign in to Tailscale | Check your account and sign-out progress, configure a hostname and sign in, reopen or cancel enrollment, or sign out. <span hidden>In Settings &gt; Server &gt; Tailscale.</span> <span hidden>login, tailscale login, sign in, sign in to tailscale, log in, sign out, account, status, signed in to tailscale, enrolled account, signing in to tailscale, browser sign in, open browser, reopen browser, preparing to sign in, this may take a moment, ready to connect, awaiting connection, finish the process in your browser, taking longer than usual, slow sign in, cancel sign in, cancel login, signing out of tailscale, remove saved login, sign out progress</span> |

### Client Info

Choose how this Mac appears to Rex servers and the command line. In Settings > Server > Client Info. <span hidden>client, device, identity, `rex client ls`, command line, cli</span>

| Setting | Description |
| --- | --- |
| <span id="setting-client-info-label" data-entry="Label"></span>Label | Name this Mac in client lists and for targeting it from rex commands. <span hidden>In Settings &gt; Server &gt; Client Info.</span> <span hidden>device label, client label, name, computer, identify, rename, rename this mac, this device, `-C`, target, targeting, client ls</span> |
| <span id="setting-client-info-remote-control" data-entry="Remote Control"></span>Remote Control | Let Rex and other clients perform actions in this app. <span hidden>In Settings &gt; Server &gt; Client Info.</span> <span hidden>remote, control, allow, allow actions, actions, command line, `rex do`, perform, security</span> |

## Interface

### Appearance

Choose the color scheme, interface materials, and theme tint. In Settings > Interface > Appearance. <span hidden>look, visual, display, theme</span>

| Setting | Description |
| --- | --- |
| <span id="setting-appearance-color-scheme" data-entry="Color scheme"></span>Color scheme | Select a live Auto, Light, or Dark material preview. Auto follows the system appearance. <span hidden>In Settings &gt; Interface &gt; Appearance.</span> <span hidden>colour, light, dark, auto, automatic, system, night mode, cards</span> |
| <span id="setting-appearance-interface-style" data-entry="Interface style"></span>Interface style | Preview Glass or Classic materials with the current theme tint and select a surface style. <span hidden>In Settings &gt; Interface &gt; Appearance.</span> <span hidden>glass, classic, liquid glass, material, native, system, modern, opaque, translucent, cards</span> |
| <span id="setting-appearance-tint-with-theme" data-entry="Tint with theme"></span>Tint with theme | Apply the selected theme's colors to interface surfaces. <span hidden>In Settings &gt; Interface &gt; Appearance.</span> <span hidden>tint, theme colors, theme colours, palette, themed, blended, blend theme, use theme</span> |
| <span id="setting-appearance-use-glass-for-panes" data-entry="Use glass for panes"></span>Use glass for panes | Available with the Glass interface style: add glass to panes alongside window and tab materials. <span hidden>In Settings &gt; Interface &gt; Appearance.</span> <span hidden>pane glass, no glass, remove glass, liquid glass, pane material, transparency</span> |

### Toolbar

Choose the toolbar layout and whether the toolbar always shows or shows on hover. In Settings > Interface > Toolbar. <span hidden>workspace, chrome, layout, navigation, tab bar, title bar</span>

| Setting | Description |
| --- | --- |
| <span id="setting-toolbar-toolbar-layout" data-entry="Toolbar layout"></span>Toolbar layout | Place the toolbar and its tabs Above, on the Left Side, or on the Right Side of the window. <span hidden>In Settings &gt; Interface &gt; Toolbar.</span> <span hidden>tab style, tab placement, tab layout, horizontal tabs, vertical tabs, vertical, horizontal, sidebar, navigation, orientation, sidebar position, above, left side, right side, left sidebar, right sidebar, edge, location</span> |
| <span id="setting-toolbar-toolbar-behavior" data-entry="Toolbar behavior"></span>Toolbar behavior | Always show the toolbar, Show on hover: hide the tab bar, or the sidebar's title row, until the pointer reaches the top of the window, or keep it Hidden until Reveal Toolbar. <span hidden>In Settings &gt; Interface &gt; Toolbar.</span> <span hidden>always show, on hover, show toolbar on hover, hidden, never, never show toolbar, reveal toolbar, toolbar visibility, auto-hide toolbar, autohide, hide toolbar, hide tab bar, floating toolbar, reveal, tab bar, title bar, title row, traffic lights, distraction free, minimal, full height, chrome, hot corner, hot corners</span> |

### Tab behavior

Choose whether tabs above the panes fill the tab bar. Opens from Customize tabs under Toolbar. In Settings > Interface > Tab behavior. <span hidden>tabs, tab bar, tab sizing, customize tabs</span>

| Setting | Description |
| --- | --- |
| <span id="setting-tab-behavior-tabs-fill-available-space" data-entry="Tabs fill available space"></span>Tabs fill available space | Available with horizontal tabs: expand tabs equally to fill the bar, showing backgrounds only for two or more tabs. <span hidden>In Settings &gt; Interface &gt; Tab behavior.</span> <span hidden>fill available space, tab width, stretch, expand, full width, equal width, maximum, capped</span> |

### Tab icons

Choose how many pane icons tabs show, and how tab and shell icons are colored. Opens from Customize tabs under Toolbar. In Settings > Interface > Tab icons. <span hidden>workspace, icons, symbols, pane cards, icon stack, tab icons, tabs, customize tabs</span>

| Setting | Description |
| --- | --- |
| <span id="setting-tab-icons-use-color-for-tab-icons" data-entry="Use color for tab icons"></span>Use color for tab icons | Show colored icons Always, For active panes, For active tabs, or Never. <span hidden>In Settings &gt; Interface &gt; Tab icons.</span> <span hidden>colour, monochrome, grayscale, greyscale, saturation, always, never, active panes, active tabs, focused pane, selected tab</span> |
| <span id="setting-tab-icons-shell-icon-theme" data-entry="Shell icon theme"></span>Shell icon theme | Choose Default artwork, Use theme for background and foreground colors, or Use theme accent for accents. <span hidden>In Settings &gt; Interface &gt; Tab icons.</span> <span hidden>shell colors, shell colours, terminal icon, shell palette, default, use theme, use theme accent, accent, background, foreground, glyph</span> |
| <span id="setting-tab-icons-show-tab-icons" data-entry="Show tab icons"></span>Show tab icons | Show zero to three pane icons in vertical tabs, or up to five in horizontal tabs. <span hidden>In Settings &gt; Interface &gt; Tab icons.</span> <span hidden>icon count, icons per tab, number of icons, stack size, stack limit, icon count slider, slider, maximum, none, hide icons, no icons, zero icons, one icon, two icons, three icons, four icons, five icons, 0, 1, 2, 3, 4, 5, pane icons</span> |

### Panes

Adjust pane spacing and headers. In Settings > Interface > Panes. <span hidden>workspace, chrome, layout, density, spacing, split</span>

| Setting | Description |
| --- | --- |
| <span id="setting-panes-pane-style" data-entry="Pane style"></span>Pane style | Choose Comfortable or Compact pane surfaces and spacing. <span hidden>In Settings &gt; Interface &gt; Panes.</span> <span hidden>compact, comfortable, pane density, padding, gaps, spacing</span> |
| <span id="setting-panes-pane-header-behavior" data-entry="Pane header behavior"></span>Pane header behavior | Show the title and controls above each pane: Always show, Show on hover, or Hidden. <span hidden>In Settings &gt; Interface &gt; Panes.</span> <span hidden>pane headers, show pane headers, titles, toolbar, chrome, hide headers, always, always show, on hover, show on hover, hidden, never, hover, floating header, auto-hide header, pane header visibility</span> |

### Window

Available with sidebar toolbar layouts: choose whether the sidebar resizes the window, which swipe shows or hides it, and the size of its text. In Settings > Interface > Window. <span hidden>window sizing, window width, sidebar, layout, swipe, trackpad, text size</span>

| Setting | Description |
| --- | --- |
| <span id="setting-window-sidebar-text-size" data-entry="Sidebar text size"></span>Sidebar text size | Available with vertical tabs: set the size of session and tab titles in the sidebar, or follow the Sidebar icon size in System Settings. <span hidden>In Settings &gt; Interface &gt; Window.</span> <span hidden>sidebar text, text size, tab title size, session title size, larger text, bigger text, sidebar icon size, sidebar row size, row height, follow system size</span> |
| <span id="setting-window-preserve-width-when-resizing-sidebar" data-entry="Preserve width when resizing sidebar"></span>Preserve width when resizing sidebar | Available with vertical tabs: resize panes when showing or hiding the sidebar, keeping the window width unchanged. <span hidden>In Settings &gt; Interface &gt; Window.</span> <span hidden>keep window width, preserve window width, fixed window width, resizing sidebar, window size, resize window, shrink window, grow window, keep window size, show sidebar, hide sidebar, toggle sidebar, collapse sidebar, fold sidebar, pane width, maximized, full screen</span> |
| <span id="setting-window-use-a-gesture-to-toggle-the-sidebar" data-entry="Use a gesture to toggle the sidebar"></span>Use a gesture to toggle the sidebar | Available with vertical tabs: choose whether a swipe or an Option-swipe shows or hides the sidebar, or turn the gesture off. Hold the ⌥ key to scroll content sideways instead. <span hidden>In Settings &gt; Interface &gt; Window.</span> <span hidden>show or hide sidebar, option-swipe, option swipe, ⌥, off, disable swipe, turn off swipe, no gesture, disable gesture, magic mouse, mouse, one finger, option scroll, alt scroll, option key, alt key, hold option, modifier, swipe, two finger swipe, two fingers, trackpad, gesture, trackpad gesture, sidebar swipe, swipe sidebar, fold sidebar, horizontal scroll, scroll sideways, sideways, side scroll, scroll left, scroll right, tui, diff, long lines</span> |

### Padding

Set the space between a pane's edges and its terminal text, and whether a program's colors fill it. Opens from Customize panes under Interface › Panes. In Settings > Interface > Padding. <span hidden>pane padding, terminal padding, inset, margin, margins, gutter, spacing around text, edges, window padding, pane background, background</span>

| Setting | Description |
| --- | --- |
| <span id="setting-padding-padding-x" data-entry="Padding X"></span>Padding X | Set the space to the left and right of the terminal text. <span hidden>In Settings &gt; Interface &gt; Padding.</span> <span hidden>horizontal padding, x padding, left padding, right padding, side padding, horizontal inset, horizontal margin</span> |
| <span id="setting-padding-padding-y" data-entry="Padding Y"></span>Padding Y | Set the space below the terminal text, and above it when pane headers are hidden. <span hidden>In Settings &gt; Interface &gt; Padding.</span> <span hidden>vertical padding, y padding, bottom padding, top padding, vertical inset, vertical margin</span> |
| <span id="setting-padding-fill-with-program-colors" data-entry="Fill with program colors"></span>Fill with program colors | Let a full-screen program color the padding Off, Automatic, or Always. <span hidden>In Settings &gt; Interface &gt; Padding.</span> <span hidden>padding color, colour, off, automatic, always, extend, extend always, background, edge cells, window-padding-color, program background, editor, full screen, tui</span> |
| <span id="setting-padding-include-pane-header" data-entry="Include pane header"></span>Include pane header | Color the pane header to match the program too, while the padding fills. <span hidden>In Settings &gt; Interface &gt; Padding.</span> <span hidden>match program backgrounds, tint, pane color, header color, pane background, terminal background, program background, editor, full screen, tui, neovim, vim, blend, match terminal</span> |

### Active Pane

Highlight the edges of the active pane and fade inactive ones. Opens from Customize panes under Interface › Panes. In Settings > Interface > Active Pane. <span hidden>customize panes, customize pane behavior, pane behavior, active pane, active panes, inactive panes, focused pane, unfocused panes, pane focus, focus appearance, dim inactive</span>

| Setting | Description |
| --- | --- |
| <span id="setting-active-pane-highlight-active-pane-edges" data-entry="Highlight active pane edges"></span>Highlight active pane edges | Highlight the edges of the focused pane, following focus. <span hidden>In Settings &gt; Interface &gt; Active Pane.</span> <span hidden>border, outline, focus ring, active border, focused pane, highlight, highlight active pane, edge highlight, indicator, focus indicator, show border on active pane</span> |
| <span id="setting-active-pane-highlight-color" data-entry="Highlight color"></span>Highlight color | Draw the highlight in the accent, a terminal palette color, or the terminal text color. <span hidden>In Settings &gt; Interface &gt; Active Pane.</span> <span hidden>accent, accent color, theme color, terminal text, terminal foreground, foreground color, text color, highlight color, border color, focus ring color, active pane border color, swatch, color swatches, palette, red, blue, green, yellow, cyan, magenta</span> |
| <span id="setting-active-pane-highlight-opacity" data-entry="Highlight opacity"></span>Highlight opacity | Set how strongly the highlight draws. <span hidden>In Settings &gt; Interface &gt; Active Pane.</span> <span hidden>opacity, transparency, alpha, translucent, faint, subtle, strength, border opacity, border strength, focus ring opacity, highlight strength, active pane border opacity</span> |
| <span id="setting-active-pane-animate-active-pane-highlight" data-entry="Animate active pane highlight"></span>Animate active pane highlight | Fade the highlight to the newly focused pane, move it there, or switch it at once. <span hidden>In Settings &gt; Interface &gt; Active Pane.</span> <span hidden>fade, move, crossfade, glide, slide, animate, animation, transition, border motion, focus animation, highlight animation, border fade, border move, off, instant, no animation, active pane border animation</span> |
| <span id="setting-active-pane-fade-inactive-panes" data-entry="Fade inactive panes"></span>Fade inactive panes | Fade unfocused pane surfaces, content, both, or neither. <span hidden>In Settings &gt; Interface &gt; Active Pane.</span> <span hidden>dim, opacity, unfocused, focus, inactive, off, none, surface, surface only, content, content only, surface &amp; content, surface and content, both, neither, decorations, styling, text, background, borders, shadows, glass, fade inactive pane content</span> |
| <span id="setting-active-pane-fade-panes-for-inactive-windows" data-entry="Fade panes for inactive windows"></span>Fade panes for inactive windows | Fade inactive windows while respecting the pane fade choice. Glass content fades equally in every pane. <span hidden>In Settings &gt; Interface &gt; Active Pane.</span> <span hidden>window inactive, window focus, background window, switch apps, window deactivation, standard window styles, theme defaults, all panes, fade panes in inactive windows, use pane fade choice, fade content for inactive windows, liquid glass content</span> |

### Splits &amp; Focus

Balance a tab's splits and let focus follow the pointer. Opens from Customize panes under Interface › Panes. In Settings > Interface > Splits &amp; Focus. <span hidden>splits, split panes, split balancing, pointer focus, pane layout</span>

| Setting | Description |
| --- | --- |
| <span id="setting-splits-focus-autobalance-splits" data-entry="Autobalance splits"></span>Autobalance splits | Evenly resize a tab's panes when one opens or closes, keeping dividers you drag. <span hidden>In Settings &gt; Interface &gt; Splits &amp;amp; Focus.</span> <span hidden>balance panes, equalize, even, equal size, new pane, close pane, split, resize, divider, keep divider, dragged divider, manual split size</span> |
| <span id="setting-splits-focus-focus-follows-mouse" data-entry="Focus follows mouse"></span>Focus follows mouse | Focus a pane by moving the pointer over it, without clicking. <span hidden>In Settings &gt; Interface &gt; Splits &amp;amp; Focus.</span> <span hidden>hover, pointer, cursor, sloppy focus, mouse focus, active pane, select pane, without clicking</span> |

### Gestures

Turn trackpad gestures for panes and the sidebar on or off. In Settings > Interface > Gestures. <span hidden>gestures, trackpad, multi-touch, fingers</span>

| Setting | Description |
| --- | --- |
| <span id="setting-gestures-use-trackpad-gestures" data-entry="Use trackpad gestures"></span>Use trackpad gestures | Spread two fingers over a pane to zoom it and pinch them to restore it, and swipe to show or hide the sidebar. <span hidden>In Settings &gt; Interface &gt; Gestures.</span> <span hidden>use gestures, pinch, pinch to zoom, pinch zoom, magnify, magnification, spread, pinch zoom pane, unzoom, trackpad zoom, trackpad pinch, two finger pinch, swipe, sidebar swipe, disable gestures, turn off gestures, no gestures, trackpad gestures</span> |

### Animation

Choose how panes move, open, and close. In Settings > Interface > Animation. <span hidden>motion, animations, transitions, movement</span>

| Setting | Description |
| --- | --- |
| <span id="setting-animation-animation-style" data-entry="Animation style"></span>Animation style | Choose Snappy, Natural, or Off for pane motion, the active border, and showing or hiding the sidebar. Off while Reduce Motion is on. <span hidden>In Settings &gt; Interface &gt; Animation.</span> <span hidden>snappy, natural, off, spring, bounce, no bounce, overshoot, disable animations, turn off animations, reduce motion, instant, fast, speed, pane animation, split animation, hyprland, sidebar animation</span> |

### App Icon

Customize Rex's app icon and Dock and menu bar icon visibility. In Settings > Interface > App Icon. <span hidden>app icon, application icon, dock icon, artwork, beta icon</span>

| Setting | Description |
| --- | --- |
| <span id="setting-app-icon-app-icon" data-entry="App Icon"></span>App Icon | Choose Default — Dynamic, Bright, or Twilight artwork for the app icon. <span hidden>In Settings &gt; Interface &gt; App Icon.</span> <span hidden>app icon, application icon, dock icon, custom icon, alternate icon, bright, bright icon, twilight, twilight icon, light icon, dark icon, beta, artwork, dynamic, dynamic icon</span> |
| <span id="setting-app-icon-show-app-icon" data-entry="Show app icon"></span>Show app icon | Show Rex's icon in the Dock and menu bar, the Dock only, or the menu bar only while no window is open. <span hidden>In Settings &gt; Interface &gt; App Icon.</span> <span hidden>dock and menu bar, dock only, menubar only, menu bar only, dock icon, hide dock icon, show dock icon, dock visibility, application switcher, app switcher, command tab, cmd tab, menubar, menu bar icon, status item, status bar, tray icon, hide menu bar icon, show menu bar icon, hide, show, visibility</span> |

### Launch

Choose whether Rex opens workspace windows at startup. In Settings > Interface > Launch. <span hidden>launch, startup, start up, opening, background</span>

| Setting | Description |
| --- | --- |
| <span id="setting-launch-show-window-on-launch" data-entry="Show window on launch"></span>Show window on launch | Automatically open workspace windows when Rex launches. <span hidden>In Settings &gt; Interface &gt; Launch.</span> <span hidden>show window on launch, open window on startup, launch in background, hide window on launch, restore windows, start minimized</span> |

## Themes

### Light Mode

Select the theme palette used in light appearance. In Settings > Themes > Light Mode. <span hidden>colors, colours, appearance, theme, palette, day, bright</span>

| Setting | Description |
| --- | --- |
| <span id="setting-light-mode-light-theme" data-entry="Light theme"></span>Light theme | Choose a custom, default, or built-in palette used in light mode. <span hidden>In Settings &gt; Themes &gt; Light Mode.</span> <span hidden>day, bright, light colors, colour scheme, new light theme, edit light theme, customize light theme, duplicate light theme, fork light theme, delete light theme, theme customizer, ansi palette, ansi standard, ansi bright, terminal colors, preview, live preview, live update, pause preview, freeze, theme editor</span> |
| <span id="setting-light-mode-use-extended-theme-palette" data-entry="Use extended theme palette"></span>Use extended theme palette | Use the theme’s colors 16–255 in light mode, or xterm’s standard palette when off. <span hidden>In Settings &gt; Themes &gt; Light Mode.</span> <span hidden>extended palette, extended colors, extended colours, xterm, xterm palette, standard palette, color cube, colour cube, grayscale ramp, greyscale ramp, generated palette, palette-generate, compatibility, on, off</span> |

### Dark Mode

Select the theme palette used in dark appearance. In Settings > Themes > Dark Mode. <span hidden>colors, colours, appearance, theme, palette, night, dim</span>

| Setting | Description |
| --- | --- |
| <span id="setting-dark-mode-dark-theme" data-entry="Dark theme"></span>Dark theme | Choose a custom, default, or built-in palette used in dark mode. <span hidden>In Settings &gt; Themes &gt; Dark Mode.</span> <span hidden>night, dim, dark colors, colour scheme, new dark theme, edit dark theme, customize dark theme, duplicate dark theme, fork dark theme, delete dark theme, theme customizer, ansi palette, ansi standard, ansi bright, terminal colors, preview, live preview, live update, pause preview, freeze, theme editor</span> |
| <span id="setting-dark-mode-use-extended-theme-palette" data-entry="Use extended theme palette"></span>Use extended theme palette | Use the theme’s colors 16–255 in dark mode, or xterm’s standard palette when off. <span hidden>In Settings &gt; Themes &gt; Dark Mode.</span> <span hidden>extended palette, extended colors, extended colours, xterm, xterm palette, standard palette, color cube, colour cube, grayscale ramp, greyscale ramp, generated palette, palette-generate, compatibility, on, off</span> |

### Import Theme

Add a Ghostty or iTerm palette to your custom themes. In Settings > Themes > Import Theme. <span hidden>import, file, ghostty, iterm, itermcolors, palette, load, colors, colours, appearance, background, light, dark</span>

## Terminal

### Typography

Preview and choose the terminal font, style, size, weight, and line height. In Settings > Terminal > Typography. <span hidden>text, typeface, readability, typography preview, font preview</span>

| Setting | Description |
| --- | --- |
| <span id="setting-typography-terminal-font" data-entry="Terminal font"></span>Terminal font | Browse monospaced or all installed fonts to choose the terminal typeface. <span hidden>In Settings &gt; Terminal &gt; Typography.</span> <span hidden>font, family, typeface, monospace, menlo, font picker, monospaced fonts, all fonts</span> |
| <span id="setting-typography-font-size" data-entry="Font size"></span>Font size | Adjust terminal text size in points. <span hidden>In Settings &gt; Terminal &gt; Typography.</span> <span hidden>text size, larger, smaller, zoom, pt</span> |
| <span id="setting-typography-font-style" data-entry="Font style"></span>Font style | Choose an installed face of the terminal font, such as Light or Book. <span hidden>In Settings &gt; Terminal &gt; Typography.</span> <span hidden>face, variant, italic, oblique, regular, light, medium, semibold, book</span> |
| <span id="setting-typography-font-weight" data-entry="Font weight"></span>Font weight | Adjust the stroke weight of a variable terminal font. <span hidden>In Settings &gt; Terminal &gt; Typography.</span> <span hidden>thickness, thin, light, medium, bold, heavy, variable font, wght</span> |
| <span id="setting-typography-line-height" data-entry="Line height"></span>Line height | Add or remove space between terminal lines. <span hidden>In Settings &gt; Terminal &gt; Typography.</span> <span hidden>cell height, line spacing, leading, adjust-cell-height, ghostty, spacing, taller, compact</span> |

### Advanced Font Settings

Adjust character spacing, ligatures, bold and italic, synthesized styles, smoothing, and fallback fonts. Opens from Advanced font settings under Typography. In Settings > Terminal > Advanced Font Settings. <span hidden>advanced font settings, advanced typography, advanced font options, advanced fonts, font rendering, text rendering, font options</span>

| Setting | Description |
| --- | --- |
| <span id="setting-advanced-font-settings-character-spacing" data-entry="Character spacing"></span>Character spacing | Tighten or widen terminal character spacing in pixels or percent. <span hidden>In Settings &gt; Terminal &gt; Advanced Font Settings.</span> <span hidden>letter spacing, kerning, tracking, cell width, adjust-cell-width, ghostty, maple mono, tighter, wider, pixels, percentage</span> |
| <span id="setting-advanced-font-settings-ligatures" data-entry="Ligatures"></span>Ligatures | Join characters like -&gt; and != into single symbols. <span hidden>In Settings &gt; Terminal &gt; Advanced Font Settings.</span> <span hidden>font features, font-feature, calt, liga, arrows, operators, fira code, jetbrains mono</span> |
| <span id="setting-advanced-font-settings-render-bold-and-italic" data-entry="Render bold and italic"></span>Render bold and italic | Draw bold and italic text, only one of them, or neither. <span hidden>In Settings &gt; Terminal &gt; Advanced Font Settings.</span> <span hidden>font-style-bold, font-style-italic, disable bold, no bold, never bold, disable italic, no italic, never italic, upright, bold face, italic face, font weight, regular, both, bold only, italic only, neither, bold, italic, ghostty</span> |
| <span id="setting-advanced-font-settings-force-bold-and-italic-styles" data-entry="Force bold and italic styles"></span>Force bold and italic styles | Use faux bold and italic rendering for fonts without native styles. Shown only for such fonts. <span hidden>In Settings &gt; Terminal &gt; Advanced Font Settings.</span> <span hidden>font-synthetic-style, synthetic style, synthesize, synthesized, pseudo bold, pseudo italic, fake bold, faux bold, fake italic, faux italic, oblique, slant, slanted, outline, emboldened, missing bold, missing italic, pixel font, bitmap font, monaco, ghostty</span> |
| <span id="setting-advanced-font-settings-font-smoothing" data-entry="Font smoothing"></span>Font smoothing | Smooth the edges of terminal text. <span hidden>In Settings &gt; Terminal &gt; Advanced Font Settings.</span> <span hidden>antialiasing, anti aliasing, text rendering, sharp, font-thicken, thicken</span> |
| <span id="setting-advanced-font-settings-fallback-fonts" data-entry="Fallback fonts"></span>Fallback fonts | Choose fonts, in order, for characters the terminal font doesn't have. <span hidden>In Settings &gt; Terminal &gt; Advanced Font Settings.</span> <span hidden>fallback, font fallback, font-family, missing glyphs, missing characters, symbols, icons, nerd font, cjk, chinese, japanese, korean, emoji, font order, ghostty</span> |

### Colors

Control terminal text colors and readability. In Settings > Terminal > Colors. <span hidden>colour, color, text, readability</span>

| Setting | Description |
| --- | --- |
| <span id="setting-colors-improve-text-contrast" data-entry="Improve text contrast"></span>Improve text contrast | Adjust low-contrast text against its background; included when using the theme palette. <span hidden>In Settings &gt; Terminal &gt; Colors.</span> <span hidden>minimum contrast, automatic, legibility, accessibility, unreadable, faint, pale, yellow, ansi, truecolor, theme, on, off</span> |
| <span id="setting-colors-force-theme-palette-for-text" data-entry="Force theme palette for text"></span>Force theme palette for text | Map RGB and extended text colors to the nearest theme color, then correct contrast. <span hidden>In Settings &gt; Terminal &gt; Colors.</span> <span hidden>force theme colors, force theme colours, remap, match colors, palette, ansi, rgb, truecolor, 256 colors, extended colors, replace, consistent, on, off</span> |
| <span id="setting-colors-use-display-p3-color-space" data-entry="Use Display P3 color space"></span>Use Display P3 color space | Read theme and program colors as Display P3, like Ghostty's window-colorspace. <span hidden>In Settings &gt; Terminal &gt; Colors.</span> <span hidden>p3, display p3, wide gamut, wide colour, color space, colour space, colorspace, window-colorspace, ghostty, srgb, saturated, vivid, on, off</span> |

### Cursor

Preview and adjust the terminal cursor shape, blinking, and shader effect. In Settings > Terminal > Cursor. <span hidden>caret, insertion, indicator, cursor preview</span>

| Setting | Description |
| --- | --- |
| <span id="setting-cursor-blink-speed" data-entry="Blink speed"></span>Blink speed | Choose Off, Slow, Normal, Fast, or Custom to edit the blink interval. <span hidden>In Settings &gt; Terminal &gt; Cursor.</span> <span hidden>flashing, steady, slow, fast, normal, off, custom</span> |
| <span id="setting-cursor-blink-interval" data-entry="Blink interval"></span>Blink interval | Set the time between cursor blinks in milliseconds when Blink speed is Custom. <span hidden>In Settings &gt; Terminal &gt; Cursor.</span> <span hidden>speed, duration, frequency, ms, custom</span> |
| <span id="setting-cursor-cursor-style" data-entry="Cursor style"></span>Cursor style | Choose a block, bar, or underline cursor. <span hidden>In Settings &gt; Terminal &gt; Cursor.</span> <span hidden>shape, beam, caret, block, bar, underline</span> |
| <span id="setting-cursor-force-style" data-entry="Force style"></span>Force style | Ignore application requests to change cursor shape. <span hidden>In Settings &gt; Terminal &gt; Cursor.</span> <span hidden>override, application, ignore, shape</span> |
| <span id="setting-cursor-cursor-shaders" data-entry="Cursor shaders"></span>Cursor shaders | Draw a trail shader, such as Warp, Comet, or Blaze, behind terminal cursors. <span hidden>In Settings &gt; Terminal &gt; Cursor.</span> <span hidden>cursor trail, cursor effect, cursor animation, smear, glow, ghostty shader, custom-shader, shadertoy, on, off</span> |
| <span id="setting-cursor-shader" data-entry="Shader"></span>Shader | With Cursor shaders on, choose the effect they draw, such as Warp, Comet, or Blaze. <span hidden>In Settings &gt; Terminal &gt; Cursor.</span> <span hidden>cursor shader, trail style, effect, warp, sweep, comet, blaze, tapered, rainbow, gradient, frozen</span> |

### Keyboard

Show key sequence hints and choose how modifier keys behave in terminal input. In Settings > Terminal > Keyboard. <span hidden>input, keys, shortcuts, modifier</span>

| Setting | Description |
| --- | --- |
| <span id="setting-keyboard-show-key-sequence-hints" data-entry="Show key sequence hints"></span>Show key sequence hints | Show the keys that can come next after the first key of a sequence, such as a tmux-style prefix. <span hidden>In Settings &gt; Terminal &gt; Keyboard.</span> <span hidden>which-key, which key, prefix, leader, chord, key sequence, popup, overlay, pill</span> |
| <span id="setting-keyboard-option-key" data-entry="Option key"></span>Option key | Choose whether the Option key sends Alt or types special characters. <span hidden>In Settings &gt; Terminal &gt; Keyboard.</span> <span hidden>option, alt, meta, esc, escape, emacs, readline, tmux, compose, accents, special characters, left, right, ∫</span> |

### Scrolling

Choose how trackpad and Magic Mouse scrolling move through terminal history. In Settings > Terminal > Scrolling. <span hidden>scroll, scrollback, history, trackpad, touchpad</span>

| Setting | Description |
| --- | --- |
| <span id="setting-scrolling-smooth-scrolling" data-entry="Smooth scrolling"></span>Smooth scrolling | Move through terminal history by pixels instead of whole lines. <span hidden>In Settings &gt; Terminal &gt; Scrolling.</span> <span hidden>smooth, pixel, precise, sub-line, fluid, momentum, inertia, trackpad, line by line, on, off</span> |

### Resizing

Choose how terminal text follows a pane as it changes size. In Settings > Terminal > Resizing. <span hidden>resize, window size, pane size</span>

| Setting | Description |
| --- | --- |
| <span id="setting-resizing-smooth-terminal-resizing" data-entry="Smooth terminal resizing"></span>Smooth terminal resizing | Reflow terminal text on the frame a pane changes size, without waiting for the server. <span hidden>In Settings &gt; Terminal &gt; Resizing.</span> <span hidden>smooth resize, resize, reflow, jitter, glitch, jump, flicker, live resize, resize preview, zoom, split, sidebar, window drag, drag, re-wrap, rewrap, on, off</span> |

### Selection

Preview selection colors and corners, and customize copying with its animation. In Settings > Terminal > Selection. <span hidden>highlight, selected text, copy, colour, animation, selection preview</span>

| Setting | Description |
| --- | --- |
| <span id="setting-selection-copy-on-selection" data-entry="Copy on selection"></span>Copy on selection | Automatically copy selected terminal text to the clipboard. <span hidden>In Settings &gt; Terminal &gt; Selection.</span> <span hidden>automatic copy, clipboard, selection, mouse, on, off</span> |
| <span id="setting-selection-play-animation-on-copy" data-entry="Play animation on copy"></span>Play animation on copy | Shine across the selection to confirm each copy. <span hidden>In Settings &gt; Terminal &gt; Selection.</span> <span hidden>shine, highlight, sheen, glint, pulse, effect, feedback, motion, animate, on, off</span> |
| <span id="setting-selection-selection-foreground" data-entry="Selection foreground"></span>Selection foreground | Choose the text color inside a selection. <span hidden>In Settings &gt; Terminal &gt; Selection.</span> <span hidden>foreground, text color, colour, contrast</span> |
| <span id="setting-selection-selection-background" data-entry="Selection background"></span>Selection background | Choose the fill color behind selected text. <span hidden>In Settings &gt; Terminal &gt; Selection.</span> <span hidden>background, highlight color, colour, fill</span> |
| <span id="setting-selection-rounded-corners" data-entry="Rounded corners"></span>Rounded corners | Round the corners of terminal text selections. <span hidden>In Settings &gt; Terminal &gt; Selection.</span> <span hidden>radius, rounding, square</span> |

### Links

Choose whether terminal links that launch other apps ask first. In Settings > Terminal > Links. <span hidden>hyperlink, url, osc 8, command click, cmd click, open link, security, safety, custom scheme, url scheme</span>

| Setting | Description |
| --- | --- |
| <span id="setting-links-links-that-open-apps" data-entry="Links that open apps"></span>Links that open apps | Ask before, always, or never open links such as ssh:// that launch another app. <span hidden>In Settings &gt; Terminal &gt; Links.</span> <span hidden>ask first, ask before opening, always open, never open, confirm, confirmation, prompt, allow, block, vscode, ssh, custom scheme, launch app, open app</span> |
| <span id="setting-links-trusted-links" data-entry="Trusted links"></span>Trusted links | Review and remove the links you chose to always open, once Always Open adds one. <span hidden>In Settings &gt; Terminal &gt; Links.</span> <span hidden>always open, allowed links, allowlist, trusted apps, remembered, forget, remove, revoke, permissions</span> |

### Integrated Path Browser

Choose what Insert Path lists when it types a path at the cursor. In Settings > Terminal > Integrated Path Browser. <span hidden>insert path, path browser, file picker, path picker, paths, files, folders, autocomplete, completion</span>

| Setting | Description |
| --- | --- |
| <span id="setting-integrated-path-browser-show-recently-used-paths" data-entry="Show recently used paths"></span>Show recently used paths | List the paths you chose most recently first, shared by every session on a host. <span hidden>In Settings &gt; Terminal &gt; Integrated Path Browser.</span> <span hidden>recents, recent files, history, recently used, mru, frequent, remember, on, off</span> |

### Titles

Choose how terminal tabs and pane headers are named. In Settings > Terminal > Titles. <span hidden>tab, window, pane, name, label</span>

| Setting | Description |
| --- | --- |
| <span id="setting-titles-prefer-generated-titles" data-entry="Prefer generated titles"></span>Prefer generated titles | Show the running program and its directory instead of titles that shells set. <span hidden>In Settings &gt; Terminal &gt; Titles.</span> <span hidden>shell title, generated title, program name, process name, directory, working directory, path, ignore shell titles, always use generated titles, automatic, on, off</span> |

### Shell

Choose the command run when a new local terminal opens. In Settings > Terminal > Shell. <span hidden>startup, session, command line, login</span>

| Setting | Description |
| --- | --- |
| <span id="setting-shell-startup-command" data-entry="Startup command"></span>Startup command | Set the executable and arguments, or leave empty for the default login shell. <span hidden>In Settings &gt; Terminal &gt; Shell.</span> <span hidden>shell, zsh, bash, fish, executable, arguments, launch, startup, working directory</span> |

## Remote Hosts

### Remote hosts

Add, reorder, and manage remote Rex hosts, view connection status, answer SSH questions, retry connection failures, and forward ports. In Settings > Remote Hosts > Remote hosts. <span hidden>remote, connections, machines, servers, add host, remove host, retry, host details, remote host status, remote host reconnect, connection warning, connection failure, host address, reorder hosts, host order, sort hosts, arrange hosts, move host up, move host down, drag hosts, ssh, ssh host, ssh passphrase, ssh password, host key, answer ssh question, waiting for an answer, needs your answer, finish setup, trust host, port forwarding, forward port, forwards, tunnel, ssh `-L`, localhost link</span>

### SSH

Choose how ssh reaches ssh:// hosts, including the SSH agent it asks for keys, such as Secretive or 1Password. In Settings > Remote Hosts > SSH. <span hidden>ssh agent, agent, SSH_AUTH_SOCK, auth sock, agent socket, IdentityAgent, identity agent, ssh keys, keys, publickey, permission denied, authentication failed, Secretive, 1Password, Bitwarden, yubikey-agent, YubiKey, gpg-agent, GnuPG, system agent, custom agent</span>

| Setting | Description |
| --- | --- |
| <span id="setting-ssh-ssh-agent" data-entry="SSH agent"></span>SSH agent | Where ssh finds keys for ssh:// hosts: the macOS agent, an agent on this Mac, or a custom socket. <span hidden>In Settings &gt; Remote Hosts &gt; SSH.</span> <span hidden>system, macOS agent, Secretive, 1Password, Bitwarden, yubikey-agent, gpg-agent, custom</span> |
| <span id="setting-ssh-ssh-agent-socket" data-entry="SSH agent socket"></span>SSH agent socket | The path of a custom agent's socket. <span hidden>In Settings &gt; Remote Hosts &gt; SSH.</span> <span hidden>socket path, SSH_AUTH_SOCK, custom agent, agent.sock</span> |
| <span id="setting-ssh-ssh-agent-status" data-entry="SSH agent status"></span>SSH agent status | Whether the chosen agent answers, and how many keys it holds. <span hidden>In Settings &gt; Remote Hosts &gt; SSH.</span> <span hidden>check again, keys, ssh-add, not running, no keys</span> |

## Developer

<div class="api-tags"><span class="api-tag developer"><a href="#setting-developer-developer-mode">Developer Mode</a></span></div>

Reveal settings for development and debugging. In Settings > Developer. <span hidden>advanced, debug, tools</span>

| Setting | Description |
| --- | --- |
| <span id="setting-developer-developer-mode" data-entry="Developer Mode"></span>Developer Mode | Show update-channel, installation, local API, web-client origin, and experiment settings. <span hidden>In Settings &gt; Developer.</span> <span hidden>enable, advanced, development, debugging</span> |

### Experimental

Try features that aren't finished when Developer Mode is enabled. In Settings > Developer > Experimental. <span hidden>experiment, beta, unfinished, labs</span>

| Setting | Description |
| --- | --- |
| <span id="setting-experimental-download-new-updates-when-available" data-entry="Download new updates when available"></span>Download new updates when available | Download updates in the background and ask before restarting. <span hidden>In Settings &gt; Developer &gt; Experimental. Needs Developer Mode.</span> <span hidden>auto download, background download, install on quit, download new updates, auto update, sparkle</span> |
| <span id="setting-experimental-install-updates-automatically" data-entry="Install updates automatically"></span>Install updates automatically | Relaunch into the new version while Rex is not visible. <span hidden>In Settings &gt; Developer &gt; Experimental. Needs Developer Mode.</span> <span hidden>auto install, auto relaunch, auto restart, silent update, unattended update, relaunch when idle</span> |
| <span id="setting-experimental-terminal-anchoring" data-entry="Terminal anchoring"></span>Terminal anchoring | Pin terminal text to the top or the bottom of each pane. <span hidden>In Settings &gt; Developer &gt; Experimental. Needs Developer Mode.</span> <span hidden>anchor, terminal alignment, grid gravity, leftover row</span> |
| <span id="setting-experimental-new-quick-switcher" data-entry="New quick switcher"></span>New quick switcher | Open one list of every session, block, and reporting process from Go to Anything. <span hidden>In Settings &gt; Developer &gt; Experimental. Needs Developer Mode.</span> <span hidden>go to anything, go to session, view activity, switcher, command palette, cmd p, jump, navigate, panes, blocks, sessions, experiment</span> |

### App Version

Choose how Rex.app receives updates when Developer Mode is enabled. In Settings > Developer > App Version. <span hidden>application, release, update, channel, upgrade</span>

| Setting | Description |
| --- | --- |
| <span id="setting-app-version-update-channel" data-entry="Update Channel"></span>Update Channel | Choose Preview or Bleeding Edge updates when Developer Mode is enabled. <span hidden>In Settings &gt; Developer &gt; App Version. Needs Developer Mode.</span> <span hidden>bleeding edge, testing, snapshot, preview, version, upgrade</span> |

### Rex Installation

Choose the included server or run Rex from a source checkout. In Settings > Developer > Rex Installation. <span hidden>binary, install, source, development</span>

| Setting | Description |
| --- | --- |
| <span id="setting-rex-installation-location" data-entry="Location"></span>Location | Use the server included with Rex.app or a local source checkout. <span hidden>In Settings &gt; Developer &gt; Rex Installation. Needs Developer Mode.</span> <span hidden>bundled, included, source checkout, installation</span> |
| <span id="setting-rex-installation-source-directory" data-entry="Source Directory"></span>Source Directory | Choose the folder containing the Rex source checkout. <span hidden>In Settings &gt; Developer &gt; Rex Installation. Needs Developer Mode.</span> <span hidden>folder, path, repository, repo, checkout, choose</span> |
| <span id="setting-rex-installation-installation-status" data-entry="Installation status"></span>Installation status | Check whether the selected Rex installation is ready to run. <span hidden>In Settings &gt; Developer &gt; Rex Installation. Needs Developer Mode.</span> <span hidden>show details, hide details, version, binary, executable, copy, paths, status</span> |
| <span id="setting-rex-installation-version" data-entry="Version"></span>Version | View and copy the included server version under Show Details. <span hidden>In Settings &gt; Developer &gt; Rex Installation. Needs Developer Mode.</span> <span hidden>release, build, included, bundled, copy</span> |
| <span id="setting-rex-installation-commit" data-entry="Commit"></span>Commit | View and copy the source commit of the included server under Show Details. <span hidden>In Settings &gt; Developer &gt; Rex Installation. Needs Developer Mode.</span> <span hidden>revision, git, hash, build, copy</span> |
| <span id="setting-rex-installation-final-sha-256" data-entry="Final SHA-256"></span>Final SHA-256 | View and copy the included server checksum under Show Details. <span hidden>In Settings &gt; Developer &gt; Rex Installation. Needs Developer Mode.</span> <span hidden>checksum, digest, hash, sha256, integrity, copy</span> |

### Local API

Inspect the local server's API endpoint when Developer Mode is enabled. In Settings > Developer > Local API. <span hidden>local server, control api, development, connection</span>

| Setting | Description |
| --- | --- |
| <span id="setting-local-api-local-api-url" data-entry="Local API URL"></span>Local API URL | View and copy the local server endpoint when Developer Mode is enabled. <span hidden>In Settings &gt; Developer &gt; Local API. Needs Developer Mode.</span> <span hidden>address, localhost, port, http, copy local endpoint, unix socket, socket path</span> |

### Web Client Origins

Manage browser origins allowed to open Rex WebSocket streams. In Settings > Developer > Web Client Origins. <span hidden>browser, web, websocket, security, cors</span>

| Setting | Description |
| --- | --- |
| <span id="setting-web-client-origins-allowed-origins" data-entry="Allowed origins"></span>Allowed origins | Review the permitted origins and remove custom entries. <span hidden>In Settings &gt; Developer &gt; Web Client Origins. Needs Developer Mode.</span> <span hidden>allowlist, delete, remove, hosted client, default, permissions</span> |
| <span id="setting-web-client-origins-origin-pattern" data-entry="Origin pattern"></span>Origin pattern | Enter a browser origin or wildcard subdomain to allow after restarting Rex. <span hidden>In Settings &gt; Developer &gt; Web Client Origins. Needs Developer Mode.</span> <span hidden>add origin, allowed origin, wildcard, subdomain, domain, https, web client</span> |

---
title: "iOS Actions"
canonical_url: https://www.superlogical.com/rex/docs/reference/ios-actions
---

# iOS Actions

This page lists every action in the Rex for iOS command palette. Use the name to run it with `rex do`.

<div class="reference-page actions" hidden></div>

## Window

| Action | Name |
| --- | --- |
| <span id="action-client-window-new" data-entry="New Window"></span>New Window | `client.window.new` <span hidden>create workspace window</span> |
| <span id="action-client-window-close" data-entry="Close Window"></span>Close Window | `client.window.close` <span hidden>close workspace window</span> |
| <span id="action-client-close" data-entry="Close"></span>Close Closes the focused pane, else the selected tab, else the window. | `client.close` <span hidden>close pane tab window focused</span> |

## App

| Action | Name |
| --- | --- |
| <span id="action-client-settings" data-entry="Settings…"></span>Settings… | `client.settings` <span hidden>preferences settings options configure</span> |
| <span id="action-client-palette-toggle" data-entry="Command Palette…"></span>Command Palette… | `client.palette.toggle` <span hidden>command palette search commands actions</span> |
| <span id="action-client-version-copy" data-entry="Copy Version Info"></span>Copy Version Info Copies the app and server versions to the clipboard. | `client.version.copy` <span hidden>version build number copy clipboard about app server release</span> |
| <span id="action-client-debug-copy" data-entry="Copy Debug Info"></span>Copy Debug Info Copies a report of the app, system, server, and host state to the clipboard. | `client.debug.copy` <span hidden>debug debugging info report copy clipboard diagnostics troubleshoot support bug commit build environment</span> |
| <span id="action-client-diagnostics" data-entry="Rex Diagnostics"></span>Rex Diagnostics | `client.diagnostics` <span hidden>diagnostics logs debug troubleshoot server report</span> |
| <span id="action-client-agents-setup" data-entry="Install Rex for Agents"></span>Install Rex for Agents Installs the Rex skill and hooks for Claude Code, Codex, and other agents. | `client.agents.setup` <span hidden>`rex skill` install agent agents coding ai claude code codex cursor copilot gemini hook hooks setup</span> |

## View

| Action | Name |
| --- | --- |
| <span id="action-client-sidebar-toggle" data-entry="Toggle Sidebar"></span>Toggle Sidebar | `client.sidebar.toggle` <span hidden>sidebar collapse expand fold hide show tabs</span> |
| <span id="action-client-sidebar-activity-toggle" data-entry="Toggle Activity"></span>Toggle Activity | `client.sidebar.activity.toggle` <span hidden>activity mode view filter working waiting attention programs status sidebar tabs</span> |
| <span id="action-client-config-copy-path" data-entry="Copy Config File Path"></span>Copy Config File Path | `client.config.copy_path` <span hidden>config configuration file path copy clipboard init.lua lua keybindings shortcuts settings location where</span> |
| <span id="action-client-font-increase" data-entry="Increase Font Size"></span>Increase Font Size | `client.font.increase` <span hidden>terminal font size larger bigger increase zoom text typography</span> |
| <span id="action-client-font-decrease" data-entry="Decrease Font Size"></span>Decrease Font Size | `client.font.decrease` <span hidden>terminal font size smaller decrease zoom text typography</span> |
| <span id="action-client-font-reset" data-entry="Reset Font Size"></span>Reset Font Size | `client.font.reset` <span hidden>terminal font size reset default original text typography</span> |
| <span id="action-client-theme-change" data-entry="Change Theme…"></span>Change Theme… | `client.theme.change` <span hidden>theme colour color scheme palette appearance</span> |
| <span id="action-client-appearance-switch-mode" data-entry="Switch Appearance Mode"></span>Switch Appearance Mode | `client.appearance.switch_mode` <span hidden>appearance mode light dark toggle color scheme</span> |
| <span id="action-client-sidebar-filter-focus" data-entry="Focus Tab Filter"></span>Focus Tab Filter | `client.sidebar.filter.focus` <span hidden>filter search find type query focus keyboard sidebar activity tabs sessions hosts</span> |
| <span id="action-client-keyboard-show" data-entry="Show Keyboard"></span>Show Keyboard | `client.keyboard.show` <span hidden>keyboard show open type input</span> |
| <span id="action-client-keyboard-hide" data-entry="Hide Keyboard"></span>Hide Keyboard | `client.keyboard.hide` <span hidden>keyboard hide close dismiss</span> |

## Session

| Action | Name |
| --- | --- |
| <span id="action-session-new" data-entry="New Session"></span>New Session | `session.new` <span hidden>create session add</span> |
| <span id="action-session-select" data-entry="Go to Session"></span>Go to Session | `session.select` <span hidden>session select show jump go identifier id</span> |
| <span id="action-session-next" data-entry="Next Session"></span>Next Session | `session.next` <span hidden>cycle session forward</span> |
| <span id="action-session-previous" data-entry="Previous Session"></span>Previous Session | `session.previous` <span hidden>cycle session back</span> |
| <span id="action-session-rename" data-entry="Rename Session"></span>Rename Session | `session.rename` <span hidden>rename session name title</span> |
| <span id="action-session-close" data-entry="Close Session"></span>Close Session | `session.close` <span hidden>close kill destroy delete session</span> |
| <span id="action-session-refresh" data-entry="Refresh Sessions"></span>Refresh Sessions | `session.refresh` <span hidden>reload sync sessions</span> |
| <span id="action-session-switch" data-entry="Change Session…"></span>Change Session… | `session.switch` <span hidden>session switch jump go</span> |

## Host

| Action | Name |
| --- | --- |
| <span id="action-client-host-add" data-entry="Add Remote Host…"></span>Add Remote Host… | `client.host.add` <span hidden>host remote add server machine url http https connect</span> |
| <span id="action-client-host-remove" data-entry="Remove Host…"></span>Remove Host… | `client.host.remove` <span hidden>remove forget delete disconnect host remote server machine</span> |
| <span id="action-client-host-forward-port" data-entry="Forward Port…"></span>Forward Port… | `client.host.forward_port` <span hidden>port forward forwarding tunnel localhost open browser ssh `-L` local remote host</span> |

## Tab

| Action | Name |
| --- | --- |
| <span id="action-client-tab-new" data-entry="New Tab"></span>New Tab | `client.tab.new` <span hidden>create tab window add</span> |
| <span id="action-client-tab-previous" data-entry="Show Previous Tab"></span>Show Previous Tab | `client.tab.previous` <span hidden>cycle tab window back</span> |
| <span id="action-client-tab-next" data-entry="Show Next Tab"></span>Show Next Tab | `client.tab.next` <span hidden>cycle tab window forward</span> |
| <span id="action-client-tab-goto" data-entry="Go to Tab"></span>Go to Tab | `client.tab.goto` <span hidden>tab window select number index jump go</span> |
| <span id="action-client-tab-move-backward" data-entry="Move Tab Backward"></span>Move Tab Backward | `client.tab.move.backward` <span hidden>move tab window reorder left up earlier</span> |
| <span id="action-client-tab-move-forward" data-entry="Move Tab Forward"></span>Move Tab Forward | `client.tab.move.forward` <span hidden>move tab window reorder right down later</span> |
| <span id="action-client-tab-rename" data-entry="Rename Tab"></span>Rename Tab | `client.tab.rename` <span hidden>rename tab window name title</span> |
| <span id="action-client-tab-close" data-entry="Close Tab"></span>Close Tab | `client.tab.close` <span hidden>close tab window</span> |
| <span id="action-client-tab-close-others" data-entry="Close Other Tabs"></span>Close Other Tabs | `client.tab.close_others` <span hidden>close other tabs windows kill only keep</span> |
| <span id="action-client-tab-move" data-entry="Move Tab"></span>Move Tab | `client.tab.move` <span hidden>move tab window reorder</span> |
| <span id="action-client-tab-move-to-new-window" data-entry="Move Tab to New Window"></span>Move Tab to New Window | `client.tab.move_to_new_window` <span hidden>move tab new window tear off detach separate pop out own</span> |

## Pane

| Action | Name |
| --- | --- |
| <span id="action-pane-split-left" data-entry="Split Pane Left"></span>Split Pane Left | `pane.split.left` <span hidden>pane split terminal divide new left</span> |
| <span id="action-pane-split-right" data-entry="Split Pane Right"></span>Split Pane Right | `pane.split.right` <span hidden>pane split terminal divide new right</span> |
| <span id="action-pane-split-up" data-entry="Split Pane Up"></span>Split Pane Up | `pane.split.up` <span hidden>pane split terminal divide new above up</span> |
| <span id="action-pane-split-down" data-entry="Split Pane Down"></span>Split Pane Down | `pane.split.down` <span hidden>pane split terminal divide new below down</span> |
| <span id="action-pane-focus-previous" data-entry="Focus Previous Pane"></span>Focus Previous Pane | `pane.focus_previous` <span hidden>pane focus previous back</span> |
| <span id="action-pane-focus-next" data-entry="Focus Next Pane"></span>Focus Next Pane | `pane.focus_next` <span hidden>pane focus next forward</span> |
| <span id="action-pane-focus-left" data-entry="Focus Pane Left"></span>Focus Pane Left | `pane.focus.left` <span hidden>pane focus navigate left</span> |
| <span id="action-pane-focus-right" data-entry="Focus Pane Right"></span>Focus Pane Right | `pane.focus.right` <span hidden>pane focus navigate right</span> |
| <span id="action-pane-focus-up" data-entry="Focus Pane Up"></span>Focus Pane Up | `pane.focus.up` <span hidden>pane focus navigate up above</span> |
| <span id="action-pane-focus-down" data-entry="Focus Pane Down"></span>Focus Pane Down | `pane.focus.down` <span hidden>pane focus navigate down below</span> |
| <span id="action-pane-move-left" data-entry="Move Pane Left"></span>Move Pane Left | `pane.move.left` <span hidden>pane move swap rearrange left</span> |
| <span id="action-pane-move-right" data-entry="Move Pane Right"></span>Move Pane Right | `pane.move.right` <span hidden>pane move swap rearrange right</span> |
| <span id="action-pane-move-up" data-entry="Move Pane Up"></span>Move Pane Up | `pane.move.up` <span hidden>pane move swap rearrange up above</span> |
| <span id="action-pane-move-down" data-entry="Move Pane Down"></span>Move Pane Down | `pane.move.down` <span hidden>pane move swap rearrange down below</span> |
| <span id="action-pane-zoom" data-entry="Zoom Pane"></span>Zoom Pane | `pane.zoom` <span hidden>pane zoom maximize restore terminal</span> |
| <span id="action-pane-move-to-new-tab" data-entry="Move Pane to New Tab"></span>Move Pane to New Tab | `pane.move_to_new_tab` <span hidden>pane block move new window tab detach split</span> |
| <span id="action-pane-close" data-entry="Close Pane"></span>Close Pane | `pane.close` <span hidden>pane close remove terminal</span> |
| <span id="action-pane-split" data-entry="Split Pane"></span>Split Pane | `pane.split` <span hidden>pane split terminal divide new</span> |
| <span id="action-pane-split-auto" data-entry="Split Pane Best Fit"></span>Split Pane Best Fit | `pane.split.auto` <span hidden>pane split terminal divide new automatic best fit</span> |
| <span id="action-pane-close-others" data-entry="Close Other Panes"></span>Close Other Panes | `pane.close_others` <span hidden>close other panes kill only keep focused</span> |
| <span id="action-pane-move-to-tab" data-entry="Move Pane to Tab…"></span>Move Pane to Tab… | `pane.move_to_tab` <span hidden>pane block move tab window send other existing join</span> |
| <span id="action-pane-go-to-directory" data-entry="Go to Directory…"></span>Go to Directory… | `pane.go_to_directory` <span hidden>go to directory folder path cd new tab open navigate</span> |
| <span id="action-pane-insert-path" data-entry="Insert Path…"></span>Insert Path… | `pane.insert_path` <span hidden>insert path file folder directory cursor type paste complete browse</span> |
| <span id="action-pane-copy-id" data-entry="Copy Pane ID"></span>Copy Pane ID Copies the focused pane's block ID to the clipboard. | `pane.copy_id` <span hidden>pane block id identifier copy clipboard target agent capture</span> |
| <span id="action-pane-rename" data-entry="Rename Pane"></span>Rename Pane | `pane.rename` <span hidden>rename pane block name title label</span> |
| <span id="action-pane-balance" data-entry="Balance Splits"></span>Balance Splits | `pane.balance` <span hidden>pane split balance equalize equalise evenly distribute resize layout</span> |
| <span id="action-pane-resize" data-entry="Resize Pane"></span>Resize Pane | `pane.resize` <span hidden>pane resize grow shrink divider wider narrower taller shorter</span> |
| <span id="action-pane-float" data-entry="Float Pane"></span>Float Pane | `pane.float` <span hidden>pane float floating window dock undock detach overlay pop out tile</span> |
| <span id="action-pane-new-floating" data-entry="New Floating Pane"></span>New Floating Pane | `pane.new_floating` <span hidden>pane new create open float floating window terminal overlay popup scratchpad</span> |
| <span id="action-pane-minimize-floating" data-entry="Minimize Floating Window"></span>Minimize Floating Window | `pane.minimize_floating` <span hidden>pane floating window hide show toggle minimize minimise restore taskbar capsule stash scratchpad</span> |
| <span id="action-pane-minimize-all-floating" data-entry="Minimize Floating Windows"></span>Minimize Floating Windows | `pane.minimize_all_floating` <span hidden>pane floating windows all hide show toggle minimize minimise restore taskbar capsule stash scratchpad</span> |
| <span id="action-pane-paste" data-entry="Paste into Pane"></span>Paste into Pane Pastes the clipboard into the focused terminal. | `pane.paste` <span hidden>paste clipboard insert text terminal</span> |
| <span id="action-pane-copy" data-entry="Copy from Pane"></span>Copy from Pane Copies the focused terminal's selection to the clipboard. | `pane.copy` <span hidden>copy selection clipboard text terminal</span> |
| <span id="action-pane-select-all" data-entry="Select All in Pane"></span>Select All in Pane Selects everything in the focused terminal, including its scrollback. | `pane.select_all` <span hidden>select all selection text terminal scrollback</span> |
| <span id="action-pane-send-key" data-entry="Send Key"></span>Send Key Sends one key to the focused terminal, as if it had been pressed there. | `pane.send_key` <span hidden>send key terminal press chord binding</span> |
| <span id="action-client-prompt-previous" data-entry="Jump to Previous Prompt"></span>Jump to Previous Prompt | `client.prompt.previous` <span hidden>jump scroll previous prompt command mark shell integration up back history</span> |
| <span id="action-client-prompt-next" data-entry="Jump to Next Prompt"></span>Jump to Next Prompt | `client.prompt.next` <span hidden>jump scroll next prompt command mark shell integration down forward</span> |
| <span id="action-client-scroll-page-up" data-entry="Scroll Page Up"></span>Scroll Page Up | `client.scroll.page_up` <span hidden>scroll page up back scrollback history</span> |
| <span id="action-client-scroll-page-down" data-entry="Scroll Page Down"></span>Scroll Page Down | `client.scroll.page_down` <span hidden>scroll page down forward scrollback history</span> |
| <span id="action-pane-go-to" data-entry="Go to Anything…"></span>Go to Anything… | `pane.go_to` <span hidden>go to anything pane block tab session agent jump switch navigate find search tree overview</span> |
| <span id="action-pane-view-activity" data-entry="View Activity…"></span>View Activity… | `pane.view_activity` <span hidden>view activity active working blocked failed done agents programs status priority</span> |

---
title: "iOS Settings"
canonical_url: https://www.superlogical.com/rex/docs/reference/ios-settings
---

# iOS Settings

This page lists every setting in Rex for iOS and where to find it.

<div class="reference-page settings" hidden></div>

## Interface

Choose the color scheme, theme tint, panes, and how panes move. In Settings > Interface. <span hidden>display, automatic, system, light, dark, animation, motion</span>

| Setting | Description |
| --- | --- |
| <span id="setting-interface-color-scheme" data-entry="Color scheme"></span>Color scheme | Appearance · Use Auto, Light, or Dark appearance. <span hidden>In Settings &gt; Interface.</span> <span hidden>appearance, display, system, device, mode, automatic, light mode, dark mode</span> |
| <span id="setting-interface-tint-with-theme" data-entry="Tint with theme"></span>Tint with theme | Appearance · Match terminal colors or use standard iOS colors. <span hidden>In Settings &gt; Interface.</span> <span hidden>theme tint, use theme tint, tinting, matching, neutral, system colors, on, off</span> |
| <span id="setting-interface-match-program-backgrounds" data-entry="Match program backgrounds"></span>Match program backgrounds | Panes · Color the screen with a full-screen program's background. <span hidden>In Settings &gt; Interface.</span> <span hidden>tint, screen color, header color, terminal background, program background, editor, full screen, tui, neovim, vim, extend, blend, match terminal</span> |

## Themes

Choose the terminal colors for each appearance. In Settings > Themes. <span hidden>palette, color scheme, light, dark</span>

| Setting | Description |
| --- | --- |
| <span id="setting-themes-light-theme" data-entry="Light theme"></span>Light theme | Light Mode · Choose a terminal palette for light appearance. <span hidden>In Settings &gt; Themes.</span> <span hidden>colors, day, scheme, light mode</span> |
| <span id="setting-themes-dark-theme" data-entry="Dark theme"></span>Dark theme | Dark Mode · Choose a terminal palette for dark appearance. <span hidden>In Settings &gt; Themes.</span> <span hidden>colors, night, scheme, dark mode</span> |

## Terminal

Configure the font, text colors, cursor, keyboard, key bar, scrolling, and titles. In Settings > Terminal. <span hidden>keyboard, input, accessory, shortcuts</span>

| Setting | Description |
| --- | --- |
| <span id="setting-terminal-terminal-font" data-entry="Terminal font"></span>Terminal font | Typography · Choose the terminal's font family. <span hidden>In Settings &gt; Terminal.</span> <span hidden>font, typeface, monospace, family, menlo, sf mono, system font, jetbrains mono, geist mono, ioskeley mono, roboto mono</span> |
| <span id="setting-terminal-font-size" data-entry="Font size"></span>Font size | Typography · Set the size of terminal text on this device. <span hidden>In Settings &gt; Terminal.</span> <span hidden>text size, bigger, larger, smaller, zoom, points, pt</span> |
| <span id="setting-terminal-improve-text-contrast" data-entry="Improve text contrast"></span>Improve text contrast | Colors · Adjust low-contrast text against its background; included when forcing the theme palette. <span hidden>In Settings &gt; Terminal.</span> <span hidden>minimum contrast, automatic, legibility, readability, accessibility, unreadable, faint, pale, colors, on, off</span> |
| <span id="setting-terminal-force-theme-palette-for-text" data-entry="Force theme palette for text"></span>Force theme palette for text | Colors · Map RGB and extended text colors to the nearest theme color, then correct contrast. <span hidden>In Settings &gt; Terminal.</span> <span hidden>force theme colors, 256 colors, truecolor, rgb, ansi, extended colors, nearest, palette</span> |
| <span id="setting-terminal-cursor-style" data-entry="Cursor style"></span>Cursor style | Cursor · Choose a block, bar, or underline cursor. <span hidden>In Settings &gt; Terminal.</span> <span hidden>cursor, caret, shape, block, bar, beam, underline</span> |
| <span id="setting-terminal-blink-speed" data-entry="Blink speed"></span>Blink speed | Cursor · Make the cursor blink slowly, normally, quickly, or not at all. <span hidden>In Settings &gt; Terminal.</span> <span hidden>cursor, blink, blinking, flash, solid, steady, off, slow, fast</span> |
| <span id="setting-terminal-force-style" data-entry="Force style"></span>Force style | Cursor · Keep your cursor style when programs ask for another. <span hidden>In Settings &gt; Terminal.</span> <span hidden>cursor, override, ignore, lock, shape, decscusr</span> |
| <span id="setting-terminal-autocorrect" data-entry="Autocorrect"></span>Autocorrect | Keyboard · Let the keyboard correct and predict words. <span hidden>In Settings &gt; Terminal.</span> <span hidden>typing, auto correction, autocorrection, predictive text, spelling</span> |
| <span id="setting-terminal-keep-keyboard-open" data-entry="Keep keyboard open"></span>Keep keyboard open | Keyboard · Open the keyboard again when you come back to a terminal. <span hidden>In Settings &gt; Terminal.</span> <span hidden>keyboard, background, reopen, restore, focus</span> |
| <span id="setting-terminal-key-repeat" data-entry="Key repeat"></span>Key repeat | Hardware Keyboard · Repeat a key you hold down on a hardware keyboard. <span hidden>In Settings &gt; Terminal.</span> <span hidden>hardware keyboard, magic keyboard, external keyboard, hold, held, repeating, backspace, delete, arrows, accessibility</span> |
| <span id="setting-terminal-delay-until-repeat" data-entry="Delay until repeat"></span>Delay until repeat | Hardware Keyboard · How long to hold a key before it starts repeating. <span hidden>In Settings &gt; Terminal.</span> <span hidden>hardware keyboard, key repeat, delay, wait, hold, seconds, speed</span> |
| <span id="setting-terminal-key-repeat-interval" data-entry="Key repeat interval"></span>Key repeat interval | Hardware Keyboard · How quickly a held key repeats. <span hidden>In Settings &gt; Terminal.</span> <span hidden>hardware keyboard, key repeat, rate, speed, fast, slow, seconds, interval</span> |
| <span id="setting-terminal-repeat-held-letters" data-entry="Repeat held letters"></span>Repeat held letters | Hardware Keyboard · Repeat a held letter instead of showing its accents. <span hidden>In Settings &gt; Terminal.</span> <span hidden>hardware keyboard, accents, accented, accent menu, alternate characters, diacritics, long press, press and hold, letters, symbols</span> |
| <span id="setting-terminal-key-bar-layout" data-entry="Key Bar Layout"></span>Key Bar Layout | Key Bar · Arrange, hide, and reorder the key bar's built-in keys. <span hidden>In Settings &gt; Terminal.</span> <span hidden>arrows, escape, tab, control, option, shift, paste, home, end, page up, page down, delete, return, rearrange, reorder, hide keys</span> |
| <span id="setting-terminal-shortcut-groups" data-entry="Shortcut Groups"></span>Shortcut Groups | Key Bar · Add Control shortcuts, function keys, and your own. <span hidden>In Settings &gt; Terminal.</span> <span hidden>function keys, f1, ctrl, control, custom keys, slash commands, hotkeys, accessory</span> |
| <span id="setting-terminal-characters" data-entry="Characters"></span>Characters | Choose the punctuation keys. <span hidden>In Settings &gt; Terminal.</span> <span hidden>punctuation, shell characters, symbols, tilde, pipe, slash</span> |
| <span id="setting-terminal-use-symbols" data-entry="Use symbols"></span>Use symbols | Key Bar · Show symbols instead of abbreviated key names. <span hidden>In Settings &gt; Terminal.</span> <span hidden>keyboard, key bar, icons, control, option, shift</span> |
| <span id="setting-terminal-haptic-feedback" data-entry="Haptic feedback"></span>Haptic feedback | Key Bar · Feel a light tap when pressing key bar buttons. <span hidden>In Settings &gt; Terminal.</span> <span hidden>keyboard, vibration, vibrate, touch</span> |
| <span id="setting-terminal-command-palette" data-entry="Command palette"></span>Command palette | Key Bar · Learn how to open commands and use modifier keys. <span hidden>In Settings &gt; Terminal.</span> <span hidden>keyboard, shortcuts, command shift p, control, option, shift, paste</span> |
| <span id="setting-terminal-smooth-scrolling" data-entry="Smooth scrolling"></span>Smooth scrolling | Scrolling · Move through history smoothly instead of a line at a time. <span hidden>In Settings &gt; Terminal.</span> <span hidden>scroll, scrollback, history, pixel, fling, momentum, drag</span> |
| <span id="setting-terminal-smooth-terminal-resizing" data-entry="Smooth terminal resizing"></span>Smooth terminal resizing | Resizing · Reflow text as soon as a terminal changes size. <span hidden>In Settings &gt; Terminal.</span> <span hidden>smooth resize, resizing, reflow, re-wrap, rewrap, rotation, rotate, orientation, jitter, jump, flicker, stage manager, split view, preview, on, off</span> |
| <span id="setting-terminal-prefer-generated-titles" data-entry="Prefer generated titles"></span>Prefer generated titles | Titles · Show the running program and its directory instead of shell titles. <span hidden>In Settings &gt; Terminal.</span> <span hidden>titles, tab title, window title, program name, directory, shell title, osc, rename</span> |

## Remote Hosts

Manage saved hosts, their connections, port forwards, and the shell each one starts. In Settings > Remote Hosts. <span hidden>server, add, pair, connect, reconnect, edit, delete, remove, startup command, shell, default shell, login shell, nushell, zsh, fish, port forwarding, forward, forwards, tunnel, localhost, ssh `-L`</span>

| Setting | Description |
| --- | --- |
| <span id="setting-remote-hosts-ssh-key" data-entry="SSH Key"></span>SSH Key | Create or import the key Rex logs in to ssh:// hosts with, and the host keys it trusts. <span hidden>In Settings &gt; Remote Hosts.</span> <span hidden>ssh, key, private key, public key, authorized_keys, secure enclave, import, passphrase, ed25519, ecdsa, host key, fingerprint, known hosts, trust</span> |

## Developer Mode

Developer · Expose settings for development and debugging. In Settings > Developer Mode. <span hidden>developer, debug, diagnostics</span>

| Setting | Description |
| --- | --- |
| <span id="setting-developer-mode-network-diagnostics" data-entry="Network Diagnostics"></span>Network Diagnostics | With Developer Mode on, float the network path and host connections over the screen. <span hidden>In Settings &gt; Developer Mode. Needs Developer Mode.</span> <span hidden>debug, network, path, wi-fi, cellular, reconnect, overlay</span> |
| <span id="setting-developer-mode-cursor-shaders" data-entry="Cursor shaders"></span>Cursor shaders | Experimental · With Developer Mode on, draw a trail shader, such as Warp, Comet, or Blaze, behind the cursor. <span hidden>In Settings &gt; Developer Mode. Needs Developer Mode.</span> <span hidden>cursor, cursor trail, cursor effect, cursor animation, smear, glow, warp, sweep, comet, blaze, tapered, rainbow, gradient, frozen, ghostty shader, custom-shader, shadertoy, shader, experimental</span> |

## About

About · See the app's version and device, and copy them for a bug report. In Settings > About. <span hidden>version, build, commit, device, model, ios, system, bug report, feedback, copy version info, copy debug info, diagnostics, support, license, open source, acknowledgements, fonts</span>

---
title: "Lua API"
canonical_url: https://www.superlogical.com/rex/docs/reference/lua
---

# Lua API

This page lists the full Lua API Rex exposes. Rex only adds API to the
global `rex` table.  Rex runs Lua 5.1 with the full standard library,
including `io`, `os`, and `require`. The one exception is `io.popen` and
`os.execute`, which raise an error. Use [`rex.exec`](#rexexecargv-opts) to run
commands instead.

## Where Each Function Works

Rex runs Lua in two places, the configuration file and scripts, and each
function below is tagged with where it works:

| Tag | Description |
| --- | --- |
| <span class="api-tag">Config</span> | Works in the [configuration file](https://www.superlogical.com/rex/docs/customize/config.md). |
| <span class="api-tag">Script</span> | Works in scripts run with [`rex do`](https://www.superlogical.com/rex/docs/automate/lua-scripts.md). |
| <span class="api-tag">Config <span class="when">at load</span></span> | Only at load, while the file runs from top to bottom. Either tag can be narrowed this way. |
| <span class="api-tag">Config <span class="when">later</span></span> | Only later, inside an action, a function bound to a key, or an event handler. |

Anything short of working is noted at the top of the function's docs.


| Function | Config, at load | Config, later | Script, at load | Script, later |
| --- | --- | --- | --- | --- |
| `rex.bind`, `rex.unbind` | Yes | Functions only | No effect | No effect |
| `rex.mode`, `rex.bindings`, `rex.modes` | Yes | Error | No effect | Error |
| `rex.action` | Yes | Error | Yes | Error |
| `rex.host` | Yes | Error | No effect | Error |
| `rex.on` | Warning | Error | Yes | Error |
| `rex.call`, `rex.session.*`, `rex.block.call` | Fails | Yes | Yes | Yes |
| `rex.invoke` | Warning | Yes | Yes | Yes |
| `rex.client.queue` | Error | Actions only | Yes | Yes |
| `rex.exec`, `rex.exec.start` | Yes | Yes | Yes | Yes |
| `rex.layout.*`, `rex.json.*`, `rex.log` | Yes | Yes | Yes | Yes |
| `rex.sleep`, `rex.wait`, `rex.stop` | Missing | Missing | Yes | Yes |
| `rex.after`, `rex.every` | Missing | Missing | Yes | Yes |
| `rex.args`, `rex.session_id`, `rex.block_id` | Missing | Missing | Yes | Yes |

- **No effect** means the call is accepted and does nothing useful. A script's
  key bindings and hosts never reach a keymap or the server.
- **Missing** means the name is `nil`. Calling it is an error, so a file shared
  between both should check for it first.
- **Fails** means the call returns `nil` and an error message, and is reported
  as a warning.

## Key Bindings

See [Keyboard Shortcuts](https://www.superlogical.com/rex/docs/customize/keyboard-shortcuts.md),
[Key Sequences](https://www.superlogical.com/rex/docs/customize/key-sequences.md), and [Key Modes](https://www.superlogical.com/rex/docs/customize/key-modes.md).

### `rex.bind(key, action[, args])`

Binds a key to an action by name. `args` is an optional table of arguments
passed to the action.

```lua
rex.bind("cmd+d", "pane.split", { direction = "right" })
```

`key` is a single key, a sequence written with `>`, or either one prefixed with
a mode and a slash.

### `rex.bind(key, fn)`

Binds a key to a Lua function called as `fn(ctx, ev)`. Return `true` to mark
the key as handled. Not allowed in a mode.

`ev.keybinding` is the key that was pressed. For a sequence, `ev.prefix` is a
list of the keys before it.

### `rex.unbind(key[, target])`

Removes the bindings of a key in this file and in the client's defaults.
Returns the number of this file's bindings it removed.

`target` is an action name or a function, and limits the removal to bindings
with that target. `rex.unbind("mode/key")` also blocks the key in that mode.

### `rex.unbind{ mode = name }`

Removes a whole mode: its bindings, its blocked keys, and its options.

### `rex.unbind(binding)`

Removes one binding, given a table returned by `rex.bindings`. Returns 1.

### `rex.mode(name[, options])`

Declares a mode and sets its options. The only option is `exclusive`, a
boolean that defaults to `false`.

### `rex.bindings([filter])`

Returns the bindings this file and the files it requires have made so far, in
order. `filter` is an optional table with any of `key`, `mode`, and `action`.

Each binding is a table:

| Field | Description |
| --- | --- |
| `key` | The key or sequence, without its mode. |
| `mode` | The mode, or `nil`. |
| `action` | The action name, or `nil` for a function binding. |
| `fn` | The function, or `nil` for an action binding. |
| `args` | The arguments table, or `nil`. |
| `source` | Where it was bound, such as `keys.lua:3`. |

### `rex.modes()`

Returns the modes declared or bound so far, each a table with `name`,
`exclusive`, and `source`.

## Actions

See [Custom Actions](https://www.superlogical.com/rex/docs/customize/actions.md).

### `rex.action{ ... }`

Defines an action.

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Required. Lowercase letters, digits, and underscores. |
| `title` | string | Required. Shown in the command palette. |
| `run` | function | Required. Called as `run(ctx, args)`. |
| `description` | string | A longer explanation. |
| `category` | string | A group for the palette. Defaults to `Actions`. |
| `keywords` | string | Extra words for search. |
| `args` | table | Argument types, or a JSON Schema. |
| `repeats` | boolean | Run again while the key is held. |

`args` types are `string`, `number`, `integer`, `boolean`, `object`, `array`,
and `any`. A trailing `?` marks an argument as optional.

`ctx` has `session_id`, `block_id`, `client_id`, `origin`, and `server`. A
field Rex doesn't know is absent. The value `run` returns is the action's
result.

### `rex.invoke(name[, args])`

Runs an action and returns its result, or `nil` and an error message.

In the configuration file, only that file's own actions can be invoked. In a
script, a name the script doesn't define is run by the server, so a script
can invoke actions from the configuration file.

Calls can nest up to 16 deep.

### `rex.client.queue(name[, args])`

Asks the client to perform one of its own actions, such as `pane.zoom`.

In an action, queued actions run in order after `run` returns, and are
discarded if it fails. In a script, each one is performed immediately.

Raises an error when there is no client to ask.

## Calling Rex

See [Lua Scripts](https://www.superlogical.com/rex/docs/automate/lua-scripts.md#calling-rex).

Every call in this section returns a result table, or `nil` and an error
message. A failure never raises an error.

### `rex.session.<method>(args)`

Calls a session method. `args` is a table. `session_id` is filled in when it
is left out. Other IDs, such as `block_id`, are not.

```lua
local view, err = rex.session.view{}
```

| Method | Description |
| --- | --- |
| `view` | Return the session's windows, layouts, and blocks. |
| `list_blocks` | List every block and whether it is placed or detached. |
| `list_clients` | List the clients attached to the session. |
| `describe_block` | Return a block's placement and the methods it offers. |
| `new_window` | Create a window from a layout. |
| `new_split` | Create a split beside an existing block. |
| `new_block` | Create a block without placing it in a layout. |
| `new_layer` | Create a floating layer in a window. |
| `close_window` | Close a window and every block in it. |
| `close_layer` | Close a floating layer and every block in it. |
| `move_block` | Move a block into a new split beside another. |
| `swap_blocks` | Exchange the positions of two blocks. |
| `detach_block` | Remove a block from its layout without closing it. |
| `float_block` | Move a tiled block into a new floating layer. |
| `zoom_block` | Toggle or set whether a block fills its window. |
| `resize_pane` | Move the border between two blocks. |
| `resize_layer` | Set a floating layer's position and size. |
| `raise_layer`, `lower_layer` | Change a floating layer's stacking order. |
| `move_window` | Move a window in the tab order. |
| `focus_block` | Focus a block. |
| `focus_window` | Focus a window. |
| `focus_direction` | Focus the nearest block in a direction. |
| `focus_next_pane`, `focus_previous_pane` | Focus the next or previous pane. |
| `focus_next_window`, `focus_previous_window` | Focus the next or previous window. |
| `set_label` | Set the session's label. |
| `set_window_label` | Set a window's label. |
| `set_block_label` | Set a block's label. |
| `attach`, `detach` | Change which session a script hears events from. |
| `help` | Describe the methods available through the session. |

For the arguments and result of a method, run `rex api describe` with its
full name:

```sh
rex api describe session.new_split
```

### `rex.call(method[, args])`

Calls any server method by its full name, including the ones `rex.session`
doesn't cover.

```lua
local result, err = rex.call("session.list")
```

`rex api list` prints every method.

### `rex.block.call(creator, method[, args])`

Calls a method on a block. `creator` is the full name of the kind of block.
`block_id` is filled in from the current block when it is left out.

```lua
local terminal = "com.superlogical.terminal"
local title = rex.block.call(terminal, "title", {}).title
```

The terminal's methods are:

| Method | Description |
| --- | --- |
| `format` | Return the screen as text, HTML, or VT sequences. |
| `process` | Describe the terminal's child and foreground processes. |
| `pwd` | Return the working directory the terminal last reported. |
| `title` | Return the terminal's title. |
| `size` | Return the terminal's size in cells and pixels. |
| `write` | Write bytes to the terminal as if they were typed. |
| `reset` | Reset the terminal. |
| `resize` | Request a size for the terminal. |
| `set_theme` | Replace the terminal's default colors. |
| `program_status` | Return the status records programs have reported. |
| `list_dir` | List the directories under a path. |

### `rex.layout`

Functions that build the `layout` table taken by `new_window`, `new_split`,
and `new_layer`. They only build tables and call nothing.

| Function | Description |
| --- | --- |
| `rex.layout.block{ flavor, options }` | One block. `flavor` names what to create, such as `com.superlogical.terminal.shell`. |
| `rex.layout.horizontal(ratio, a, b)` | Two layouts split horizontally. `ratio` is the share given to `a`, from 0 to 1. |
| `rex.layout.vertical(ratio, a, b)` | Two layouts split vertically. |

The options for a terminal are:

| Option | Description |
| --- | --- |
| `command` | A list with the program to run and its arguments. Without it, an interactive shell starts. |
| `shell` | The shell to use. `none` starts `command` directly. |
| `cwd` | The working directory. |
| `initial_input` | Text typed into the terminal once it starts. |
| `exit` | `{ on_completion = false }` keeps the terminal open after its program exits. |

## Events

See [Lua Events](https://www.superlogical.com/rex/docs/automate/lua-events.md). These work in scripts run with
`rex do` only.

### `rex.on(event, fn)`

Calls `fn(ctx, ev)` each time `event` happens. `ev` holds the event's fields.
`ctx` has `session_id`, and `block_id` for a block event.

The [events reference](https://www.superlogical.com/rex/docs/reference/events.md) lists the event names.

### `rex.wait(event[, filter][, timeout])`

Blocks until `event` happens, and returns `ev` and `ctx`. Returns `nil` and
`"timeout"` if `timeout` seconds pass first.

`filter` is a table of fields the event must match, or a function called as
`filter(ev, ctx)` that returns `true` for a match.

### `rex.sleep(seconds)`

Blocks for a number of seconds, which may be fractional.

### `rex.stop([code])`

Ends the script immediately. `rex do` exits with `code`, or 0.

## Hosts and Other Servers

### `rex.host{ ... }`

Declares another Rex server you connect to. Configuration file only.

| Field | Description |
| --- | --- |
| `label` | Required. The host's name. |
| `endpoints` | Required. A list of addresses, tried in order. |
| `endpoint` | A single address, in place of `endpoints`. |
| `id` | A stable ID, so the host keeps its identity when renamed. |
| `icon` | An icon name for clients to show. |

### `rex.servers()`

Returns the labels of the other servers available to the running action or
script, as a list.

### `rex.server(label)`

Returns a table with the same `session`, `block`, and `call` functions as
`rex`, whose calls go to that server. Raises an error for an unknown label.

Calls made through it don't fill in a `session_id` or `block_id`.

## Other Functions

Functions for running commands, timers, JSON, and logging. See
[Running Commands](https://www.superlogical.com/rex/docs/automate/lua-scripts.md#running-commands) and
[Timers](https://www.superlogical.com/rex/docs/automate/lua-scripts.md#timers).

### `rex.exec(argv[, opts])`

Runs a command, waits for it to exit, and returns a result table. Returns
`nil` and an error message only when the command never ran, such as when the
program doesn't exist.

```lua
local r, err = rex.exec({ "git", "status", "--short" }, { cwd = dir })
```

`argv` is a list of strings: the program, then each of its arguments. No shell
reads it, so arguments need no quoting. A program without a slash in its name
is looked up in the command's `PATH`.

`opts` is an optional table:

| Option | Description |
| --- | --- |
| `cwd` | The working directory. Defaults to Rex's own. |
| `env` | Environment variables to set, such as `{ GIT_DIR = "/repo/.git" }`. `false` removes one. |
| `clear_env` | `true` starts from an empty environment. `env` still applies. |
| `stdin` | A string written to the command's standard input. |
| `timeout` | Seconds until the command is sent `SIGTERM`, then `SIGKILL` two seconds later. |
| `stdout`, `stderr` | `false` discards the stream. Both are captured by default. |
| `text` | `false` keeps `\r\n` in the output on Windows. |

An option Rex doesn't know is ignored with a warning.

The result has:

| Field | Description |
| --- | --- |
| `code` | The exit status, or `-1` if a signal ended the command. |
| `signal` | The name of that signal, such as `SIGTERM`, or `nil`. |
| `stdout`, `stderr` | What the command printed. `""` when it printed nothing. |
| `timed_out` | `true` if `timeout` stopped the command. |
| `ok` | `true` if `code` is 0 and the command didn't time out. |

A non-zero exit is still a result, not an error. Check `ok`.

While an action in the configuration file waits for a command, the server
keeps handling keys and other actions. Everywhere else, the wait blocks the
script.

### `rex.exec.start(argv[, opts])`

Starts a command and returns a handle without waiting, or `nil` and an error
message. It takes the same arguments as `rex.exec`, and a few more options:

| Option | Description |
| --- | --- |
| `stdin` | `true` keeps standard input open for `h:write`. |
| `stdout`, `stderr` | A function called with each piece of output as it arrives. A piece may end mid-line. |
| `detach` | `true` lets the command outlive the work that started it. |

The handle has:

| Method | Description |
| --- | --- |
| `h:wait([seconds])` | Waits for the command to exit and returns the same result as `rex.exec`. Returns `nil` and `"timeout"` if `seconds` pass first. |
| `h:write(data)` | Writes to standard input. Returns `true`, or `nil` and an error message. |
| `h:close_stdin()` | Closes standard input. |
| `h:kill([signal])` | Sends a signal, `SIGTERM` by default. `signal` is a name, such as `SIGINT`, or a number. |
| `h:running()` | Returns whether the command is still running. |
| `h.pid` | The command's process ID. |

A command lasts as long as the work that started it. When an action, handler,
or timer returns, or a script's top level finishes, Rex stops the commands it
started. A command started with `detach = true` is never stopped this way.

### `rex.after(seconds, fn)`

Calls `fn(ctx)` once, after `seconds`, and returns a timer with the same
methods as [`rex.every`](#rexeveryseconds-fn)'s. `seconds` may be zero or
fractional.

`ctx` is the same table an action's `run` gets, with `ctx.origin` set to
`timer`. API calls default to the script's session and block.

### `rex.every(seconds, fn)`

Calls `fn(ctx)` every `seconds` until the timer is cancelled, and returns a
timer. `seconds` must be more than zero.

If the script is still busy when a tick comes, `fn` runs once when it is free,
and the missed ticks are dropped.

A timer has two methods:

| Method | Description |
| --- | --- |
| `t:cancel()` | Stops the timer. |
| `t:active()` | Returns `true` until the timer is cancelled or has fired for the last time. |

An error in `fn` is reported and the timer keeps running. A script can have at
most 1,024 timers at once. Creating one more stops the script with status 1.

### `rex.json.encode(value[, opts])`

Turns a Lua value into JSON text, or returns `nil` and an error message.
`opts.indent = true` indents the text by two spaces.

```lua
local text = rex.json.encode({ name = "work", panes = { 1, 2 } })
```

Tables convert the same way they do in an API call. A table whose keys are 1
to n becomes an array, and any other table, including an empty one, becomes
an object. A function, or a table that contains itself, can't be encoded.

### `rex.json.decode(text)`

Turns JSON text into a Lua value, or returns `nil` and an error message.
`null` becomes `nil`, and a `null` in an array is dropped.

### `rex.log([level, ]message)`

Writes a log message. `level` is `debug`, `info`, `warn`, or `error`, and
defaults to `info`. In a script the message is printed to the terminal.

## Script Values

Set in scripts run with `rex do` only.

| Value | Description |
| --- | --- |
| `rex.args` | The script's arguments, as a table. Empty when none were passed. |
| `rex.session_id` | The ID of the session the script runs against. |
| `rex.block_id` | The ID of the block the script runs against, or `nil`. |

---
title: "Events"
canonical_url: https://www.superlogical.com/rex/docs/reference/events
---

# Events

Rex emits an event whenever something changes: a window opens, a terminal
changes directory, a program exits, a session is destroyed. Events can be
handled in Lua with [`rex.on` and `rex.wait`](https://www.superlogical.com/rex/docs/automate/lua-events.md), or
streamed from the CLI with `rex events`.

Events come from three places:

| Kind | Describes | Lua name |
| --- | --- | --- |
| [Session events](#session-events) | Changes to one session's windows, blocks, and clients. | The event's type, such as `block_closed`. |
| [Terminal events](#terminal-events) | Things a terminal reports. | `terminal.` and the event, such as `terminal.bell`. |
| [Server events](#server-events) | Changes to the server as a whole. | The event's type, such as `session_created`. |

## Events in Lua

A handler is called with `ctx` and `ev`. The fields listed for each event on
this page are found directly on `ev`:

```lua
rex.on("terminal.child_exited", function(ctx, ev)
  print(ctx.block_id, ev.exit_code)
end)
```

Every `ev` also has these fields:

| Field | Description |
| --- | --- |
| `ev.type` | The event's type. For every terminal event this is `block_event`. |
| `ev.session_id` | The session the event happened in. Absent for most server events. |
| `ev.payload` | The event's own fields again, as a table of their own. |

A terminal event, and any other block event, adds:

| Field | Description |
| --- | --- |
| `ev.block_id` | The block the event came from. |
| `ev.creator_name` | The kind of block, such as `com.superlogical.terminal`. |
| `ev.name` | The event's name without the creator, such as `bell`. |

`ctx.session_id` and `ctx.block_id` hold the same IDs, and are what API calls
in the handler default to.

Register for `block_event` to receive every event from every block, and use
`ev.creator_name` and `ev.name` to tell them apart.

## Session Events

These describe one session. A script hears them for the session it runs
against, or for every session with `--all`.

| Event | When | Fields |
| --- | --- | --- |
| `window_created` | A window is created. | `window_id`, `label` |
| `window_closed` | A window is closed. | `window_id` |
| `window_label_changed` | A window is renamed. | `window_id`, `label` |
| `active_window_changed` | A different window becomes the active one. | `window_id` |
| `block_created` | A block is created. | `block_id` |
| `block_closed` | A block is closed. | `block_id` |
| `session_label_changed` | The session is renamed. | `label` |
| `session_view_changed` | The session's layout changes in any way. | `revision` |
| `client_connected` | A client attaches to the session. | `client_id`, `principal`, `connected_at` |
| `client_disconnected` | A client detaches from the session. | `client_id`, `principal` |
| `client_connection_changed` | An attached client's connection is lost or restored. | `client_id`, `principal`, `connection_state` |
| `creator_changed` | The kinds of block the session can create have changed. | |

`session_view_changed` follows almost every other event in this table, because
most of them change the layout. Handle it to react to any structural change,
and call `rex.session.view` to get the new layout.

## Terminal Events

These come from terminal blocks. In Lua, write them as `terminal.` followed by
the name, or in full as `com.superlogical.terminal.` followed by the name.

| Event | When | Fields |
| --- | --- | --- |
| `terminal.child_exited` | The terminal's program exits. | `exit_code`, and `error` when it is not zero |
| `terminal.pwd_changed` | The program reports a new working directory. | `pwd`, `owner` |
| `terminal.title_changed` | The terminal's title changes. | `title`, `owner` |
| `terminal.bell` | The terminal receives a bell character. | |
| `terminal.process_changed` | The foreground process changes. | `generation`, `foreground`, `ancestors` |
| `terminal.desktop_notification` | The program asks for a desktop notification. | `title`, `body` |
| `terminal.progress_report` | The program reports progress. | `state`, `progress` |
| `terminal.clipboard_written` | The program writes to the clipboard. | `text` |
| `terminal.program_status_changed` | A program reports or updates its status. | `id`, `record`, `reason` |
| `terminal.program_status_removed` | Program status records are removed. | `ids`, `reason` |

A few notes on these:

- `terminal.child_exited` is about the program the terminal started, which is
  usually your shell. It doesn't fire for each command you run inside a shell.
  A terminal started with a `command` fires it when that command exits.
- `pwd` is a URL such as `file://hostname/Users/you/src`, not a plain path. It
  is only reported by shells set up to report it.
- `owner` is a table with the `pid` and `name` of the process that made the
  change.
- `foreground` is a table describing the process in the foreground, with
  `name`, `cwd`, `pid`, and more. It can be `nil`.

## Server Events

These describe the server rather than one session. In Lua, a script hears them
when run with `--all`.

| Event | When | Fields |
| --- | --- | --- |
| `session_created` | A session is created. | `session`, a table with `session_id` and `label` |
| `session_destroyed` | A session is destroyed. | `session_id` |
| `client_joined` | A client connects to the server. | `client` |
| `client_left` | A client disconnects from the server. | `client_id` |
| `client_changed` | A connected client's details change. | `client` |
| `keymap_changed` | The configuration file is reloaded. | `generation`, `hash` |
| `hosts_changed` | The list of hosts changes. | `hash` |
| `tailscale_status_changed` | The server's Tailscale status changes. | |

`client` is a table with the client's `client_id`, `info` (its `kind`, `name`,
`platform`, and `version`), `principal`, and `connected_at`.

## Events on the CLI

`rex events --json` prints one JSON object per line. Each has a `type` and a
`payload` holding the fields listed on this page:

```json
{"type":"session_created","payload":{"session":{"session_id":"session:01a1...","label":"work"}}}
```

A session event arrives wrapped in a `session_event`, which adds the session
it belongs to. A terminal event is wrapped once more, in a `block_event`
(shown here on several lines):

```json
{"type":"session_event","payload":{
  "session_id":"session:01a1...",
  "event":{"type":"block_event","payload":{
    "block_id":"block:01a1...",
    "creator_name":"com.superlogical.terminal",
    "name":"child_exited",
    "payload":{"exit_code":2,"error":"exit status 2"}}}}}
```

Lua removes both layers of wrapping, which is why a handler sees `exit_code`
directly on `ev`.

What `rex events` prints depends on its scope:

| Command | Prints |
| --- | --- |
| `rex events` | Sessions being created, destroyed, and changed. |
| `rex events -s SESSION` | Everything in one session, including terminal events. |
| `rex events --all` | Everything, including clients joining and leaving. |

## Unknown Events

New versions of Rex add events and fields. Rex never checks an event name
against a list, so a handler can be registered for an event that only a newer
server sends. Ignore fields you don't recognize rather than failing on them.

---
title: "Privacy & Telemetry"
canonical_url: https://www.superlogical.com/rex/docs/reference/privacy
---

# Privacy & Telemetry

**Rex doesn't collect telemetry.** It has no usage analytics, no crash
reporting of its own, and no account or license checks.

This page lists every network connection Rex makes, when it makes it,
and what it sends, for each part of Rex. Programs you run inside Rex
aren't covered. They make their own connections, and we don't control
them.

**Rex is still in testing.** Rex changes quickly and this page could fall
behind. If anything here is wrong, please apply
[Hanlon's razor](https://en.wikipedia.org/wiki/Hanlon%27s_razor): assume
it's a mistake, not malice, and report it in the beta Discord so we can
fix it.

## Outbound Connections

This is the list of outbound connections that Rex may make.

| Connection      | When                                                | Where                             |
| --------------- | --------------------------------------------------- | --------------------------------- |
| Update check    | Rex for Mac, at launch and once a day               | Superlogical's update server      |
| Remote servers  | Only servers you add                                | The servers you add               |
| Crash reports   | Rex for iOS, if you share them or use TestFlight    | Apple, then Superlogical          |
| Package updates | Linux and WinGet, when your package manager updates | Superlogical's package repository |
| Tailscale       | Only if you turn it on                              | Tailscale's servers               |

## Rex for Mac

### Update Checks

Rex for Mac checks for new versions at launch and then once a day by
downloading a signed feed of releases from Superlogical's update server.
Update checks can't be turned off.

Each check sends only what any web request does:

- Your IP address.
- A user agent with the app's name and version, such as
  `Rex Beta/0.1.0 Sparkle/2.9.6`.

It doesn't send an account, a device identifier, a system profile, or
anything about how you use Rex. An update is downloaded only when you
choose to install it.

### The Local Server

Rex for Mac runs a Rex server on your Mac and talks to it over a Unix
socket, so that traffic never leaves your machine. The app starts it
without any network listeners or Tailscale.

### Remote Servers

Rex for Mac connects only to the servers you add. It stays connected to
them, and reconnects at launch, until you remove them. It doesn't look
for servers on your network.

## Rex for iOS

Rex for iOS doesn't collect telemetry of its own. Apple collects some
data for every app, and shares it with Superlogical only if you allow it.

### Crash Reports and Analytics

If **Share With App Developers** is on, in **Settings > Privacy &
Security > Analytics & Improvements**, Apple sends us:

- Crash reports, with the device model, iOS version, app version, and
  where in Rex's code the crash happened.
- Diagnostics such as hangs, slow launches, and heavy battery or disk use.
- App Store analytics, such as sessions and active devices. This data
  is combined with other users' so no one person can be identified.

None of this data includes any extra app-specific metadata such as
your server list, names, session layouts, terminal contents, etc. It is
only the generic app data that all App Store apps report. With
**Share With App Developers** off, Apple doesn't send us any of it.

Apple always reports how many times Rex was downloaded, as it does for
every app, whatever this setting is.

If you use a TestFlight build (a preview release), TestFlight sends us
crash reports and any feedback you submit, regardless of the **Share With
App Developers** setting. This is a requirement of TestFlight.

### Updates

Rex for iOS never checks for updates itself. The App Store or TestFlight
updates it.

### Remote Servers

Rex for iOS connects only to the servers you add, the same way Rex for
Mac does. It doesn't look for servers on your network.

## Rex Server (All Platforms)

With default settings, the Rex server and the `rex` CLI make no outbound
connections.

### Updates

The server never checks for updates itself. On Linux, your system's
package manager updates it along with your other packages, from
Superlogical's package repository. With Nix, it's updated from
Superlogical's servers when you update your flake. On Windows, WinGet
checks Superlogical's WinGet source and downloads from Superlogical's
servers, but only when a `winget` command names it with `--source rex`.

These requests send only what the package manager or Nix always sends,
such as your IP address and its user agent.

`winget list` and `winget upgrade` normally send each source the product
codes, names and publishers of the programs installed on your PC. Rex's
WinGet source declares those fields unsupported, so WinGet leaves them out
of its searches, and the source never learns what else you have
installed. It answers from the list of Rex releases, and doesn't record
the searches it receives.

### Remote Servers

The CLI connects to another server only when you name one, with
`--server`, `REX_SERVER`, `rex home`, or `rex hosts`.

### Built-in Tailscale

[Built-in Tailscale](https://www.superlogical.com/rex/docs/linux/remote-access.md#built-in-tailscale) is off
until you turn it on. When it's on, the server runs its own Tailscale
device, which connects to Tailscale like any other device does:

- Tailscale's coordination server, to sign in and join your tailnet.
- Tailscale's relay servers, and other devices on your tailnet.
- Your router, to open a port with UPnP, NAT-PMP, or PCP.
- Let's Encrypt, for the server's HTTPS certificate.

The Tailscale device also uploads its logs and client metrics to
Tailscale, as Tailscale's own apps do by default. These are Tailscale's
logs about the connection, not your terminal contents. If your tailnet's
admin turns on network flow logs, those go to Tailscale too.
[Tailscale's privacy policy](https://tailscale.com/privacy-policy) covers
this data. None of it goes to Superlogical.

### Your Machine's Tailscale

With Tailscale Serve or a tailnet address, Rex only asks the Tailscale
already installed on your machine who is connecting, over its local
socket, and makes no connections of its own.

---
title: "Installing on Linux"
canonical_url: https://www.superlogical.com/rex/docs/linux/installation
---

# Installing on Linux

Rex can run as a server on Linux so that clients can access it as a remote
host.

**IMPORTANT:** Rex on Linux is in testing. The install commands show below
once you've logged in with Discord.

## Quickstart

This section documents the recommended setup to get up and running quickly.
It reaches your own personal server from your clients over SSH, with nothing
else to set up. We recommend you read the later sections, and
[Remote Access](https://www.superlogical.com/rex/docs/linux/remote-access.md#ssh), to understand the details and
other access options.

1. On the Linux machine, install Rex as [Install](#install) shows.

2. Start your personal server at boot, and keep it running while you're
   logged out. This needs no sudo, run it as the user you want to connect
   with:

   ```sh
   rex server install
   ```

   Without systemd, such as on Alpine, skip this step. A server that the
   first connection starts then runs until the machine restarts.

3. Check that you can log in with your SSH key from your client, and that
   SSH finds `rex` there:

   ```sh
   ssh alice@build.example.com rex version
   ```

4. Connect from your client:

   ```sh
   rex -S ssh://alice@build.example.com ls
   ```

   In Rex for Mac, choose **Add Remote Host…** and enter
   `ssh://alice@build.example.com`.

If several people need Rex on one machine, see
[Server Modes](https://www.superlogical.com/rex/docs/linux/server-modes.md).

## System Requirements

| Requirement  | Supported                                                                 |
| ------------ | ------------------------------------------------------------------------- |
| Architecture | x86-64, ARM64                                                             |
| C library    | glibc 2.26 or later (Amazon Linux 2, Ubuntu 18.04, Debian 10, RHEL 8, or newer), or musl on Alpine 3.21 or later |
| Init system  | systemd, or OpenRC on Alpine, for the shared server and the login server. The `rex` command runs without either. |
| Packages     | `.deb` (Debian, Ubuntu), `.rpm` (Fedora, RHEL and its rebuilds, openSUSE), `.pkg.tar.zst` (Arch Linux and derivatives), `.apk` (Alpine), Nix |

The Alpine package holds the musl build of Rex. It's dynamically linked,
not static, because Rex loads PAM at run time. On other distributions,
including other musl ones, install the binary alone with `--tarball`, which
picks the musl build on a musl system.

## Install


The **install script (`install.sh`)** is a convenience that adds the Rex
repository to apt, dnf, zypper, pacman, or apk and installs the `rex`
package, so updates arrive with your normal system updates. Running it
again upgrades Rex, as [Upgrading](https://www.superlogical.com/rex/docs/linux/upgrading.md) covers. To see what it
would do first, add `-s -- --dry-run` after `sh`. On NixOS and other
distributions without a Rex repository, it explains the options and exits.

``

| Option           | Effect                                                                   |
| ---------------- | ------------------------------------------------------------------------ |
| `--channel NAME` | Install from `stable`, `preview`, or `snapshot`.                         |
| `--tarball`      | Install only the `rex` binary, into `/usr/local/bin`, with no services. Needs glibc 2.26 or later, or musl, where it installs the musl build. Run the script again to update. |
| `--dry-run`      | Print every command without running it.                                  |

Pass options after `sh -s --`. For example, to install only the binary,
with no services:

```sh
curl -fsSL https://pkgs.superlogical.com/rex/install.sh | sh -s -- --tarball
```

## Channels

Rex publishes to three channels:

| Channel    | What it gets                         |
| ---------- | ------------------------------------ |
| `stable`   | Tagged releases (none right now)                      |
| `preview`  | Tagged previews of the next release  |
| `snapshot` | A frequent, likely unstable build              |

The install script defaults to `stable` but until the first stable
release exists, will fall back to `preview` and say so in the output.

To switch channels later, change the channel name in the repository file:
`Suites:` in `/etc/apt/sources.list.d/rex.sources`, or the `name` and
`baseurl` lines in `/etc/yum.repos.d/rex.repo` or zypper's repository, or
the `Server` line in `/etc/pacman.d/rex.conf`. On Alpine, change it in
`/etc/apk/repositories.d/rex.list` if the install script set up the
repository, or in the Rex line in `/etc/apk/repositories` if you added it
yourself, then run `apk update`.

Moving from snapshot to preview or stable upgrades Rex. Moving back to
snapshot is a downgrade, which you have to ask for. See
[Switching Channels and Downgrading](https://www.superlogical.com/rex/docs/linux/upgrading.md#switching-channels-and-downgrading).

## Package Contents

The `rex` package installs:

| Path                                | What it is                                                         |
| ----------------------------------- | ------------------------------------------------------------------ |
| `/usr/bin/rex`                      | The server and the CLI, in one binary                              |
| `rex.service`                       | One shared server, run as the `rex` account. Not enabled.          |
| `rex.socket`                        | Starts `rex.service` when a client first connects. Not enabled.    |
| `rex-login-server.service`          | One server per account, after a login. Not enabled.                |
| `rex-login-server.socket`           | Starts `rex-login-server.service` when a client first connects. Not enabled. |
| `rex` account and group             | A locked system account, with its home at `/var/lib/rex`           |
| `/etc/rex/accounts.jsonc`           | The login server's accounts. It ships empty, which admits nobody.  |
| `/etc/pam.d/rex-login-server`       | The login server's PAM policy, for your distribution               |
| `xterm-rex` terminfo                | Rex's terminal type                                                |
| `/etc/ssh/ssh_config.d/50-rex.conf` | Falls back to `xterm-256color` when you SSH from Rex to a host that doesn't know `xterm-rex` |

To run a server, enable one of the four units. Each socket starts its
service the first time a client connects, instead of at boot.
[Server Modes](https://www.superlogical.com/rex/docs/linux/server-modes.md) helps you choose.

On Alpine, OpenRC scripts at `/etc/init.d/rex` and
`/etc/init.d/rex-login-server` take the place of the two services. The
package never ships `/etc/conf.d/rex` or `/etc/conf.d/rex-login-server`,
so your changes to the services go there.

On Arch, the package creates the `rex` account through
`/usr/lib/sysusers.d/rex.conf`, and its home through
`/usr/lib/tmpfiles.d/rex.conf`.

Upgrades keep your changes to the accounts file and the PAM policy. See
[Keeping Your Changes](https://www.superlogical.com/rex/docs/linux/upgrading.md#keeping-your-changes).

### The SSH Fallback

`50-rex.conf` sets `TERM` with ssh's `SetEnv` keyword, so what it does
depends on your OpenSSH version:

| OpenSSH        | What happens                                                             |
| -------------- | ------------------------------------------------------------------------ |
| 8.7 or later   | ssh sends `xterm-256color` to hosts that don't know `xterm-rex`.         |
| 7.8 to 8.6     | ssh reads the fallback but still sends `xterm-rex`. RHEL 8 has 8.0.      |
| Older than 7.8 | ssh rejects any configuration that contains the fallback, so every ssh command fails. Amazon Linux 2 has 7.4, Ubuntu 18.04 has 7.6. |

`rex terminfo setup`, with or without `--system`, checks the version
first. On an ssh older than 7.8, it installs the terminfo entry, skips the
fallback, says why, and exits with status 1. The package can't check, so
it installs `50-rex.conf` everywhere. That's safe on Ubuntu 18.04 only
because its ssh never reads the file, as
[Debian and Ubuntu](#debian-and-ubuntu) explains.

To check that ssh still parses its configuration, run:

```sh
ssh -G example.com >/dev/null && echo ok
```

## Debian and Ubuntu

Debian and Ubuntu ship an unrelated package also named `rex`: (R)?ex, a
Perl automation framework. The install script and the **Debian** tab pin
the Rex repository above it, so apt installs Rex, and replaces (R)?ex if
it's already installed.

If `rex -v` prints `(R)?ex`, your machine's own apt preferences outranked
the pin. Run `apt-cache policy rex` to see which rule won, fix it, then run
the install script again.

On Ubuntu 18.04, `/etc/ssh/ssh_config` has no `Include` line, so ssh never
reads `/etc/ssh/ssh_config.d/50-rex.conf`. Its OpenSSH 7.6 is too old for
[the SSH fallback](#the-ssh-fallback). **Don't add an `Include` line for
`/etc/ssh/ssh_config.d` on Ubuntu 18.04.** It breaks ssh for every account
on the machine.

## Fedora

Fedora ships (R)?ex, the same unrelated tool, as `Rex`. Both packages own
`/usr/bin/rex`, so remove it with `sudo dnf remove Rex` before you install
Rex.

## Amazon Linux 2

Amazon Linux 2 reached the end of its support on 2026-06-30, and its rpm
and systemd are too old for the Rex package. So the install script installs
only the `rex` binary there, into `/usr/local/bin`, as `--tarball` does,
and prints a notice saying so. The **Fedora** tab doesn't work on Amazon
Linux 2, so use the install script. Amazon Linux 2023 uses the Rex
repository, like Fedora.

There are no services and no login server. To start a
[personal server](https://www.superlogical.com/rex/docs/linux/server-modes.md#personal-server) for your account,
run:

```sh
rex server start
```

To update Rex, run the install script again. A server that's already
running keeps the old version until it stops.

If an earlier run of the script left `/etc/yum.repos.d/rex.repo` behind,
the script removes it. Its OpenSSH 7.4 is too old for
[the SSH fallback](#the-ssh-fallback), so `rex terminfo setup` installs
only the terminfo entry.

## Arch Linux

On Arch, installing a package also upgrades the system, because pacman
doesn't support partial upgrades. So the **Arch Linux** tab and the install
script both run `pacman -Syu`. The script also passes `--needed`, so running
it again with Rex up to date doesn't reinstall Rex or restart its services.

Derivatives that set `ID_LIKE=arch` in `/etc/os-release`, such as Manjaro,
EndeavourOS, CachyOS, and Garuda, install the same way. SteamOS doesn't,
because its root file system is read-only. Omarchy needs one change, which
[Omarchy](#omarchy) covers.

The repository lists one version of Rex at a time, the newest on its
channel. Older packages stay in the channel's directory, so to go back to
one, pass its URL to `pacman -U`:

```sh
sudo pacman -U https://pkgs.superlogical.com/rex/arch/snapshot/x86_64/rex-VERSION-1-x86_64.pkg.tar.zst
```

If you installed `rex-bin` from the AUR, remove it first with
`sudo pacman -R rex-bin`. It conflicts with `rex`, so the install script
stops until it's gone. Unlike `rex`, removing `rex-bin` doesn't stop or
disable the Rex units, so stop and disable any you enabled first:

```sh
sudo systemctl disable --now rex.service rex.socket \
  rex-login-server.service rex-login-server.socket
```

### Omarchy

Omarchy upgrades the system only through `omarchy update`, and it cancels
`pacman -Syu` with a "Woah partner" message. The install script handles
this. On Omarchy, it installs Rex without upgrading anything else, and
`omarchy update` keeps Rex up to date from then on.

To install by hand, run the **Arch Linux** tab without its last line,
`sudo pacman -Syu rex`, then run:

```sh
omarchy update
sudo pacman -S --needed rex
```

`omarchy refresh pacman` and `omarchy channel set` replace
`/etc/pacman.conf`, which removes the Rex repository. Rex stays installed
but stops getting updates until you run the install script again.

## Alpine

Alpine has no `sudo` by default, so run the commands on these pages as
root. Alpine uses OpenRC instead of systemd. Where a page gives a
`systemctl` command, use the OpenRC form:

| On systemd                                             | On Alpine                                                          |
| ------------------------------------------------------ | ------------------------------------------------------------------ |
| `sudo systemctl enable --now rex.service`              | `rc-update add rex`, then `rc-service rex start`                   |
| `sudo systemctl enable --now rex-login-server.service` | `rc-update add rex-login-server`, then `rc-service rex-login-server start` |
| `sudo systemctl reload rex-login-server.service`       | `rc-service rex-login-server reload`                               |
| `sudo systemctl disable --now rex.service`             | `rc-service rex stop`, then `rc-update del rex`                    |
| `sudo systemctl edit rex.service`                      | Set `command_args` in `/etc/conf.d/rex`                            |
| `journalctl -u rex.service -f`                         | `tail -f /var/log/rex/server.log`                                  |
| `journalctl -u rex-login-server.service`               | `cat /var/log/rex-login-server.log`                                |
| `sudo usermod --append --groups rex alice`             | `addgroup alice rex`                                               |

OpenRC has no socket activation, so `rex.socket` and
`rex-login-server.socket` have no Alpine form. Use the services instead.

`/etc/conf.d/rex` is a shell script that OpenRC reads before the service's
own script, so a `command_args` set there replaces the server's command
line. [Shared Server](https://www.superlogical.com/rex/docs/linux/shared-server.md) and
[Remote Access](https://www.superlogical.com/rex/docs/linux/remote-access.md) show what to put in it.

The services need OpenRC, which a full Alpine install has. Containers
often don't. To install it, run `apk add openrc`.

On musl, `who` and `last` don't list Rex sessions, because musl keeps no
utmp or wtmp. `lastlog` records logins as usual.

## Nix and NixOS

Rex has a flake for each channel, at
`https://pkgs.superlogical.com/rex/nix/<channel>.tar.gz`. It packages the
release binary rather than building Rex from source, for x86-64 and ARM64
Linux. The **Nix** tab installs it into your profile. Older versions of
Nix call `nix profile add` by its old name, `nix profile install`.

To try Rex without installing it:

```sh
nix run https://pkgs.superlogical.com/rex/nix/snapshot.tar.gz -- --help
```

The channel URL always serves the newest release on its channel, but
`flake.lock` records the exact release, so a lock keeps working after newer
releases come out. To move to the newest one, run `nix flake update rex`.

### The NixOS Module

On NixOS, add the flake as an input and import `rex.nixosModules.default`.
The module runs Rex as a systemd service in one of two modes, the same two
the packages ship:

- `services.rex.enable` runs one shared server as the `rex` account, on
  `/run/rex/server.sock`. Members of the `rex` group can connect.
- `services.rex.loginServer.enable` runs one root process that identifies
  each caller, maps them to an account, and runs a server per account as
  that account. It listens on `/run/rex/login-server.sock`.

They can't both be on. Either mode also installs `rex` for every user, puts
Rex's terminal type where the service can find it, and adds the SSH
fallback to `/etc/ssh/ssh_config`.

`services.rex.socketActivation` and
`services.rex.loginServer.socketActivation` start the server the first time
a client connects, through `rex.socket` or `rex-login-server.socket`,
instead of at boot. Each needs its mode's `enable`, and
`services.rex.socketActivation` also needs a Unix `socketPath`. The module
keeps the socket unit and the server's command line in agreement. With
`tailscaleServe.enable`, the socket unit holds the Serve socket too, so a
request through Tailscale Serve also starts the server.

Unlike the packages' services, the module's services require their
sockets, so stopping `rex.socket` alone stops the server too. Turning
either option on or off restarts a running server at the next
`nixos-rebuild switch`, which ends every session on a shared server.

A shared server that one person can use:

```nix
{
  inputs.rex.url = "https://pkgs.superlogical.com/rex/nix/snapshot.tar.gz";

  outputs = { nixpkgs, rex, ... }: {
    nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        rex.nixosModules.default
        {
          services.rex.enable = true;
          users.users.alice = {
            isNormalUser = true;
            extraGroups = [ "rex" ];
          };
        }
      ];
    };
  };
}
```

A login server that lets Alice use her own account over Tailscale Serve.
The module writes the accounts to `/etc/rex/accounts.jsonc`, but doesn't
create them:

```nix
{
  services.tailscale.enable = true;
  users.users.alice.isNormalUser = true;

  services.rex.loginServer = {
    enable = true;
    accounts.alice.principals = [ "tailscale:alice@example.com" ];
    tailscaleServe.enable = true;
  };
}
```

The host must be logged in to Tailscale, with HTTPS enabled for the
tailnet. Changes to the accounts reload the login server without ending
sessions. Connections whose account is no longer allowed are closed.

### The home-manager Module

The home-manager module sets up Rex for your own account. It works on any
Linux with Nix and systemd, such as Fedora or Amazon Linux 2023, not only
on NixOS. Add the flake as an input and import
`rex.homeManagerModules.default`. It's also exported as
`rex.homeModules.default`, the name some setups expect.

A personal server that keeps running between sessions:

```nix
{
  inputs.rex.url = "https://pkgs.superlogical.com/rex/nix/snapshot.tar.gz";

  outputs = { home-manager, nixpkgs, rex, ... }: {
    homeConfigurations.alice = home-manager.lib.homeManagerConfiguration {
      pkgs = nixpkgs.legacyPackages.x86_64-linux;
      modules = [
        rex.homeManagerModules.default
        {
          home.username = "alice";
          home.homeDirectory = "/home/alice";
          home.stateVersion = "26.05";

          programs.rex.enable = true;
          services.rex.enable = true;

          # home-manager writes ~/.ssh/config only when this is on, and
          # programs.rex adds the SSH fallback to it.
          programs.ssh.enable = true;
        }
      ];
    };
  };
}
```

The module has two parts:

| Option                | Effect                                                                   |
| --------------------- | ------------------------------------------------------------------------ |
| `programs.rex.enable` | Installs `rex`, writes the `xterm-rex` terminfo entry to `~/.terminfo`, and adds the SSH fallback to `~/.ssh/config`. `programs.rex.config` writes `~/.config/rex/init.lua`. |
| `services.rex.enable` | Runs a [personal server](https://www.superlogical.com/rex/docs/linux/server-modes.md#personal-server) as your account, in a systemd user service named `rex`. Also turns on `programs.rex`. |

The SSH fallback needs `programs.ssh.enable = true`. Without it, the module
prints a warning, and you can run `rex terminfo setup` once instead.
Running `rex terminfo setup` with the module in place is harmless.
It sees the guard home-manager already includes and
writes nothing to your ssh configuration, and `rex terminfo status` reports
that guard as in effect.

Unlike `rex terminfo setup`, the module doesn't check your OpenSSH
version. On an ssh older than 7.8, set `programs.rex.sshGuard = false`,
because [the SSH fallback](#the-ssh-fallback) breaks every ssh command
there.

The service uses the same socket as every `rex` command, so `rex ls`,
`rex server status`, and `rex server stop` all reach it. systemd restarts
it if it fails, but not after `rex server stop`. To start it again, run
`systemctl --user start rex`. It returns once the server is accepting
connections, so a `rex` command you run right after it works. Its log is in
`journalctl --user -u rex`.

The service starts when you log in and stops when you log out, which ends
every session. `home-manager switch` warns about this each time. To start it
at boot and keep it running while you're logged out, run this once:

```sh
loginctl enable-linger
```

For remote access, `services.rex.tailscale.enable` turns on
[built-in Tailscale](https://www.superlogical.com/rex/docs/linux/remote-access.md#built-in-tailscale).
`authKeyFile` needs systemd 250 or later (Fedora 36, Amazon Linux 2023).
The other options, such as `extraListen`, `blockDirs`, and `maxSessions`,
are described in `home-manager-module.nix`, which comes with the flake.

On macOS, turn on only `programs.rex`. The Rex app runs the server, so
`services.rex` stops the build with an error there.

The [shared server](https://www.superlogical.com/rex/docs/linux/shared-server.md) and the
[login server](https://www.superlogical.com/rex/docs/linux/login-server.md) need root, so home-manager can't run
them. Use the NixOS module, or the packages.

---
title: "Installing on FreeBSD"
canonical_url: https://www.superlogical.com/rex/docs/freebsd/installation
---

# Installing on FreeBSD

Rex can run as a server on FreeBSD so that clients can access it as a
remote host.

**IMPORTANT:** Rex on FreeBSD is in testing. The install commands show
below once you've logged in with Discord.

The other server pages, starting with [Server Modes](https://www.superlogical.com/rex/docs/linux/server-modes.md),
apply to FreeBSD too. Where they give a Linux command or path, use the one
in [Commands and Paths](#commands-and-paths).

## Quickstart

This section documents the recommended setup to get up and running quickly.
It reaches your own personal server from your clients over SSH, with nothing
else to set up. We recommend you read the later sections, and
[Remote Access](https://www.superlogical.com/rex/docs/linux/remote-access.md#ssh), to understand the details and
other access options.

1. On the FreeBSD machine, install Rex as [Install](#install) documents.

2. Check that you can log in with your SSH key from your client, and that
   SSH can access `rex` there:

   ```sh
   ssh alice@build.example.com rex version
   ```

3. Connect from your client:

   ```sh
   rex -S ssh://alice@build.example.com ls
   ```

   In Rex for Mac, choose **Add Remote Host…** and enter
   `ssh://alice@build.example.com`.

The first connection starts your personal server, and it keeps running
after you log out, until you stop it or the machine restarts. If several
people need Rex on one machine, see [Server Modes](https://www.superlogical.com/rex/docs/linux/server-modes.md).

## System Requirements

| Requirement  | Supported                                                                     |
| ------------ | ----------------------------------------------------------------------------- |
| Release      | FreeBSD 14.0 or later                                                         |
| Architecture | x86-64, ARM64                                                                 |
| Dependencies | None. Rex uses only the base system.                                          |
| Services     | rc.d, for the shared server and the login server. The `rex` command runs without them. |
| Packages     | `.pkg`, from the Rex repository                                               |

One package serves FreeBSD 14 and every later release.

## Install


The **install script (`install.sh`)** adds the Rex repository to pkg and
installs the `rex` package, so `pkg upgrade` updates Rex along with your
other packages. It takes the same options as on
[Linux](https://www.superlogical.com/rex/docs/linux/installation.md#install), such as `--channel`, `--tarball`, and
`--dry-run`. Pass them after `sh -s --`.

FreeBSD has no `sudo` by default, so run the **pkg** tab's commands as
root. The first, `umask 022`, makes the files they create readable by every
user, whatever root's umask is. The install script needs root too, or `sudo`
if you've installed it.

FreeBSD has no curl either, so the commands download with FreeBSD's own
`fetch`.

If a channel has no FreeBSD package yet, the script installs only the
`rex` binary into `/usr/local/bin`, as `--tarball` does, with no services.
Run the script again once the channel has a package, and it switches to the
package.

pkg trusts the Rex repository by the fingerprint of its signing key, in
`/usr/local/etc/pkg/fingerprints/rex`. If the key changes, run the install
script again to trust the new one.

## Channels

FreeBSD has the same three [channels](https://www.superlogical.com/rex/docs/linux/installation.md#channels) as
Linux. To switch, change the channel name in the `url` line of
`/usr/local/etc/pkg/repos/rex.conf`, then update the catalogue:

```sh
pkg update -r rex
```

Moving from snapshot to preview or stable upgrades Rex. Moving back to
snapshot is a downgrade, which you have to ask for. See
[Upgrading on FreeBSD](https://www.superlogical.com/rex/docs/linux/upgrading.md#freebsd).

## Package Contents

The `rex` package installs:

| Path                                    | What it is                                                         |
| --------------------------------------- | ------------------------------------------------------------------ |
| `/usr/local/bin/rex`                    | The server and the CLI, in one binary                              |
| `/usr/local/etc/rc.d/rex`               | One shared server, run as the `rex` account. Not enabled.          |
| `/usr/local/etc/rc.d/rex-login-server`  | One server per account, after a login. Not enabled.                |
| `rex` account and group                 | A locked system account with the ID 729, or the next free one, and its home at `/var/db/rex` |
| `/usr/local/etc/rex/accounts.jsonc`     | The login server's accounts. It ships empty, which admits nobody.  |
| `/usr/local/etc/pam.d/rex-login-server` | The login server's PAM policy                                      |
| `xterm-rex` terminfo                    | Rex's terminal type, in `/usr/local/share/terminfo`                |

Unlike the Linux packages, it has no SSH fallback, because FreeBSD's
`/etc/ssh/ssh_config` reads no other files. See
[Terminfo and SSH](#terminfo-and-ssh).

Upgrades keep your changes to the accounts file, but replace the PAM
policy. See [Upgrading on FreeBSD](https://www.superlogical.com/rex/docs/linux/upgrading.md#freebsd). To see the
package's setup notes again, run `pkg info -D rex`.

## Commands and Paths

The other server pages give systemd commands. On FreeBSD, use these
instead:

| On systemd                                             | On FreeBSD                                                         |
| ------------------------------------------------------ | ------------------------------------------------------------------ |
| `sudo systemctl enable --now rex.service`              | `sysrc rex_enable=YES`, then `service rex start`                   |
| `sudo systemctl enable --now rex-login-server.service` | `sysrc rex_login_server_enable=YES`, then `service rex-login-server start` |
| `sudo systemctl reload rex-login-server.service`       | `service rex-login-server reload`                                  |
| `sudo systemctl restart rex.service`                   | `service rex restart`                                              |
| `sudo systemctl disable --now rex.service`             | `service rex stop`, then `sysrc rex_enable=NO`                     |
| `sudo systemctl edit rex.service`                      | Set `rex_flags` with `sysrc`. See [Settings](#settings).           |
| `journalctl -u rex.service`                            | `grep ' rex\[' /var/log/messages`                                  |
| `journalctl -u rex-login-server.service`               | `grep ' rex-login-server\[' /var/log/messages`                     |
| `sudo usermod --append --groups rex alice`             | `pw groupmod rex -m alice`                                         |
| `sudo usermod --shell /bin/bash rex`                   | `pw usermod rex -s /bin/sh`                                        |

FreeBSD has no socket activation, so `rex.socket` and
`rex-login-server.socket` have no FreeBSD form. Use the services instead.

Files are in different places too:

| On Linux                      | On FreeBSD                              |
| ----------------------------- | --------------------------------------- |
| `/usr/bin/rex`                | `/usr/local/bin/rex`                    |
| `/run/rex`                    | `/var/run/rex`                          |
| `/var/lib/rex`                | `/var/db/rex`                           |
| `/etc/rex/accounts.jsonc`     | `/usr/local/etc/rex/accounts.jsonc`     |
| `/etc/pam.d/rex-login-server` | `/usr/local/etc/pam.d/rex-login-server` |
| `/etc/nologin`                | `/var/run/nologin`                      |
| `/run/user/<uid>/rex`         | `~/.local/state/rex/run`                |

Both services log to syslog, which writes to `/var/log/messages` by
default. The servers that the login server starts for each account log to
`/var/run/rex/accounts/<account>/server.log` instead.

If a server exits for any reason, its service starts it again 5 seconds
later. Only `service rex stop` stops it for good. `service rex start`
returns as soon as the server's process exists, which can be a moment
before it accepts connections.

## Settings

The services read their settings from `/etc/rc.conf`. Set them with
`sysrc`, then restart the service to apply them. Upgrades replace the rc.d
scripts but never change `/etc/rc.conf`. This example raises the shared
server's session limit:

```sh
sysrc rex_flags="--max-sessions 128"
service rex restart
```

| Setting                        | Default                             | Effect                                         |
| ------------------------------ | ----------------------------------- | ---------------------------------------------- |
| `rex_flags`                    | None                                | More options for `rex server run`              |
| `rex_umask`                    | `0007`                              | The file mode mask for the server and every program it starts |
| `rex_user`                     | `rex`                               | The account the server runs as                 |
| `rex_group`                    | `rex`                               | The group that may use the socket              |
| `rex_runtime_dir`              | `/var/run/rex`                      | The directory that holds the socket            |
| `rex_state_dir`                | `/var/db/rex`                       | The server's data                              |
| `rex_login_server_flags`       | None                                | More options for `rex login-server`            |
| `rex_login_server_accounts`    | `/usr/local/etc/rex/accounts.jsonc` | The accounts file                              |
| `rex_login_server_runtime_dir` | `/var/run/rex`                      | The login server's socket and each account server's directory |

`rex_flags` and `rex_login_server_flags` add options to the service's
command line. A systemd drop-in replaces the command line instead, so where
another page shows a whole `ExecStart` line, put only the options it adds
in the flags. The comment at the top of each rc.d script explains every
setting.

The shared server can run as another account instead of `rex`.
This example runs it as `alice`:

```sh
sysrc rex_user=alice rex_group=alice
service rex restart
```

The files the server wrote before the change still belong to the old
account. On the next start, the service gives them to the new one and prints
a line for each directory it changes:

```
Giving the files in /var/run/rex to alice:alice.
Giving the files in /var/db/rex to alice:alice.
```

The server keeps its saved sessions, and the programs in them now run as
`alice`. Changing back to `rex` works the same way.

## Terminfo and SSH

Programs in Rex terminals see `TERM=xterm-rex`, and the package installs
that terminal type for every account. ssh passes `TERM` on to other hosts,
which usually don't know it. To make ssh send `xterm-256color` to those
hosts instead, for every account, run as root:

```sh
rex terminfo setup --system
```

FreeBSD's `/etc/ssh/ssh_config` reads no other files, so this also adds a
marked `Include` line to it. `rex terminfo remove --system` takes the line
out again. To set it up for your own account only, run
`rex terminfo setup`.

---
title: "Installing on Windows"
canonical_url: https://www.superlogical.com/rex/docs/windows/installation
---

# Installing on Windows

Rex can run as a server on Windows so that clients can access it as a
remote host.

**IMPORTANT:** Rex on Windows is in testing and is currently open to
a subset of testers in Discord. We will extend testing to more people
soon.

On Windows, every server is a personal server, which runs as your own
account. You can configure the Rex server to start at boot or at sign-in,
as [Running on Windows](https://www.superlogical.com/rex/docs/windows/running.md) explains.
[Server Modes](https://www.superlogical.com/rex/docs/linux/server-modes.md),
[Shared Server](https://www.superlogical.com/rex/docs/linux/shared-server.md) and
[Login Server](https://www.superlogical.com/rex/docs/linux/login-server.md) _do not apply to Windows_.

## System Requirements

| Requirement  | Supported                                                                 |
| ------------ | ------------------------------------------------------------------------- |
| Release      | Windows 10 1809 or later                                                  |
| Architecture | x86-64, ARM64                                                             |
| Packages     | WinGet, MSI, or Zip

## Install

Rex isn't signed yet, so while it's in testing, Windows may warn that the
publisher is unknown.

Every installation scenario installs `rex.exe` and puts it on your PATH,
but **does not automatically start a server or install a Windows Service.**
You must do this manually with `rex server install` after Rex is installed.

### WinGet (Recommended)

WinGet is the recommended way to install Rex Server. You can install
via WinGet from either a Zip source (default) or MSI:


Administrator rights are required to add a new WinGet source, so run
the first line in a terminal with "Run as Administrator" rights.

The **WinGet** tab installs Rex for your own account, with no administrator
rights. The **WinGet (MSI)** tab installs it for the whole machine instead,
as explained in [MSI](#msi).

After installation, `rex` is added to your `PATH`. PATH changes require
opening a new terminal, so open a new terminal to verify this.

### MSI


The MSI installs Rex for every account, into `%ProgramFiles%\Rex Server`,
and adds that folder to the system PATH. It needs administrator rights.

The download above is the x86-64 MSI. On ARM64, use the **WinGet (MSI)**
tab or the [zip](#zip). Open the MSI to run its setup wizard, or install
it without a window from a terminal opened with **Run as administrator**:

```powershell
msiexec /i rex-windows-amd64.msi /qn
```

To install somewhere other than `%ProgramFiles%\Rex Server`, add
`INSTALLDIR="D:\Tools\Rex Server"`.

### Zip

The zip holds Rex's files with no installer.

{#if download}

Download the newest preview for your machine:

<ul>
{#each download.zips as zip}
<li><a href={zip.href}>{zip.filename}</a> ({zip.arch})</li>
{/each}
</ul>

{:else}

The downloads show here for testers with the Windows role.

{/if}

If you are using the Zip, it is expected that you know how to move it into
a proper location and add `rex.exe` to your `PATH`.

## Channels

Rex publishes to three channels, each with its own WinGet package:

| Channel    | Package                           | What it gets                         |
| ---------- | --------------------------------- | ------------------------------------ |
| `stable`   | `Superlogical.RexServer`          | Tagged stable releases      |
| `preview`  | `Superlogical.RexServer.Preview`  | Tagged previews of the next release  |
| `snapshot` | `Superlogical.RexServer.Snapshot` | Development builds   |

The install commands use `preview`. Until the first stable release,
`winget install Superlogical.RexServer` fails, because WinGet treats a
channel with no release as a package that doesn't exist.

To switch channels, remove the package you have, as
[Removing Rex](#removing-rex) shows, then install the other one with
`--source rex`.

## Package Contents

All packages have the same set of files:

| File                      | What it is                                                     |
| ------------------------- | -------------------------------------------------------------- |
| `rex.exe`                 | The server and the CLI, in one binary                          |
| `OpenConsole.exe`         | The console host from Windows Terminal |
| `LICENSE-OpenConsole.txt` | The license of `OpenConsole.exe`                               |

Each way of installing puts them in its own folder:

| Package | Folder                                                                  | Added to    |
| ------- | ----------------------------------------------------------------------- | ----------- |
| WinGet  | `%LocalAppData%\Microsoft\WinGet\Packages\Superlogical.RexServer.Preview_…` | User PATH   |
| MSI     | `%ProgramFiles%\Rex Server`                                             | System PATH |
| Zip     | The folder you extract it into                                          | User PATH, once you add it |

## Removing Rex

**Run `rex server uninstall` before you remove the package.** Otherwise the
Windows Service or sign-in entry that `rex server install` made is left
pointing at a `rex.exe` that's gone. Removing the service needs a terminal
opened with **Run as administrator**.

Removing the package doesn't stop a server that's running, so stop it with
`rex server stop`. Then remove the package. For the zip, use a normal
terminal, because WinGet won't remove it from one opened with **Run as
administrator**:

```powershell
winget uninstall --id Superlogical.RexServer.Preview --source rex
```

For an MSI you installed yourself, remove "Rex Server" in
**Add or Remove Programs** instead.

For a zip, delete its folder and take it out of your user PATH.

WinGet leaves the zip's folder on your user PATH, though the folder is
gone. You can take it out of your PATH by hand.

Your configuration, `%AppData%\rex\init.lua`, and Rex's state and logs, in
`%LocalAppData%\rex`, stay. Delete them to remove every trace of Rex.

---
title: "Running on Windows"
canonical_url: https://www.superlogical.com/rex/docs/windows/running
---

# Running on Windows

On Windows, every Rex server can only login as a single account:
the account that installs the Windows service.
[Server Modes](https://www.superlogical.com/rex/docs/linux/server-modes.md),
[Shared Server](https://www.superlogical.com/rex/docs/linux/shared-server.md) and
[Login Server](https://www.superlogical.com/rex/docs/linux/login-server.md) are for Linux and FreeBSD, and don't
apply here.

## Quickstart

This section documents the recommended setup to get up and running quickly.
It purposely omits many details about how everything works. We recommend
you read the later sections in detail to understand.

1. Install Rex with WinGet, as
   [Installing on Windows](https://www.superlogical.com/rex/docs/windows/installation.md#winget-recommended)
   shows.

2. Open a terminal with **Run as administrator**, and install and start
   Microsoft's OpenSSH Server:

   ```powershell
   Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
   Set-Service sshd -StartupType Automatic
   Start-Service sshd
   ```

3. In the same terminal, add your public key, such as the contents
   of `~/.ssh/id_ed25519.pub`. For an administrator account, sshd reads
   it from this file, and only if only administrators can change it:

   ```powershell
   Add-Content C:\ProgramData\ssh\administrators_authorized_keys "ssh-ed25519 AAAA... alice@mac"
   icacls.exe C:\ProgramData\ssh\administrators_authorized_keys /inheritance:r /grant Administrators:F /grant SYSTEM:F
   ```

   For a standard account, add the key to `~\.ssh\authorized_keys`
   instead.

4. On your Mac, connect to the PC, where `pc` is its name or address:

   ```sh
   rex -S ssh://alice@pc ls
   ```

   In Rex for Mac, choose **Add Remote Host…** and enter
   `ssh://alice@pc`.

The first connection starts your server, and it keeps running until you
stop it. Windows lets SSH in only on networks set to Private. For
Tailscale and other ways to connect, see
[Remote Access](https://www.superlogical.com/rex/docs/linux/remote-access.md#windows).

## Server Lifetimes

How you run or install the server determines how long it stays running:

| Started by                  | Starts                        | Keeps running                          | Needs administrator |
| --------------------------- | ----------------------------- | -------------------------------------- | ------------------- |
| Any `rex` command           | When you run it               | Until about 10 s after its last client | No                  |
| An `ssh://` connection      | When it connects              | Until you stop it                      | No                  |
| `rex server start`          | Now                           | Until you stop it or sign out          | No                  |
| `rex server install --user` | Each time you sign in         | Until you sign out                     | No                  |
| `rex server install`        | At boot, as a Windows Service | Until shutdown, signed in or not       | Yes                 |

If you only use Rex locally while you're at your PC, let `rex` commands
autostart the server.

If you want to connect to Rex remotely, start it at boot with
`rex server install`. If you can't get administrator rights, then
start it at sign in with `rex server install --user`.

## CLI-Triggered Servers

Any `rex` command automatically starts a server if none is running:

```powershell
rex ls
```

An automatically started server stops on its own about 10 seconds after
its last session and its last connection close. To start one that keeps
running until you stop it or sign out, start it explicitly:

```powershell
rex server start
```

`rex server status`, `rex server logs` and `rex server stop` work as they
do on Linux. See [Personal Server](https://www.superlogical.com/rex/docs/linux/server-modes.md#personal-server).

The server listens on the named pipe `\\.\pipe\rex-<SID>`, where `<SID>`
is your account's security identifier. If you set `REX_RUNTIME_DIR`, the
name gets a suffix, so servers with different runtime directories don't
collide.

To keep `rex` commands from starting a server at all, set
`REX_AUTOSTART=0`.

## Starting at Boot

`rex server install` creates a Windows Service that starts your server at
boot and keeps it running while nobody is signed in. Run it from a
terminal started with **Run as administrator**:

```powershell
rex server install
```

The service runs as your own account, not as LocalSystem. The command
prints each change before it makes it:

- It creates the service `RexServer-<SID>`, shown as
  "Rex Server (PC\alice)" in the Services console. It's set to
  **Automatic (Delayed Start)** and runs `"<path to rex.exe>" server run`
  with your [server flags](#server-flags).
- It grants your account the right to log on as a service.
- It registers "Rex Server" as a source of the Event Log.
- It deletes the [sign-in entry](#starting-at-sign-in), if you added one.

If a server is already running and nothing is using it, the command stops
it and starts the service in its place. If the server has sessions or
clients, it keeps running, and the service starts at the next boot. The
command says which happened.

Running the command again is safe, and keeps your server flags. Run it
again after you move `rex.exe`, such as when you switch between the WinGet
zip and the MSI, so that the service runs the new path.

## Starting at Sign-In

`rex server install --user` starts your server each time you sign in to
Windows, and needs no administrator rights:

```powershell
rex server install --user
```

It adds a value named "Rex Server" to
`HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, which runs
`rex server start` with your [server flags](#server-flags). It also starts
a server now if none is running. The server stops when you sign out.

Running the command again is safe, and keeps your server flags.

The value shows in the **Startup** tab of Task Manager, and in Settings
under **Apps > Startup**. If it's turned off there, Windows skips it, and
the command says so rather than turning it back on.

The sign-in entry and the Windows Service can't both start your server.
While the service exists, `rex server install --user` fails. Remove the
service first, as [Removing the Service](#removing-the-service) shows.

## Server Flags

`rex server tailscale on` and other changes to a running server last only
until it stops. To give your server flags each time it starts, put them
after `--`:

```powershell
rex server install -- --tailscale
rex server install --user -- --extra-listen tailnet://:7297
```

The Windows Service runs `server run` with the flags, and the sign-in
entry runs `server start` with them. With `--user`, the server that the
command starts right away gets them too. `rex server run --help` and
`rex server start --help` list the flags that each one takes.

What `--` does when you run the command again:

| Command                             | Server flags                   |
| ----------------------------------- | ------------------------------ |
| `rex server install`                | Keeps the flags already set    |
| `rex server install -- --tailscale` | Replaces them with the new ones |
| `rex server install --`             | Removes them                   |

The same goes for `--user`. Moving from `--user` to the Windows Service
carries the sign-in entry's flags over to the service.

The command checks the flags before it changes anything, and refuses
those that the server would refuse. It also refuses these, which don't
work for a server that Windows starts:

| Flag                          | Why                                                         |
| ----------------------------- | ----------------------------------------------------------- |
| `--listen`                    | `rex` commands look for the server on its named pipe. Use `--extra-listen` to add an endpoint. |
| `--exit-empty`                | The service would stop once it's idle, and Windows wouldn't start it again until the next boot. |
| `--tailscale-auth-key-file -` | Windows gives the server no standard input. Give the path of a file instead. |

A server that's already running keeps its old flags until it restarts.
See [Stopping and Restarting](#stopping-and-restarting). `rex server start`
doesn't read the sign-in entry, so to restart that server with its flags,
stop it and run `rex server install --user` again, or sign in again.

## Administrator Rights

**A server that the Windows Service starts runs without administrator
rights, even if your account is an administrator.** The service starts it
with a reduced copy of its token, so every terminal, block plugin and
`rex.exec` program runs as it would after a normal sign-in.

A server that `rex server start` or any `rex` command starts gets the
same reduced rights, even from a terminal opened with **Run as
administrator**, or from an administrator's `ssh` session. If Windows
can't make the reduced token, server startup fails.

**`rex server run` in a terminal opened with Run as administrator keeps
full administrator rights.** We assume in this scenario you explicitly want
this.

## Logs

`rex server logs` shows the log of your server however it was started,
the service included:

```powershell
rex server logs --follow
```

The log is in `%LocalAppData%\rex\servers`. Rex clears it before it grows
past 50MiB.

The Windows Service also writes to the Application log in Event Viewer,
under the source "Rex Server":

| Event ID | Level       | Meaning                       |
| -------- | ----------- | ----------------------------- |
| 1        | Information | The service is starting       |
| 2        | Information | The service is running        |
| 3        | Information | Windows asked it to stop      |
| 4        | Information | The service stopped           |
| 5        | Error       | The service stopped with an error, which the event names |

## Stopping and Restarting

`rex server stop` stops your server, however it was started. This ends
every program running in its sessions. The server saves its session
layouts, and `rex ls` lists them again once it restarts:

```powershell
rex server stop
```

When the service started it, the service stays stopped until the next
boot, or until you start it again. To start or stop the service, run
these from a terminal started with **Run as administrator**:

```powershell
sc.exe start RexServer-<SID>
sc.exe stop RexServer-<SID>
```

The Services console works too. Look for "Rex Server (PC\alice)". When the
service stops, it gives the server 15 seconds to save its sessions and
exit before it ends it.

While the service is stopped, a `rex` command can start a server on its
own, and `sc.exe start` then fails until that server stops. Set
`REX_AUTOSTART=0` if you want to keep it from happening.

## Removing the Service

`rex server uninstall` stops the Windows Service, which ends every program
running in its sessions, and then deletes it. Run it from a terminal
started with **Run as administrator**:

```powershell
rex server uninstall
```

It also unregisters "Rex Server" as a source of the Event Log, unless
another account's service still uses it, and deletes the
[sign-in entry](#starting-at-sign-in).

Once a service exists, the command needs administrator rights, even to
delete only the sign-in entry. Without a service, it needs none, and a
server that the sign-in entry started keeps running. Stop it with
`rex server stop`.

Your account keeps the right to log on as a service, because other
services may need it. To remove it, open Local Security Policy
(`secpol.msc`) and go to **Local Policies > User Rights Assignment >
Log on as a service**.

## Terminals and Shells

Each terminal runs the first of `pwsh.exe` and `powershell.exe` on the
server's PATH, or `%ComSpec%` if neither is there. There's no setting to
change the default. To run another shell, give it as the command:

```powershell
rex new -- cmd.exe
```

A server started by a `rex` command, by `rex server start` or by the
sign-in entry passes terminals only an allowlist of variables: the usual
Windows ones, such as `USERPROFILE`, `APPDATA` and `ProgramFiles`, common
toolchain variables, such as `JAVA_HOME` and `GOPATH`, and `PATH`, `REX_*`,
`XDG_*`, `LC_*` and the proxy variables. To pass others, start the server
with `--env`:

```powershell
rex server start --env NAME --env OTHER=value
```

`--env` applies only when it starts a new server. To change it, stop the
server and start it again. To keep it each time you sign in, give it to
the sign-in entry as a [server flag](#server-flags):

```powershell
rex server install --user -- --env NAME
```

A server that the Windows Service starts gets the system and user
variables Windows had when the service started. A variable you add in
Settings reaches its terminals after the service restarts. The service
refuses `--env`, because only `rex server start` takes it.

Terminals have `TERM` set to `xterm-256color`, and `rex terminfo setup` is
refused on Windows. Closing a terminal ends every process in it at once.

## Not Available on Windows

These parts of Rex need a Unix system:

| Feature                                        | On Windows                                               |
| ---------------------------------------------- | -------------------------------------------------------- |
| The shared server and the login server         | Each account runs its own server                         |
| Starting on demand through socket activation   | Any `rex` command starts the server                      |
| `--socket-mode`                                | Only your account can open the pipe                      |
| `tailnet-serve://` and `login-server://` listeners | Rex's own Tailscale node, with `rex server install -- --tailscale` |
| `tailnet://` with a userspace `tailscaled`     | `tailscaled` with its normal network adapter             |
| `rex terminfo setup`                           | Terminals keep `TERM=xterm-256color`                     |
| `rex server install ssh://` to a Windows PC    | Install Rex on the PC, then reach it with an [`ssh://` endpoint](https://www.superlogical.com/rex/docs/linux/remote-access.md#ssh-with-windows) |
| Reopening the log with `SIGHUP`                | `rex server logs` and the 50MiB limit                    |

---
title: "Server Modes"
canonical_url: https://www.superlogical.com/rex/docs/linux/server-modes
---

# Server Modes

How you run a Rex server decides which account it runs as and who can
connect to it. **On Windows, there's only the personal server, as
[Running on Windows](https://www.superlogical.com/rex/docs/windows/running.md) explains.**

**If you only want to reach Rex on one machine from your Mac, start with a
quickstart.** Each sets up a personal server that you reach over SSH:
[Linux](https://www.superlogical.com/rex/docs/linux/installation.md#quickstart),
[FreeBSD](https://www.superlogical.com/rex/docs/freebsd/installation.md#quickstart), or
[Windows](https://www.superlogical.com/rex/docs/windows/running.md#quickstart).

There are three ways to run the Rex server on Linux and FreeBSD:

| Mode            | Who it's for                            | Runs as                           | Started by                                                  |
| --------------- | --------------------------------------- | --------------------------------- | ----------------------------------------------------------- |
| Personal        | Just you                                | Your account                      | Any `rex` command, or `rex server start`                    |
| Shared          | A few people you trust with one account | `rex`                             | `rex.service`, or `rex.socket` on demand                    |
| Login           | Several people, each as themselves      | root, then each person's account  | `rex-login-server.service`, or `rex-login-server.socket` on demand |

If you're the only person using Rex on the machine, run a personal server.
If a few people need it and can share one account, use the shared server.
If you or others need to access and login as separate accounts, use the
login server.

The shared server and the login server can't run together. Their services
and sockets all use `/run/rex`, so starting the service or socket of one
stops both units of the other. On Alpine and FreeBSD, each refuses to start
while the other is running. Personal servers can run alongside either one.
With the login server, your personal server is the same server the login
server connects you to, so local `rex` commands and remote connections see
the same sessions.

Every mode listens only on Unix sockets on this machine by default. To
connect from another computer, see [Remote Access](https://www.superlogical.com/rex/docs/linux/remote-access.md).

## Personal Server

A personal server runs as your own user account and doesn't require any
special setup or root access. Any `rex` command automatically starts a
personal server if none is running:

```sh
rex ls
```

An automatically started personal server stops on its own around 10 seconds
after its last session and its last connection close. To start one that remains
running you must explicitly start it:

```sh
rex server start
```

`rex server start` does nothing if a server is already running, including
one that a `rex` command started, so its safe to call multiple times. Its
options, such as `--max-sessions`, apply only when it starts a new server.
To change them, stop the server and start it again.

| Command                    | Effect                                                         |
| -------------------------- | -------------------------------------------------------------- |
| `rex server status`        | Show the running server's status.                              |
| `rex server logs --follow` | Show and follow the log of a server started by `rex server start`. |
| `rex server stop`          | Stop the server. This ends every session on it.                |

The socket is in `$XDG_RUNTIME_DIR/rex`, which is usually
`/run/user/<uid>/rex`. Logs and other state are in `~/.local/state/rex`, or
`$XDG_STATE_HOME/rex` if you've set it. FreeBSD has no `/run/user`, so
there the socket is in `~/.local/state/rex/run`. The same goes for Linux
machines without it, such as many containers.

A `rex` command starts a server on its own only for a socket in that
directory, or in `REX_RUNTIME_DIR` instead if you've set it. Given any
other socket with `-S`, such as the shared server's, it starts nothing and
says how to start a server there. To run one of your own at another path,
start it yourself. Your account must be able to create files in the
socket's directory:

```sh
rex -S PATH server start
```

To keep `rex` commands from starting a server at all, set
`REX_AUTOSTART=0`.

## Shared Server

A shared server is one server that runs as the `rex` system account.
Members of the `rex` group can connect to it, and every program they start
runs as `rex`. It's the simplest way to let more than one person use Rex.

**Access to the shared server is access to the `rex` account.** Anyone in
the group can see and type into every session on it. Only add people you
would trust with that account.

Add each person to the group, then start the service now and at every boot:

```sh
sudo usermod --append --groups rex alice
sudo systemctl enable --now rex.service
```

A group change applies the next time that person logs in. The server keeps
its data in `/var/lib/rex`, or `/var/db/rex` on FreeBSD. On Alpine, the
[OpenRC commands](https://www.superlogical.com/rex/docs/linux/installation.md#alpine) do both steps, and on
FreeBSD, the [FreeBSD commands](https://www.superlogical.com/rex/docs/freebsd/installation.md#commands-and-paths).

To start the server only when someone first connects, enable `rex.socket`
instead of `rex.service`. See
[Starting on Demand](https://www.superlogical.com/rex/docs/linux/shared-server.md#starting-on-demand).

A package upgrade restarts the service, which automatically ends every
session on it. See [Upgrading](https://www.superlogical.com/rex/docs/linux/upgrading.md).

Read [Shared Server](https://www.superlogical.com/rex/docs/linux/shared-server.md) before you set it up for other
people. It covers giving them access, configuring the server, and changing
the service without losing your changes on upgrade.

## Login Server

The login server gives each person a server that runs as their own account,
with their own files and permissions. It runs as root. When someone
connects, it identifies them, checks `/etc/rex/accounts.jsonc` for the
accounts they may use, and hands the connection to a server running as that
account. It starts that server if needed.

The login server is the most similar to SSH: one root service that lets each
person in as their own account. Unlike SSH, it has no passwords or keys.
**It identifies people only by how they connect**, such as the local account
on the other end of its socket or their Tailscale login. Anyone who can
reach it that way can use the accounts listed for them.

Each account's server keeps running when the login server stops, restarts,
or is upgraded, so sessions survive all three. After an upgrade, each one
keeps the old version until its sessions end.

The accounts file ships empty, which lets nobody in. Add at least one
account, then start the service now and at every boot:

```sh
sudo $EDITOR /etc/rex/accounts.jsonc
sudo systemctl enable --now rex-login-server.service
```

On Alpine, use the [OpenRC commands](https://www.superlogical.com/rex/docs/linux/installation.md#alpine) instead.
On FreeBSD, use the
[FreeBSD commands](https://www.superlogical.com/rex/docs/freebsd/installation.md#commands-and-paths), and edit
`/usr/local/etc/rex/accounts.jsonc`.

To start the login server only when someone first connects, enable
`rex-login-server.socket` instead of `rex-login-server.service`. See
[Starting on Demand](https://www.superlogical.com/rex/docs/linux/login-server.md#starting-on-demand).

After you change the accounts file, reload it. This doesn't drop any
connections:

```sh
sudo systemctl reload rex-login-server.service
```

On Alpine, run `rc-service rex-login-server reload`. On FreeBSD, run
`service rex-login-server reload`.

Read [Login Server](https://www.superlogical.com/rex/docs/linux/login-server.md) before you add accounts. It covers
the accounts file, how people choose an account when they connect, and the
PAM policy the login server checks accounts against.

## Switching Between the Services

Starting either server stops the other, but disable the old server's
service and socket too, so that neither comes back at the next boot:

```sh
sudo systemctl disable --now rex.service rex.socket
sudo systemctl enable --now rex-login-server.service
```

To start the new server on demand, enable its socket instead, such as
`rex-login-server.socket`. To switch back, swap the names: disable
`rex-login-server.service` and `rex-login-server.socket`, then enable
`rex.service` or `rex.socket`.

On Alpine, starting either service fails while the other is running, so
stop the old one first:

```sh
rc-service rex stop
rc-update del rex
rc-update add rex-login-server
rc-service rex-login-server start
```

On FreeBSD, the same applies. Stop and disable the old one first:

```sh
service rex stop
sysrc rex_enable=NO
sysrc rex_login_server_enable=YES
service rex-login-server start
```

Switching from the shared server ends every session on it.

Switching from the login server leaves each account's server running. On
machines without systemd-logind, or with a login server run with
`--pam=false`, those servers keep their sockets in `/run/rex/accounts`, and
`rex.service` and `rex.socket` refuse to start while that directory exists.
Once nobody needs those sessions, clear the directory and start the shared
server:

```sh
sudo rm -rf /run/rex
sudo systemctl start rex.service
```

To start the shared server on demand, start `rex.socket` instead. A
refused start can still stop the login server's units, so if you back out,
start the login server's service or socket again.

Alpine has no systemd-logind, so the account servers always keep their
sockets in `/run/rex/accounts` there. Clear the directory the same way,
then run `rc-service rex start`.

FreeBSD has no systemd-logind either, and keeps `/var/run` across reboots,
so there the `rex` service refuses to start only while
`/var/run/rex/accounts` holds a socket. Clear it the same way with
`rm -rf /var/run/rex`, then run `service rex start`.

---
title: "Shared Server"
canonical_url: https://www.superlogical.com/rex/docs/linux/shared-server
---

# Shared Server

The shared server is one Rex server that runs as the `rex` system account,
as `rex.service`. Members of the `rex` group can connect to it, and every
program they start runs as `rex`. It's the simplest
[server mode](https://www.superlogical.com/rex/docs/linux/server-modes.md) for a few people who are comfortable
sharing one account.

**Shared servers are only on Linux and FreeBSD.** On Windows, see
[Running on Windows](https://www.superlogical.com/rex/docs/windows/running.md).

**If you're the only one using Rex on the machine, you don't need the
shared server.** Start with the quickstart for
[Linux](https://www.superlogical.com/rex/docs/linux/installation.md#quickstart) or
[FreeBSD](https://www.superlogical.com/rex/docs/freebsd/installation.md#quickstart) instead, which sets up a
personal server that you reach over SSH.

**Access to the shared server is access to the `rex` account.** Anyone in
the group can see and type into every session on it and start programs as
`rex`. Only add people you would trust with that account. If people need
their own files and permissions, use the [login server](https://www.superlogical.com/rex/docs/linux/login-server.md)
instead.

## Starting the Server

The package creates the `rex` account with `nologin` as its shell, and Rex
starts each terminal with its account's shell. Give the account a real
shell first, or every terminal on the shared server exits as soon as it
starts:

```sh
sudo usermod --shell /bin/bash rex
```

Alpine has neither `usermod` nor bash by default. Install `usermod` with
the `shadow` package and use `/bin/sh`:

```sh
apk add shadow
usermod --shell /bin/sh rex
```

FreeBSD has no bash by default either. Use `pw` and `/bin/sh` there:

```sh
pw usermod rex -s /bin/sh
```

The account stays locked, with no password, so nobody can log in as `rex`
this way.

Then start the service now and at every boot:

```sh
sudo systemctl enable --now rex.service
```

On Alpine, run `rc-update add rex`, then `rc-service rex start`. On
FreeBSD, run `sysrc rex_enable=YES`, then `service rex start`.

To start the server only when someone first connects, enable `rex.socket`
instead. See [Starting on Demand](#starting-on-demand).

The server listens on `/run/rex/server.sock` and keeps its data in
`/var/lib/rex`, the `rex` account's home directory. On FreeBSD, those are
`/var/run/rex/server.sock` and `/var/db/rex`.

## Starting on Demand

Rex can be paired with systemd to automatically start the shared server
when a client connections, instead of at boot using `rex.socket`.

When this is enabled, systemd creates `/run/rex/server.sock` and waits. When
a client connects, systemd starts `rex.service` with the same command line
and hands it the socket. The client waits a moment while the server starts,
then works as usual. Access is the same: members of the `rex` group can use
the socket.

Enable the socket instead of the service:

```sh
sudo systemctl enable --now rex.socket
```

Enable one or the other, not both. The socket starts the service when it
needs to. `systemctl status rex.socket` shows "active (listening)" until
the first client connects.

Once it starts, the server keeps running, even when nobody is using it.
The socket changes when the server starts, not how long it runs.

While the socket is active, any connection starts the server, including
`rex server status`. To check on it without starting it, ask systemd:

```sh
systemctl status rex.service rex.socket
```

Stopping `rex.service` alone lasts only until the next connection. To stop
the server and keep it stopped, stop both units in one command. This ends
every session on it:

```sh
sudo systemctl stop rex.socket rex.service
```

Stopping only `rex.socket` leaves the server running without its socket
file, so nobody new can connect to it. Don't use `rex server stop` either.
It connects to the socket to check that the server stopped, which can start
it again.

To switch to a server that always runs, disable both units, then enable
the service:

```sh
sudo systemctl disable --now rex.socket rex.service
sudo systemctl enable --now rex.service
```

To switch back, stop the service first. systemd won't listen on the socket
while the server is running:

```sh
sudo systemctl disable --now rex.service
sudo systemctl enable --now rex.socket
```

Either switch ends every session on a running shared server.

Alpine and FreeBSD have no socket activation, so there the service is the
only way to run the shared server. On NixOS, set
`services.rex.socketActivation`. See
[The NixOS Module](https://www.superlogical.com/rex/docs/linux/installation.md#the-nixos-module).

## Giving People Access

Add each person to the `rex` group. The change applies the next time they
log in:

```sh
sudo usermod --append --groups rex alice
```

On Alpine, run `addgroup alice rex`. On FreeBSD, run
`pw groupmod rex -m alice`.

Their `rex` commands look for their own [personal
server](https://www.superlogical.com/rex/docs/linux/server-modes.md#personal-server) by default, and start one if
it isn't running. To use the shared server instead, pass its socket with
`-S`, or set `REX_SERVER` in their shell profile so every `rex` command
uses it:

```sh
rex -S /run/rex/server.sock ls
export REX_SERVER=/run/rex/server.sock
```

On FreeBSD, the socket is `/var/run/rex/server.sock`. Inside the shared
server's own terminals, `rex` commands already use the shared server.

A `rex` command never starts a personal server at the shared server's
socket. If the shared server isn't running, the command fails, and on Linux
the error names the service to start. With `rex.socket` enabled, the
connection starts the shared server instead. People outside the `rex` group
get a permission error, because only the group can open `/run/rex`.

The service isn't sandboxed on purpose. Sandboxing would also restrict the
programs people run in it, so the `rex` account is the security boundary.

To reach the shared server from another computer, see
[Remote Access](https://www.superlogical.com/rex/docs/linux/remote-access.md).

## Configuration

The shared server reads its [configuration file](https://www.superlogical.com/rex/docs/customize/config.md) from
the `rex` account's home:

```
/var/lib/rex/.config/rex/init.lua
```

On FreeBSD, it's `/var/db/rex/.config/rex/init.lua`. Only root can edit
it, because the home directory is private to the `rex` account. After you
change it, any member of the group can reload it:

```sh
sudo $EDITOR /var/lib/rex/.config/rex/init.lua
rex -S /run/rex/server.sock config reload
```

To keep the file somewhere else, pass `--config PATH` in a drop-in, as
below.

## Customizing the Service

Package upgrades replace `rex.service`, so put changes in a drop-in file
instead. This command creates one and opens it in your editor:

```sh
sudo systemctl edit rex.service
```

To change the server's options, clear `ExecStart` and set it again. This
example raises the session limit:

```ini
[Service]
ExecStart=
ExecStart=/usr/bin/rex server run --listen /run/rex/server.sock \
  --state-dir /var/lib/rex --socket-mode 0660 --log-format text \
  --max-sessions 128
```

On Alpine, upgrades replace `/etc/init.d/rex`, so set `command_args` in
`/etc/conf.d/rex` instead. The package never creates or changes that file.
The same example there leaves out `/usr/bin/rex`:

```sh
command_args="server run --listen /run/rex/server.sock \
  --state-dir /var/lib/rex --socket-mode 0660 --log-format text \
  --max-sessions 128"
```

Keep the four options from the original command line. The same pattern adds
a network listener for [Remote Access](https://www.superlogical.com/rex/docs/linux/remote-access.md) or a block
plugin directory with `--block-dir`. Run `rex server run --help` for every
option.

With `rex.socket`, the same drop-in applies, because the socket starts
`rex.service`. The socket's path and mode come from the socket unit, so to
change them, edit both units. See
[Customizing the Socket](#customizing-the-socket).

On FreeBSD, upgrades replace `/usr/local/etc/rc.d/rex` but never change
`/etc/rc.conf`, so set `rex_flags` there instead. It adds options to the
service's command line rather than replacing it, so leave out the four it
already has:

```sh
sysrc rex_flags="--max-sessions 128"
service rex restart
```

[Settings](https://www.superlogical.com/rex/docs/freebsd/installation.md#settings) lists the service's other
settings, such as `rex_umask`.

## Customizing the Socket

With `rex.socket`, systemd creates the server's socket and hands it to the
server, which serves it instead of creating its own. Package upgrades
replace `rex.socket` too, so put changes in a drop-in for it:

```sh
sudo systemctl edit rex.socket
```

The server serves only the sockets its command line names. Every
`ListenStream=` path in the socket unit needs a matching `--listen` or
`--extra-listen` in `rex.service`. Otherwise the server refuses to start,
and clients wait until they time out.

To move the socket to `/run/rex/rex.sock`, stop both units first. systemd
removes the socket file it knows about when the unit stops, so stopping
after the edit would leave the old file behind:

```sh
sudo systemctl stop rex.socket rex.service
```

Then run `sudo systemctl edit rex.socket` and set the new path. The empty
`ListenStream=` clears the one in the unit:

```ini
[Socket]
ListenStream=
ListenStream=/run/rex/rex.sock
```

Run `sudo systemctl edit rex.service` and set the same path:

```ini
[Service]
ExecStart=
ExecStart=/usr/bin/rex server run --listen /run/rex/rex.sock \
  --state-dir /var/lib/rex --socket-mode 0660 --log-format text
```

Then start the socket again:

```sh
sudo systemctl start rex.socket
```

Clients then connect with `rex -S /run/rex/rex.sock`. To serve a second
socket, add another `ListenStream=` line to the socket unit and a matching
`--extra-listen unix://PATH` to the service. A socket for Tailscale Serve
needs `tailnet-serve://` instead, as
[Tailscale Serve](https://www.superlogical.com/rex/docs/linux/remote-access.md#tailscale-serve) shows.

The server keeps the mode that `SocketMode=` gives the socket, and ignores
`--socket-mode` for it. It refuses a mode that lets every user on the
machine connect, such as `0666`, so keep it at `0660` or narrower. `0600`
works, but locks out the `rex` group.

The server takes only listening stream sockets, from `ListenStream=`, and
refuses `Accept=yes`. A TCP `ListenStream=` needs `--extra-listen` with the
same port and `--allow-anonymous`, as
[Plain HTTP](https://www.superlogical.com/rex/docs/linux/remote-access.md#plain-http) describes. Give it an IP
address, not a host name.

To apply any later change to `rex.socket`, stop both units and start the
socket again, as above. `sudo systemctl restart rex.socket` fails while
the server is running, and leaves the server without its socket file.

## Logs

The shared server logs to the journal:

```sh
journalctl -u rex.service -f
```

Alpine has no journal, so there the server logs to a file:

```sh
tail -f /var/log/rex/server.log
```

On FreeBSD, the server logs to syslog, which writes to `/var/log/messages`:

```sh
grep ' rex\[' /var/log/messages
```

The logs are plain text. For JSON, change `--log-format text` to
`--log-format json` in a drop-in.

With `rex.socket`, the server still logs as `rex.service`. If it refuses
to start, such as for a socket it isn't configured to serve, the journal
names the socket and the option to add. Clients aren't refused meanwhile.
Each one waits until its `--timeout` runs out, 10 seconds by default.

`rex server logs` shows only servers started with `rex server start`, not
the service. Use the journal instead.

## Upgrading

Upgrading the package restarts a running shared server, so it runs the new
version right away. On FreeBSD, pkg restarts nothing, so the server keeps
the old version until you run `service rex restart`.

With `rex.socket`, a running server restarts the same way, and one that
hasn't started yet runs the new version when the first client connects.
Upgrades never restart `rex.socket` itself. If a new version changes the
socket unit, the change applies once you stop both units and start the
socket again, or at the next reboot.

**Restarting ends every session and every program in them.** Upgrade when
nobody has work running, or
[hold the package](https://www.superlogical.com/rex/docs/linux/upgrading.md#holding-an-upgrade) until then.

The login server doesn't have this problem. Its sessions survive upgrades.
[Upgrading](https://www.superlogical.com/rex/docs/linux/upgrading.md) covers the rest.

---
title: "Login Server"
canonical_url: https://www.superlogical.com/rex/docs/linux/login-server
---

# Login Server

The login server lets several people use Rex on one machine, each as their
own operating system account, with their own files and permissions. It's
the [server mode](https://www.superlogical.com/rex/docs/linux/server-modes.md) to use when people shouldn't share
an account.

**Login servers are only on Linux and FreeBSD.** On Windows, see
[Running on Windows](https://www.superlogical.com/rex/docs/windows/running.md).

**If you're the only one using Rex on the machine, you don't need the
login server.** Start with the quickstart for
[Linux](https://www.superlogical.com/rex/docs/linux/installation.md#quickstart) or
[FreeBSD](https://www.superlogical.com/rex/docs/freebsd/installation.md#quickstart) instead, which sets up a
personal server that you reach over SSH.

**The login server has no passwords or keys.** It identifies people only by
how they connect, such as their Tailscale login. Anyone who can reach it
that way can use every account the accounts file lists for them. This
is only a short-term limitation, we have a full authentication and
authorization system planned (including supporting legacy SSH keys),
but until then we recommend using something like Tailscale.

## How It Works

The login server runs as root, as `rex-login-server.service`. It can also
start when the first client connects, through `rex-login-server.socket`.
See [Starting on Demand](#starting-on-demand). When a client connects:

1. The login server identifies the caller, such as by their Tailscale login
   or their local user ID.
2. It checks `/etc/rex/accounts.jsonc` for the accounts that caller may use,
   and picks one.
3. It opens a login session for that account through PAM, the same as a
   console or SSH login, and starts a Rex server as that account. If one is
   already running, it uses that one instead.
4. It hands the connection to that server.

Each account's server keeps running when the login server stops, restarts,
or is upgraded, so sessions survive all three. After an upgrade, each one
keeps the old version until its sessions end. See
[What Restarts](https://www.superlogical.com/rex/docs/linux/upgrading.md#what-restarts).

On machines with systemd-logind, an account's server is the same as that
person's [personal server](https://www.superlogical.com/rex/docs/linux/server-modes.md#personal-server). Sessions
they start with `rex` on the machine and sessions they start from another
computer are on the same server.

## Accounts File

`/etc/rex/accounts.jsonc` lists the accounts people may use and who may use
each one. It's JSON, with comments and trailing commas allowed. The file ships
empty, which lets nobody in by default.

This example lets Alice use her own account and root. She connects as
`alice` unless she asks for root:

```jsonc
{
  "version": 1,
  "accounts": {
    "alice": { "principals": ["tailscale:alice@example.com"] },
    "root": { "principals": ["tailscale:alice@example.com"] },
  },
  "defaults": { "tailscale:alice@example.com": "alice" },
}
```

Under `accounts`, each key is an account name, and `principals` lists the
callers allowed to use it. A caller may use an account if any of its
principals match:

| Principal                     | Matches                                                       |
| ----------------------------- | ------------------------------------------------------------- |
| `tailscale:alice@example.com` | A person, by their Tailscale login                            |
| `tailscale:tag:ci`            | A device with the Tailscale tag `tag:ci`                      |
| `tailscale:*`                 | Any person on the tailnet                                     |
| `tailscale:tag:*`             | Any tagged device                                             |
| `peer:1000`                   | A local user, by user ID                                      |
| `identified`                  | Any caller Rex can identify                                   |
| `anonymous`                   | Callers Rex can't identify, such as on a plain TCP listener   |

A tagged device never matches a person's login or `tailscale:*`.

### Any Account

The special account `"*"` lets its callers use any regular account, meaning
any account with a user ID of 1000 or higher, whether or not the file names
it. Root and system accounts must always be listed by name.

```jsonc
{
  "version": 1,
  "accounts": {
    "*": { "principals": ["tailscale:*"] },
  },
}
```

For safety, `anonymous` can't appear under `"*"`, and anonymous callers can
never use root.

### Defaults

When a client doesn't ask for a specific account, `defaults` picks one. A
default only chooses an account. The caller must still be allowed to use
it.

If several defaults match a caller, the most specific one wins: a login or
user ID first, then a tag, then a Tailscale wildcard, then `identified`,
then `anonymous`. Between equally specific matches, the first in the file
wins.

With no matching default, a caller allowed exactly one account connects as
that account. Otherwise, the client must name the account it wants.

## Starting and Reloading

Add at least one account so a user can access, then start the service:

```sh
sudo $EDITOR /etc/rex/accounts.jsonc
sudo systemctl enable --now rex-login-server.service
```

This will enable it to start now and for every reboot. On Alpine, use the
[OpenRC commands](https://www.superlogical.com/rex/docs/linux/installation.md#alpine) instead, and on FreeBSD, the
[FreeBSD commands](https://www.superlogical.com/rex/docs/freebsd/installation.md#commands-and-paths).

Reload the server anytime you change the accounts file:

```sh
sudo systemctl reload rex-login-server.service
```

On Alpine, run `rc-service rex-login-server reload`. On FreeBSD, run
`service rex-login-server reload`.

Reloading doesn't drop connections that are still allowed. It closes
connections whose access you removed, but leaves every account's server and
its sessions running. The journal shows `account policy reloaded`, or
`account policy reload failed; previous policy retained` if the new file
has an error.

Package upgrades keep your changes to the accounts file. The login
server's logs are in the journal:

```sh
journalctl -u rex-login-server.service
```

Alpine has no journal, so there the logs are in
`/var/log/rex-login-server.log`. On FreeBSD, they're in
`/var/log/messages`. See [FreeBSD](#freebsd).

The login server listens only on `/run/rex/login-server.sock`, which only
root can use. To let people connect from other computers, see
[Remote Access](https://www.superlogical.com/rex/docs/linux/remote-access.md).

## Starting on Demand

Rex can be paired with systemd to automatically start the login server
when a client connections, instead of at boot using `rex-login-server.socket`.

When this is enabled, systemd creates `/run/rex/login-server.sock` and waits.
When a client connects, systemd starts `rex-login-server.service` and hands
it the socket. The client waits a moment while the login server starts,
then works as usual. Only root can use the socket, the same as when the
login server creates it.

Add at least one account, then enable the socket instead of the service:

```sh
sudo $EDITOR /etc/rex/accounts.jsonc
sudo systemctl enable --now rex-login-server.socket
```

Enable one or the other, not both. The socket starts the service when it
needs to. `systemctl status rex-login-server.socket` shows "active
(listening)" until the first client connects.

While the socket is active, any connection starts the login server,
including `rex server status`. To check on it without starting it, run
`systemctl status rex-login-server.service rex-login-server.socket`.

Reload the accounts file the same way as above. Before any client has
connected, the login server isn't running, so the reload fails. There's
nothing to do then: the login server reads the new file when it starts.

If the accounts file is missing or invalid, the login server exits with
`loading accounts file:` and the reason, and starts again 5 seconds later.
Clients aren't refused meanwhile. Each one waits until its `--timeout`
runs out, 10 seconds by default. The journal shows why.

Stopping `rex-login-server.service` alone lasts only until the next
connection. To stop the login server and keep it stopped, stop both units
in one command:

```sh
sudo systemctl stop rex-login-server.socket rex-login-server.service
```

Stopping only the socket leaves the login server running without its
socket file, so nobody new can connect to it. Either way, each account's
server and its sessions keep running. Clients reach them again once the
login server is back.

To switch to a login server that always runs, disable both units, then
enable the service:

```sh
sudo systemctl disable --now rex-login-server.socket rex-login-server.service
sudo systemctl enable --now rex-login-server.service
```

To switch back, stop the service first. systemd won't listen on the socket
while the login server is running:

```sh
sudo systemctl disable --now rex-login-server.service
sudo systemctl enable --now rex-login-server.socket
```

To change the socket, run `sudo systemctl edit rex-login-server.socket`.
Every `ListenStream=` in it needs a matching option on the login server's
command line. It works the same way as for the shared server. See
[Customizing the Socket](https://www.superlogical.com/rex/docs/linux/shared-server.md#customizing-the-socket). To
have a request through Tailscale Serve start the login server too, see
[Tailscale Serve](https://www.superlogical.com/rex/docs/linux/remote-access.md#tailscale-serve).

Alpine and FreeBSD have no socket activation. On NixOS, set
`services.rex.loginServer.socketActivation`. See [NixOS](#nixos).

## Choosing an Account

To ask for a specific account, put it before the host in the server's
address:

```sh
rex -S root@build.example.ts.net ls
```

The same address works in the Mac app's **Add Remote Host…**. Without an
account in the address, the caller's default applies.

Accounts can only be chosen in an `https://` or `http://` address, so the
login server needs a network listener first. [Remote Access](https://www.superlogical.com/rex/docs/linux/remote-access.md)
covers setting one up and connecting to it as each account.

## PAM

The login server uses PAM each time it starts a server for an account. PAM
decides whether the account may log in at all, such as whether it's locked
or expired, and sets up the login session: resource limits, environment
variables, and a systemd-logind session on machines that have one.

Rex has already identified the caller before PAM runs, so its policy never
asks for a password, the same as SSH with keys. While `/etc/nologin`
exists, only root may log in. Accounts whose shell is `nologin` or `false`
are always refused.

The package installs a policy at `/etc/pam.d/rex-login-server` that fits
your distribution: Debian and Ubuntu, Red Hat and its relatives, SUSE, or
Alpine. Upgrades keep your changes to it. FreeBSD is different. See
[FreeBSD](#freebsd).

To skip PAM, run `sudo systemctl edit rex-login-server.service` and
replace the service's command line with one that adds `--pam=false`. Rex
then checks
`/etc/shadow` and `/etc/nologin` itself and opens no login session.

On Alpine, set `command_args` in `/etc/conf.d/rex-login-server` instead,
to the service's command line with `--pam=false` added:

```sh
command_args="login-server --listen unix:///run/rex/login-server.sock \
  --accounts /etc/rex/accounts.jsonc --runtime-root /run/rex --log-format text \
  --pam-service rex-login-server --pam=false"
```

## NixOS

The NixOS module writes the accounts file from
`services.rex.loginServer.accounts` and `services.rex.loginServer.defaults`.
It doesn't create the accounts, so declare them with `users.users`:

```nix
{
  users.users.alice.isNormalUser = true;

  services.rex.loginServer = {
    enable = true;
    accounts.alice.principals = [ "tailscale:alice@example.com" ];
  };
}
```

To keep the order of `defaults`, which decides ties, use
`services.rex.loginServer.accountsFile` to load your own file instead.

To start the login server only when the first client connects, set
`services.rex.loginServer.socketActivation = true;`. To stop it and keep it
stopped, stop `rex-login-server.socket` and `rex-login-server.service`
together. See [The NixOS Module](https://www.superlogical.com/rex/docs/linux/installation.md#the-nixos-module).

## FreeBSD

On FreeBSD, the accounts file is `/usr/local/etc/rex/accounts.jsonc`, and
the [FreeBSD commands](https://www.superlogical.com/rex/docs/freebsd/installation.md#commands-and-paths) start and
reload the service. The login server logs to syslog, in
`/var/log/messages`. Each account's server
logs to `/var/run/rex/accounts/<account>/server.log` instead, and can be read
by the person using that account, too.

The package installs its PAM policy at
`/usr/local/etc/pam.d/rex-login-server`, and **every upgrade replaces it**.
To change the rules, copy it to `/etc/pam.d` and edit the copy. OpenPAM
reads `/etc/pam.d` first, and upgrades never touch it:

```sh
install -o root -g wheel -m 0644 /usr/local/etc/pam.d/rex-login-server \
  /etc/pam.d/rex-login-server
$EDITOR /etc/pam.d/rex-login-server
```

OpenPAM ignores a policy that root doesn't own, so keep the copy owned by
root.

The policy checks accounts the same way sshd does. An account locked with
`pw lock` is refused, `/etc/login.access` applies, and while
`/var/run/nologin` exists, only root may log in. Rex applies each account's
login class from `/etc/login.conf` to its server, such as its resource
limits.

To skip PAM, add `--pam=false` to `rex_login_server_flags`, then restart
the service:

```sh
sysrc rex_login_server_flags="--pam=false"
service rex-login-server restart
```

Rex then checks `/etc/master.passwd` and `/var/run/nologin` itself.

---
title: "Remote Access"
canonical_url: https://www.superlogical.com/rex/docs/linux/remote-access
---

# Remote Access

This page documents how you access your servers remotely once you have Rex
Server installed and running.
On Linux or FreeBSD, Rex Server listens only on Unix sockets on that
machine by default. On Windows, it listens only on a named pipe that only
the account owner can open.

To connect to your server from a client on another machine, you must
specify a network listener or access it over SSH. This page covers each
option and the details of it.

**Rex trusts a caller based only on how they connect today**, so it can't
check who someone is unless the network tells it. That's why servers listen
only locally by default and why every listener below is cautious about who
it lets in. This is a short-term limitation. A full authentication and
authorization system is planned, including support for existing SSH keys.

Until then, we recommend [SSH](#ssh). Most machines already run it, it
uses the keys you already have, and the server sees your SSH login, which
is how Rex decides who gets in. Nothing listens on the network but sshd.

| Listener            | Needs                          | Works with                 | Identifies callers |
| ------------------- | ------------------------------ | -------------------------- | ------------------ |
| SSH                 | SSH access to the machine      | Personal and shared server | Yes                |
| Built-in Tailscale  | A Tailscale account            | Personal and shared server | Yes                |
| Tailscale Serve     | Tailscale on the machine       | Shared and login server    | Yes                |
| Tailnet address     | Tailscale on the machine       | Shared and login server    | Yes                |
| Plain HTTP          | `--allow-anonymous`            | Every mode                 | No                 |
| Another VPN         | `--allow-anonymous`            | Every mode                 | No                 |

## SSH

SSH is installed on almost every network-connected machine already, and
you can connect to Rex using your existing SSH credentials. This is the
recommended starting path for accessing Rex remotely.

You can use an `ssh://` address to connect to a server via SSH. Rex
runs your own system `ssh` if available, so it uses your own keys, agents,
config, etc.

```sh
rex -S ssh://alice@build.example.com ls
rex hosts add build ssh://alice@build.example.com
```

The address form is `ssh://[USER@]HOST[:PORT][/SOCKET]`. Without a socket,
Rex finds the SSH user's personal server or starts it if none is running.
If `REX_AUTOSTART=0` is set on the machine, Rex will not autostart your
server over SSH.

On Linux, your personal server's socket goes away when you log out
which will *kill all your running sessions.** Enable linger with
`loginctl enable-linger` to prevent this.

For SSH to work, `rex` must be on the PATH for SSH connections, or in
`~/.local/bin` which we check explicitly since that's where our unprivileged
user install goes.

## Built-in Tailscale

With `--tailscale`, the server joins your tailnet as its own device, using
Tailscale built into Rex. The machine doesn't need Tailscale installed.

```sh
rex server start --tailscale
```

The first time, the server needs to sign in to Tailscale. Its sign-in link
appears in `rex server status` and the server's log. To sign in without a
browser, pass a one-time auth key with `--tailscale-auth-key-file FILE`, or
`-` to read it from standard input. The server keeps its login, so it needs
the key only once.

The device is named `rex` unless you choose another name with
`--tailscale-hostname`. Its address is `https://rex.<tailnet>.ts.net`. Your
tailnet must have MagicDNS and HTTPS certificates turned on.

To sign it in as a tagged device, add `--tailscale-advertise-tag tag:NAME`.
A tagged device has no owner, so this also needs
`--tailscale-allow-tailnet-members`. See [Who Can Connect](#who-can-connect).

On Windows, you must specify these options to `rex server install` after
`--`. A Windows Service has no standard input, so give the auth key as a
file:

```powershell
rex server install -- --tailscale --tailscale-auth-key-file C:\Users\alice\ts-key.txt
```

## Tailscale Serve

If the machine already runs Tailscale, Tailscale Serve can forward HTTPS
connections to a socket that Rex listens on. This is the usual way to reach
the shared server or the login server.

Run `sudo systemctl edit rex.service` (or
`rex-login-server.service`) and add the socket to its command line. Package
upgrades keep this file. For the shared server:

```ini
[Unit]
After=tailscaled.service
Requires=tailscaled.service

[Service]
ExecStart=
ExecStart=/usr/bin/rex server run --listen /run/rex/server.sock \
  --state-dir /var/lib/rex --socket-mode 0660 --log-format text \
  --extra-listen tailnet-serve:///run/rex/serve.sock
```

For the login server, the command line is:

```ini
ExecStart=
ExecStart=/usr/bin/rex login-server --listen unix:///run/rex/login-server.sock \
  --accounts /etc/rex/accounts.jsonc --runtime-root /run/rex --log-format text \
  --pam-service rex-login-server --extra-listen tailnet-serve:///run/rex/serve.sock
```

On Alpine, set `command_args` in `/etc/conf.d/rex` (or
`/etc/conf.d/rex-login-server`) to the same command line, without
`/usr/bin/rex`. `rc_need` takes the place of `After=` and `Requires=`. For
the shared server:

```sh
command_args="server run --listen /run/rex/server.sock \
  --state-dir /var/lib/rex --socket-mode 0660 --log-format text \
  --extra-listen tailnet-serve:///run/rex/serve.sock"
rc_need="tailscale"
```

On FreeBSD, add the socket to `rex_flags` (or `rex_login_server_flags`),
then restart the service. Its socket is in `/var/run/rex`:

```sh
sysrc rex_flags="--extra-listen tailnet-serve:///var/run/rex/serve.sock"
service rex restart
```

Then point Tailscale Serve at the socket:

```sh
sudo tailscale serve --bg --https=443 unix:/run/rex/serve.sock
```

On FreeBSD, that's `unix:/var/run/rex/serve.sock`.

The address is the machine's own tailnet name, such as
`https://build.<tailnet>.ts.net`.

Tailscale Serve doesn't tell Rex who a tagged device is, so tagged devices
such as CI runners can't connect this way. Use a tailnet address for them
instead.

**Use `tailnet-serve://` for the Serve socket, never `unix://`.** With
`unix://`, the server starts without an error, but treats every tailnet
caller as the account that runs tailscaled, usually root.

### Socket-Activated Servers

With `rex.socket` or `rex-login-server.socket`, the drop-in above works
unchanged, but the server creates the Serve socket only once it's running,
so a request through Tailscale Serve can't start it. To let it, add the
Serve socket to the socket unit too. Run `sudo systemctl edit rex.socket`
(or `rex-login-server.socket`) and add:

```ini
[Socket]
ListenStream=/run/rex/serve.sock
```

Keep the `--extra-listen` option in the service's drop-in. Then stop both
units and start the socket again, so that systemd creates the new socket:

```sh
sudo systemctl stop rex.socket rex.service
sudo systemctl start rex.socket
```

For the login server, that's `rex-login-server.socket` and
`rex-login-server.service`. The Serve socket gets the socket unit's mode,
and Tailscale Serve still reaches it, because tailscaled normally runs as
root. See
[Customizing the Socket](https://www.superlogical.com/rex/docs/linux/shared-server.md#customizing-the-socket).

## Tailnet Address

A server can also listen directly on the machine's tailnet addresses, using
the machine's own Tailscale to identify callers. Add this to the server's
command line, the same way as the Serve socket above:

```sh
--extra-listen tailnet://:7297
```

The port is required. The connection is plain HTTP, encrypted by Tailscale,
at `http://build.<tailnet>.ts.net:7297`.

## Who Can Connect

**A connection to a server can do anything that server can,** including
starting programs as its account. Control access with care.

For a personal server or the shared server, only the Tailscale user who
owns the machine (or the Rex device, with built-in Tailscale) is let in by
default. To let in other people:

| Listener           | Option                              | Lets in                                    |
| ------------------ | ----------------------------------- | ------------------------------------------ |
| Built-in Tailscale | `--tailscale-allow-tailnet-members` | Anyone on the tailnet your ACLs allow      |
| Serve or address   | `--tailnet-allow-identified`        | Anyone Tailscale can identify              |

A tagged machine or device has no owner, so it needs one of these options
to let anyone in.

The login server is different. It lets in everyone Tailscale can identify,
because its [accounts file](https://www.superlogical.com/rex/docs/linux/login-server.md#accounts-file) decides
which accounts each person may use.

To see who's connected to a server, run `rex client ls` on it. Its
`principal` column shows the identity of each client.

## Plain HTTP

A plain HTTP listener can't tell callers apart, so Rex refuses one unless
you also pass `--allow-anonymous`:

```sh
rex server start --extra-listen :7297 --allow-anonymous
```

**Every connection then has full access.** Only do this on a network you
completely control. The login server lets such callers use only accounts
whose principals include `anonymous`, and never root.

Again, on an open network this is **super dangerous** and you probably
shouldn't do it. This option exists so that you can pair it with a network
that limits who can reach the server, such as
[your own VPN](#vpns).

## VPNs

You can use any network device or custom VPN you want, such as WireGuard,
ZeroTier, NetBird, etc. To do this, listen directly on the proper network
device:

```sh
rex server start \
  --extra-listen http://10.0.0.5:7297 --allow-anonymous
```

Use the VPN address itself. An address without a host, such as `:7297`, listens
on every address the machine has, including those on its other networks.

**Everyone who can reach that address has full access.** Rex can't tell callers
apart, so each one can do anything the server can, including starting programs
as its account. On a private VPN, that means everyone on the VPN and every
account on this machine. If your VPN has access rules, use them to limit which
devices can reach this port. The login server limits these callers as described
in [Plain HTTP](#plain-http).

The listener depends on the VPN being up. A server that can't listen on an
address doesn't start at all, not even on its Unix socket. So put these options
on the service's command line, as in [Tailscale Serve](#tailscale-serve), rather
than relying on `rex server start`, which doesn't save its options or change a
server that's already running. systemd and FreeBSD try again every 5 seconds, so
the server comes up soon after the VPN connects.

To connect, add `http://10.0.0.5:7297` as a host on a device that's on the VPN.
Some devices run only one VPN at a time, so turning on another one, including
Tailscale, disconnects Rex.

## Adding the Server to a Client

In Rex for Mac, choose **Add Remote Host…** and enter the server's address.

On the command line, pass the address with `-S`, or save it as a host and
use its label:

```sh
rex -S https://build.example.ts.net ls
rex hosts add build https://build.example.ts.net
rex -S build ls
```

A bare host name, such as `build.example.ts.net`, tries HTTPS on port 443,
then HTTPS on port 7297, then HTTP on port 7297.

To connect to the login server as a specific account, put it before the
host, such as `https://root@build.example.ts.net`. See
[Choosing an Account](https://www.superlogical.com/rex/docs/linux/login-server.md#choosing-an-account).

**Note: `rex hosts` today is CLI-only.** It doesn't sync to your GUI client
or across machines. This will change in the future.

## NixOS

The NixOS module has options for each listener:

| Option                                           | Sets up                                                     |
| ------------------------------------------------ | ----------------------------------------------------------- |
| `services.rex.tailscale.enable`                  | Built-in Tailscale, with `hostname`, `authKeyFile`, and `advertiseTags` |
| `services.rex.tailscaleServe.enable`             | Tailscale Serve for the shared server                       |
| `services.rex.loginServer.tailscaleServe.enable` | Tailscale Serve for the login server                        |
| `services.rex.allowAnonymous`                    | Plain HTTP for the shared server. `openFirewall` opens its `port`. |
| `services.rex.loginServer.allowAnonymous`        | Plain HTTP listeners for the login server                   |

With `socketActivation` on, the module's socket unit also holds the Serve
socket, so a request through Tailscale Serve starts the server.

## Windows

On Windows, each listener works as follows:

| Listener           | On Windows                                                        |
| ------------------ | ----------------------------------------------------------------- |
| SSH                | Works, with Microsoft's OpenSSH Server. See [SSH with Windows](#ssh-with-windows) |
| Built-in Tailscale | Works, with `--tailscale`                                         |
| Tailscale Serve    | Refused, and so is `login-server://`                              |
| Tailnet address    | Works through `tailscaled` with its normal network adapter. Refused with a userspace `tailscaled` |
| Plain HTTP         | Works, with `--extra-listen` and `--allow-anonymous`              |
| Another VPN        | Works, with `--extra-listen` and `--allow-anonymous`              |

To keep listeners each time the server starts, give them to
`rex server install` after `--`, with or without `--user`:

```powershell
rex server install -- --tailscale
rex server install -- --extra-listen http://10.0.0.5:7297 --allow-anonymous
```

`rex server start` takes the same options, but they last only until the
server stops. See [Server Flags](https://www.superlogical.com/rex/docs/windows/running.md#server-flags).

### SSH with Windows

[SSH](#ssh) needs Microsoft's OpenSSH Server installed
and configured. From a terminal with **Run as administrator**:

```powershell
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
Set-Service sshd -StartupType Automatic
Start-Service sshd
```

Its firewall rule lets SSH in only on networks set to Private. For an
administrator account, sshd reads keys from
`C:\ProgramData\ssh\administrators_authorized_keys`, not from
`~\.ssh\authorized_keys`.

### The Firewall

The first time a server that you started listens on a TCP port, Windows
Security asks "Do you want to allow public and private networks to access
this app?" for Rex Server. Choose **Allow**.

**Cancel leaves the port blocked.** Windows adds Block rules named
"Rex Server" as soon as the prompt appears, and Cancel keeps them. Later
starts don't ask again, and connections time out. To undo it, go to
**Windows Security > Firewall & network protection > Allow an app through
firewall** and allow Rex Server, or delete the rules from a terminal
started with **Run as administrator**:

```powershell
Remove-NetFirewallRule -DisplayName "Rex Server"
```

**The Windows Service requires manual setup.** Nobody may be signed in to see a
prompt, so Windows blocks its port without one, and adds no rules. Allow
`rex.exe` ahead of time from a terminal started with
**Run as administrator**:

```powershell
New-NetFirewallRule -DisplayName "Rex Server" -Direction Inbound `
  -Program "C:\Program Files\Rex Server\rex.exe" -Action Allow
```

For the WinGet zip, use the path that `(Get-Command rex).Source` prints. A
Block rule for the same path wins over this one, so delete those first.

Each rule names one path of `rex.exe`. Switching to another WinGet channel,
or between the WinGet zip and the MSI, moves `rex.exe`, so you need to
allow it again. An MSI upgrade keeps its folder, and its rules.

---
title: "Upgrading"
canonical_url: https://www.superlogical.com/rex/docs/linux/upgrading
---

# Upgrading

You can upgrade Rex using your standard package manager that you installed
with. If you used the `install.sh` script, running that again is also safe;
it merely detects and invokes your package manager.

On Linux, `install.sh` and standard package managers auto-restart
the running server. On FreeBSD and Windows, nothing restarts automatically;
you must manually restart the server into the latest version. See
[FreeBSD](#freebsd) and [Windows](#windows).

**Upgrading ends every session on a running shared server.** The login
server and personal servers keep their sessions. See
[What Restarts](#what-restarts).

## Upgrading the Package

| Installed with   | Upgrade                                                            |
| ---------------- | ------------------------------------------------------------------ |
| Debian, Ubuntu   | `sudo apt-get update && sudo apt-get install --only-upgrade rex`   |
| Fedora, RHEL     | `sudo dnf upgrade --refresh rex`                                   |
| openSUSE         | `sudo zypper update rex`                                           |
| Arch Linux       | `sudo pacman -Syu`                                                 |
| Omarchy          | `omarchy update`                                                   |
| Alpine           | `apk update && apk add -u rex`                                     |
| FreeBSD          | `pkg upgrade -r rex rex`                                           |
| Nix profile      | `nix profile upgrade --regex '.*superlogical.*'`                   |
| Install script   | Run the same command you installed with.                           |
| `--tarball`      | Run the install script again with `--tarball`.                     |
| Amazon Linux 2   | Run the install script again.                                      |
| Windows          | See [Windows](#windows).                                           |

When you run the install script again, pass the same `--channel` you
installed with. Without it, the script switches the repository to the
default channel.

## What Restarts

The package restarts the Rex services that are running, and leaves stopped
ones stopped. In containers without a running init system, it restarts
nothing, and neither does the FreeBSD package. See [FreeBSD](#freebsd).

The package never restarts `rex.socket` or `rex-login-server.socket`. A
server that a socket started restarts like any other, and one that hasn't
started yet runs the new version when the first client connects. If a new
version changes a socket unit, the change applies once you stop the socket
and its service and start the socket again, or at the next reboot.

| Server          | On upgrade        | Sessions | Runs the new version    |
| --------------- | ----------------- | -------- | ----------------------- |
| Shared server   | Restarts          | End      | Right away              |
| Login server    | Restarts          | Survive  | Right away              |
| Account servers | Keep running      | Survive  | When their sessions end |
| Personal server | Keeps running     | Survive  | When it stops           |

**Restarting the shared server ends every session and every program in
them.** Upgrade when nobody has work running, or
[hold the package](#holding-an-upgrade) until then.

The servers the login server starts for each account keep running the
version they started with. Each one stops around 10 seconds after its last
session ends and its last client disconnects, and the next connection
starts it again on the new version. On machines with systemd-logind, a
person can switch sooner by running `rex server stop` on the machine as
themselves, which ends their sessions.

A personal server that a `rex` command started stops on its own once it's
idle, so it picks up the new version by itself. One started with
`rex server start` keeps running the old version until you stop it and
start it again:

```sh
rex server stop
rex server start
```

## Checking the Version

`rex version` shows the version that's installed, and `rex server status`
shows the version of the server that's running:

```sh
rex version
rex server status
```

If the first line of `rex version` doesn't match the `version:` line of
`rex server status`, that server is still running the old version. Through
the login server, `rex server status` shows the version of your account's
server.

## Holding an Upgrade

To keep Rex at its current version until you're ready:

| Package manager | Hold                          | Release                         |
| --------------- | ----------------------------- | ------------------------------- |
| apt             | `sudo apt-mark hold rex`      | `sudo apt-mark unhold rex`      |
| dnf             | `sudo dnf versionlock add rex` | `sudo dnf versionlock delete rex` |
| zypper          | `sudo zypper addlock rex`     | `sudo zypper removelock rex`    |
| pacman          | Add `IgnorePkg = rex` under `[options]` in `/etc/pacman.conf` | Remove that line |
| apk             | `apk add rex=VERSION`         | `apk add -u rex`                |
| pkg             | `pkg lock rex`                | `pkg unlock rex`                |

On RHEL and its rebuilds, `versionlock` is a plugin. Install it with
`sudo dnf install 'dnf-command(versionlock)'`. Fedora has it built in.

On Alpine, `VERSION` is the full version, such as `0.1.0-r0`.
`apk info -e -v rex` shows the one that's installed.

## Keeping Your Changes

Upgrades keep your changes to:

- The accounts file, `/etc/rex/accounts.jsonc`
- The PAM policy, `/etc/pam.d/rex-login-server`
- Drop-ins for `rex.service`, `rex-login-server.service`, and their sockets
- `/etc/conf.d/rex` and `/etc/conf.d/rex-login-server` on Alpine
- `/etc/rc.conf` on FreeBSD

Upgrades replace the services themselves, `rex.service`,
`rex-login-server.service`, `rex.socket`, `rex-login-server.socket`, the
scripts in `/etc/init.d`, and the scripts in `/usr/local/etc/rc.d` on
FreeBSD. On FreeBSD, they replace the PAM policy too. See
[FreeBSD](#freebsd). Put your changes in a drop-in or `/etc/conf.d`
instead, as
[Customizing the Service](https://www.superlogical.com/rex/docs/linux/shared-server.md#customizing-the-service)
shows.

If you changed a config file and a new version of Rex changes it too, you
keep yours, and the new one is saved beside it:

| Package manager | New file saved as | Notes                                           |
| --------------- | ----------------- | ----------------------------------------------- |
| apt             | `.dpkg-dist`      | apt asks first. The install script doesn't ask. |
| dnf, zypper     | `.rpmnew`         |                                                 |
| apk             | `.apk-new`        |                                                 |
| pkg             | `.sample`         | Only the accounts file. pkg updates the sample on every upgrade. |

Compare the two and copy over any changes you want. The SSH fallback,
`/etc/ssh/ssh_config.d/50-rex.conf`, is the exception on dnf and zypper: an
upgrade replaces it and saves yours as `.rpmsave`.

## Client and Server Versions

Rex for Mac and the server don't need to be the same version, and either
one can be newer. A newer client can't use features an older server
doesn't have. When it needs one, it says so:

```
this server is too old for rex client ls; it needs Rex 0.1.0 or later
```

If the two are too far apart to work together at all, the server refuses
the connection, and the message ends with `update the server` or
`update the client`.

## Switching Channels and Downgrading

[Switching channels](https://www.superlogical.com/rex/docs/linux/installation.md#channels) from snapshot to preview
or stable upgrades Rex as usual. Switching back to snapshot is a downgrade,
because snapshot versions sort below every release. A downgrade doesn't
happen on its own, so ask for the version you want:

| Package manager | List versions                       | Downgrade                                         |
| --------------- | ----------------------------------- | ------------------------------------------------- |
| apt             | `apt-cache madison rex`             | `sudo apt-get install --allow-downgrades rex=VERSION` |
| dnf             | `dnf list --showduplicates rex`     | `sudo dnf downgrade rex-VERSION`                  |
| zypper          | `zypper search -s -x rex`           | `sudo zypper install --oldpackage rex-VERSION`    |
| pacman          | One version only. See [Arch Linux](https://www.superlogical.com/rex/docs/linux/installation.md#arch-linux). | `sudo pacman -U URL`        |
| apk             | `apk search -a -x rex`              | `apk add rex=VERSION`                             |
| pkg             | `pkg search -r rex rex`             | See [FreeBSD](#freebsd).                          |

A downgrade restarts services the same way an upgrade does. Your data is
safe across a downgrade. A version of Rex that finds data written in a
newer format refuses to use it instead of overwriting it.

## Changing How Rex Is Installed

Keep one copy of Rex on a machine. The `--tarball` binary in
`/usr/local/bin` comes before the package's `/usr/bin/rex` on the usual
`PATH`, so with both installed, `rex` commands and the personal servers
they start run the tarball's version, whichever one you upgrade. To see
every `rex` on your `PATH`:

```sh
which -a rex
```

To move from `--tarball` to a package, install the package, then remove
the binary:

```sh
sudo rm /usr/local/bin/rex
```

To move from a package to `--tarball` or Nix, remove the package first:

| Package manager | Remove                    |
| --------------- | ------------------------- |
| apt             | `sudo apt-get remove rex` |
| dnf             | `sudo dnf remove rex`     |
| zypper          | `sudo zypper remove rex`  |
| pacman          | `sudo pacman -R rex`      |
| apk             | `apk del rex`             |
| pkg             | `pkg delete rex`          |

Removing the package stops and disables its services and sockets, which
ends every session on the shared server. `rex-bin` from the AUR doesn't, so
stop and disable them yourself first. See
[Arch Linux](https://www.superlogical.com/rex/docs/linux/installation.md#arch-linux).

Removing the package keeps `/var/lib/rex` and the `rex` account, so
installing the package again picks up the shared server's data. On
FreeBSD, it keeps `/var/db/rex`, and your settings in `/etc/rc.conf`.

The install script and the commands on the other install tabs set up the
same repository, so you can switch between them. On Alpine, the script
puts the repository in `/etc/apk/repositories.d/rex.list`, and the
**Alpine** tab adds it to `/etc/apk/repositories`. Keep only one of them.

## NixOS

Update the Rex input in your flake, then switch:

```sh
nix flake update rex
sudo nixos-rebuild switch --flake .
```

`nixos-rebuild switch` restarts the Rex service when Rex changes, the same
as a package upgrade: the shared server's sessions end, and account servers
keep the old version until their sessions end.

`flake.lock` holds Rex at its current version until you update it, so
there's nothing else to do to hold an upgrade.

## FreeBSD

pkg can't restart services, so after an upgrade, the services that were
running keep the old version. pkg says which ones at the end of its output,
and so does the install script. Restart each one when you're ready:

```sh
service rex restart
service rex-login-server restart
```

**Restarting `rex` ends every session on the shared server**, the same as
on Linux. Restarting `rex-login-server` keeps each account's server and its
sessions, and they keep the old version until their sessions end.

Upgrades keep your changes to the accounts file, but replace the PAM policy
in `/usr/local/etc/pam.d`. To change the policy, edit a copy in
`/etc/pam.d` instead, as [Login Server](https://www.superlogical.com/rex/docs/linux/login-server.md#freebsd) shows.

pkg doesn't downgrade with `pkg install`, so delete the newer version
first. Deleting stops both services, which ends every session on the
shared server:

```sh
pkg delete rex
pkg install -r rex rex-VERSION
```

`VERSION` is the full version, such as `0.0.1792508098.g07b896`.
`pkg info rex` shows the one that's installed.

## Windows

Upgrade Rex the same way you installed it:

| Installed with        | Upgrade                                                              |
| --------------------- | -------------------------------------------------------------------- |
| WinGet                | `winget upgrade --id Superlogical.RexServer.Preview --source rex`    |
| Manual MSI | Download the new [MSI](https://www.superlogical.com/rex/docs/windows/installation.md#msi) and run it.         |
| Manual Zip| Stop the server, then extract the new zip over the old folder.        |

**Stop the server before you upgrade with WinGet.** WinGet can't replace
the files of a running server, so the upgrade fails with "Access is
denied" and Rex stays at the old version.

In all cases, nothing automatically restarts. How you restart your server
depends on how it was started, as
[Server Lifetimes](https://www.superlogical.com/rex/docs/windows/running.md#server-lifetimes) explains:

| Started by                  | To run the new version                                         |
| --------------------------- | -------------------------------------------------------------- |
| Any `rex` command           | Nothing. It stops once it's idle, and the next command starts the new version. |
| `rex server start`          | `rex server stop`, then `rex server start`                     |
| `rex server install --user` | `rex server stop`, then `rex server install --user`, or sign in again |
| `rex server install`        | `rex server stop`, then `sc.exe start RexServer-<SID>`         |

`sc.exe start` needs a terminal opened with **Run as administrator**. See
[Stopping and Restarting](https://www.superlogical.com/rex/docs/windows/running.md#stopping-and-restarting).

**Warning! Restarting the server ends every program running in its sessions.**
The server saves its session layouts but the programs that were running
in them don't come back today.

### Holding an Upgrade

To keep WinGet from upgrading Rex until you're ready, pin it:

```powershell
winget pin add --id Superlogical.RexServer.Preview --source rex
```

To see your pins, and to remove this one:

```powershell
winget pin list --source rex
winget pin remove --id Superlogical.RexServer.Preview --source rex
```

`winget pin list` shows nothing without `--source rex`, because the source
is explicit. An MSI or zip you installed yourself is never upgraded until
you install a new one.

---
title: "Community MCP"
canonical_url: https://www.superlogical.com/rex/docs/beta/community-mcp
---

# Community MCP

Rex documentation, the changelog, and current and past discussions from the
Rex Beta Discord are available to agents as an MCP server.

An agent can use it to search documentation and past commentary and issues from the community,
along with the Superlogical team's commentary and feedback. It can also read the release notes
for every Rex version posted in #changelog.

It's useful for learning about Rex, for seeing what's new or coming, and for
finding the thinking behind decisions that is buried in archived threads. It is 
behind Discord OAuth, and you need to be a member of the Rex Beta Discord server.

## Install


**Note:** We recommend naming the server `superlogical` rather than `rex`,
so it doesn't conflict with Rex skills, instructions, and the CLI.
