Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Huskarl AI colleagues on your own Mac Starf An activity monitor for Apple silicon that never invents a number etrans An SSH and SFTP client for macOS Elyra VM Virtual machines for macOS, Linux and Windows on your Mac Litr A small, native web browser for macOS Notr A notebook for macOS e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Refr Local-first PDF workspace for macOS Elyra Workspace A desktop workspace for coding agents Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Release notes
Changelog
Elyra
Configuration

Configuration

Settings live in ~/.config/eterm/settings.toml and are written on every change made in the settings window (cmd-,). The file can also be edited by hand; eterm reads it at startup.

The file is not inside the application, so upgrading never touches it. It also survives being unreadable, which matters more than it sounds: settings are saved on every change, so defaults loaded into memory would otherwise become defaults on disk the moment you touched a toggle.

  • A file that cannot be parsed is moved to settings.toml.broken and the settings window says so along the bottom, with the reason and the line. Nothing is lost; copy back what you want.
  • The previous file is kept as settings.toml.bak before each save.
  • Saving writes a temporary file and renames it over the old one, so a crash or a full disk halfway through cannot leave a half-written file.
  • Anything the file does not mention takes its default, and anything eterm does not recognise is ignored — so a file written by an older or a newer version still loads.

Everything below is optional. An empty file is a valid file.

appearance

[appearance]
palette = "palenight"      # one of the 22 built-in palettes
font_family = ""           # empty picks the best installed monospace font
font_size = 14.0
line_height = 1.4          # multiplier, not pixels
letter_spacing = 0.0       # extra pixels between cells
padding = 18.0             # around the grid
cursor_style = "block"     # block, bar, underline
show_footer = true         # the directory and title strip at the bottom
highlight_input = true     # colour the command line as you type it
warn_destructive = true    # and red, with the reason in the footer, when it looks destructive
ligatures = true           # => and != drawn as the font joins them
font_features = []         # OpenType features: "ss01", "+cv05", "-liga"
font_fallback = []         # families to try for a character the font lacks

highlight_input turns the first word green when it is a command that exists. It needs shell integration, which is what reports where the prompt ends, and it never touches a cell the shell has already coloured — so a prompt framework or zsh-syntax-highlighting keeps its own work.

warn_destructive paints the whole line red while what is typed has one of the shapes AI mode holds a proposal back for — rm -rf, git push --force, DROP TABLE, curl … | sh, about forty of them — and the footer says which: this deletes files recursively or without asking. It is a second look offered before the command runs rather than after, and it is only a look: return still runs it. The red does override a prompt framework's colours, because a warning that gives way to somebody's yellow is not one. Same requirement as highlight_input: the shell has to say where the prompt ends.

A word we do not recognise is left alone rather than marked wrong, because it may be an alias or a shell function, and neither of those is visible from here.

The palette list is in the Appearance pane. Eight of them are designed to sit on a shader surface; four of those are light.

font_family is resolved against what is installed. If the named font is missing, eterm falls back through JetBrains Mono, SF Mono, Berkeley Mono, IBM Plex Mono, Fira Code, Menlo and Monaco, and the Appearance pane offers only the ones actually present.

ligatures is the font's calt feature, which is how coding fonts join =>, != and ->; off draws each character alone, as it does in a font that has none. font_features turns other features on and off by their four-letter tags — the stylistic sets (ss01…) and character variants (cv01…) that JetBrains Mono, Fira Code or Monaspace ship — and one written there wins over the switch. A tag that is not four letters and digits is ignored.

font_fallback is where a character the font does not have is looked for, in order: a Nerd Font for the icons a prompt draws, say. Menlo and Apple Color Emoji always come after it.

Every character sits in its own cell whatever font drew it. A character two cells wide — an emoji, a CJK character — or a ligature drawn as one glyph for several characters, does not move what comes after it on the row.

motion

[motion]
cursor_blink = true
blink_interval = 530       # milliseconds per half cycle
smooth_scroll = true
animate_surface = false    # drift the shader surface

animate_surface is off by default because a moving surface means the window never idles: about 5% of a core at 30fps. The surface itself costs 0.10ms a frame when it does move.

shell

[shell]
program = ""               # empty means your login shell
args = []
working_directory = ""     # empty means home
scrollback = 10000

# Directories put in front of the inherited PATH. ~ is expanded.
path = [
    "~/.local/bin",
    "~/.cargo/bin",
    "/opt/homebrew/bin",
    "/opt/homebrew/sbin",
    "/usr/local/bin",
]

# Environment variables for the session. These win over everything eterm sets,
# the shell integration included. A leading ~/ is expanded.
[shell.env]
EDITOR = "hx"

path matters more than it looks. A process launched from the Dock inherits launchd's PATH, which on a stock Mac is four system directories and nothing else, so cargo and brew are missing even though they work in another terminal. The Shell pane shows the result: every directory marked inherited, added or missing, and which of 21 known tools are reachable.

A login shell can still rewrite PATH in its own rc files afterwards. What eterm sets is the starting point, not the last word.

LANG is set for you when nothing else did. An application launched from the Dock inherits no locale — launchd has none to give — and a shell without one works in bytes rather than characters: æøå is three characters and six bytes, and the line editor counts six. Backspace then takes half a letter. eterm sets LANG from the region macOS is set to, falling back to en_US.UTF-8 when that region has no locale installed, and leaves it alone when LANG or LC_ALL already says something.

shell.env is also the way out of the automatic shell integration: eterm points zsh at a ZDOTDIR of its own, and setting ZDOTDIR here is what overrules it.

Secrets on screen

cat .env, and the tokens are on the screen — when somebody is looking over your shoulder, when the screen is shared, and when the screen is about to be handed to a model. With hide_secrets on, anything shaped like a secret is drawn as a bar in the accent colour, and shown only while the pointer is over it. Copying still copies the real text: this is for the shoulder, not for you.

What counts is decided by shape, chosen for precision: GitHub, Anthropic, OpenAI, AWS, Slack, Stripe and Google tokens by their prefixes and lengths, JWTs, the header of a private key, and the value of any SOMETHING_KEY=, _SECRET=, _TOKEN= or _PASSWORD= line of eight characters or more. A commit hash is not a secret and is left alone.

The same shapes are replaced with [redacted] in everything that leaves for a model — the screen sent with a first question, the output of every step after it — and in a block handed to another pane with cmd-shift-s, since what is in that pane is usually a model too. See AI mode.

A tab that wants you

Agents sit and wait in panes you are not looking at. When a program in a session asks for you — with the bell, with iTerm2's OSC 9, urxvt's OSC 777 or kitty's OSC 99, or by saying through OSC 9;4 that it has stopped being busy, which is how Elyra reports progress — the dot on its tab grows to full size and full colour, and the Dock bounces if eterm is not the app in front. The mark clears the moment you type or click in that session.

Claude Code rings the bell by default when it needs an answer; set preferredNotifChannel to iterm2 and it sends OSC 9 instead, which is heard the same way.

While you were away

Come back to the window after half a minute or more elsewhere, and if anything happened in the meantime a line under the session says what:

While you were away · claude is waiting for you · cargo test failed with 101 5 min ago · cargo build finished 8 min ago · OK

Each thing on it is a way to the pane it happened in, wherever that pane is — another tab included — and it leaves the line once read. Waiting comes first, because a program stopped for an answer is time still being lost; failures next; then what merely finished, most recent first. Four things at most, and a count of the rest. Nothing that ended before you left is on it: you saw that. Leaving again clears it, and away_summary = false turns it off.

keys

[keys]
option_as_meta = false

[keys.bind]
"cmd-shift-t" = "new_tab"
"cmd-k" = "none"

[keys.bind] moves, adds and takes away keys; see changing a key.

Off leaves the option key to the system, so dead keys and input methods compose: ⌥U then A is ä. On sends ESC-prefixed keys, which is what readline expects from ⌥B. It cannot be both.

links

[links]
enabled = true             # cmd-click opens paths and addresses in plain text
editor = "default"         # where a file opens: zed, vscode, cursor, idea … or a template

A program can mark a link with OSC 8, and a few do. Most print Listening on http://localhost:3000 or --> src/main.rs:12:5 and leave the rest to whoever is reading. With cmd held, such a thing is underlined; a cmd-click opens an address in the browser and a file in the editor, at the line and column the text named — even inside a program that has taken the mouse, as Claude Code does, because a cmd-click on a link goes to the link first. localhost:5173, 127.0.0.1:8000 and 0.0.0.0:3000 are read as http; www. too. Quotes, brackets and the punctuation of a sentence around a link are not part of it. A path is resolved against the directory the shell reports, and only counts when the file is actually there — v1.2.3 has a dot in it and is not a file — which is also why this works best with shell integration. Python's File "app/x.py", line 42 is understood as well as x.py:42.

Editors are reached by the URL schemes they register — zed://, vscode://, cursor://, windsurf://, subl://, txmt://, x-bbedit://, nova://, and idea://, phpstorm://, webstorm://, pycharm://, goland://, rustrover://, clion:// — so opening a file at a line is opening a URL. e has no scheme, and is started instead, as its own command line starts it: open -n -a e.app --args <file>, with the file as one argument and no shell in between. It is told the file and not the line, which is all it takes. The Links pane offers only the editors found under /Applications. default hands the file to whatever macOS opens it with, and loses the line. Anything else is taken as a template of your own:

editor = "myeditor://open?file={path}&line={line}&column={column}"

{path} is absolute and percent-encoded; {line} and {column} are 1 when the text named none.

ai

[ai]
enabled = false
provider = "anthropic"     # anthropic or open_ai
model = "claude-opus-5"
context_lines = 40         # lines of screen sent with the question
fetched_models = []        # filled in by Refresh in the settings pane

The API key is deliberately not here. See AI mode.

input

[input]
suggestions = true         # the rest of a command you have run before, in grey
completions = true         # a list of commands, options or paths while typing

Both need shell integration. See Suggestions for what they read and what they leave alone.

review

[review]
enabled = true             # cmd-r opens a pull request for reading

Off means the command does not exist and the window cannot be opened. The token is not here — it is in the login keychain, for the same reason the model key is. See Review.

renderer

[renderer]
vsync = true
max_fps = 120

updates

[updates]
check = true               # ask elyracode.com once a day whether there is a newer eterm

Nothing but the request goes out, and nothing is fetched or installed until you press the button. See Updates.

other

[other]
notify_after = 20          # bounce the Dock icon for commands this slow; 0 is off
confirm_on_quit = false
copy_on_select = false     # copy a selection as soon as it is made
bell = true               # a program ringing the bell is asking for you
hide_secrets = true       # tokens and keys drawn as a bar, and kept from models
away_summary = true       # on coming back, one line on what finished and who waits

notify_after only applies when eterm is not the application in front — you are already looking at it otherwise. A command that succeeded bounces the Dock icon once; one that failed keeps bouncing until you come back.

hooks

[hooks]
on_command_finished = ""   # post a finished command here: this Mac or https; empty is off
min_seconds = 0            # only commands that ran at least this long
only_failures = false      # only commands that failed

What is posted, and how to point it at a Huskarl bot, is in Huskarl.

huskarl

[huskarl]
enabled = true             # show Huskarl's waiting proposals on coming back and in cmd-j

See Huskarl.

Session state

~/.config/eterm/session.toml is written by eterm and holds the window size, the tabs, how each was split, the ratios and the directory each pane was in. Deleting it gives you a plain window. Only the window a run starts with restores and records it; a window opened with cmd-n is a new window.

~/.config/eterm/durations.toml is written by eterm too: how long each command has taken in each directory, the last five successful runs of three seconds or more. It is what fills the bar under a tab while a known command runs. Deleting it forgets the times; nothing else depends on it.

A project's layout

A project can say which sessions it wants open, in a .eterm.toml in its root:

[[pane]]
run = "npm run dev"

[[pane]]
run = "npm test -- --watch"
split = "down"

[[pane]]

Each pane is split off the one before it — to the right, or down — and runs its command; a pane without run is a shell. At most six panes are read.

Nothing runs because the file is there. When a session arrives in the directory, a strip at the bottom of the window names every command the layout would run, and Open in a new tab is what runs them. Not now puts it away for the rest of the window's life, and Open this project's layout in the command palette brings it back. A file that does not parse, or a run with a line break in it — two commands wearing one — is not offered at all.