Workspaces config
Workspaces live in ~/.config/zen-term/workspaces, one [Title] section per workspace. ZenTerm does not scan directories, so the list is yours to curate. Each section becomes an entry in the ⌘⇧P picker, and opening it applies the section as a recipe: the first pane runs main, the drawers open their commands, and focus lands where you set it. Copy the annotated sample to start.
Fields
[Title]requiredsection header → picker label + sidebar name
pathrequiredworking directory (`~` is expanded)
taboptionalstarts a tab; `tab = name` names it, a bare `tab` does not
mainoptionalthe tab's first-pane program; absent, empty or `shell` → a plain shell
rightoptionalthe tab's right-drawer command; absent or empty → closed; `shell` → open with a plain shell
bottomoptionalthe tab's bottom-drawer command; absent or empty → closed; `shell` → open with a plain shell
focusoptionalmain | right | bottom, in the tab it sits in (default main, first tab)
envoptionalKEY=VALUE, repeatable; injected into every surface
carryoptionala path to copy into a new worktree, repeatable
Sample workspaces file
# zen-term: workspaces reference
#
# Copy to ~/.config/zen-term/workspaces (or $XDG_CONFIG_HOME/zen-term/workspaces) and
# add a section per workspace. This list is deliberate and hand-curated. zen-term does
# NOT scan any directory. Each section becomes an entry in the ⌘P picker.
#
# Format: INI-style. A `[Title]` line opens a section; `key = value` lines below it, and
# bare `tab` lines, configure that workspace until the next header. Lines starting with
# `#` and blank lines are ignored; unknown keys are ignored. A section with no `path` is skipped, a
# malformed `env` line is dropped, a `carry` entry that leaves the workspace is dropped,
# and nothing here can crash the app (bad lines are logged and skipped). Manage workspaces
# from inside the app too, in Settings → Workspaces (add / edit / delete / reorder), or the
# Add Workspace… row at the foot of the ⌘P picker. A workspace added either way opens.
# Hand-edits to this file are picked up the next time you open the ⌘P picker (or on
# restart).
#
# The section header is the title: it's shown in the picker and names the workspace in the
# sidebar, so it's independent of the folder name (`[ZenTerm]` can point at ~/Dev/zen-term).
#
# Order: the picker's Configured section lists workspaces in the order their sections
# appear here, so to put one first, move its whole `[Title]` block up. (Above it, Open and
# Open Elsewhere list what is already running, in the order the sidebar holds them.) ⌥↑/⌥↓ on a row in Settings → Workspaces
# does the same edit for you. A comment sitting directly on a header moves with it; a
# comment with a blank line under it stays where it is, so a banner at the top of the
# file keeps its place.
#
# Tabs: a `tab` line starts a tab, and the main / right / bottom / focus lines under it
# belong to that tab until the next `tab` line. `tab = name` names it, and the tab bar
# shows that name; a bare `tab` (or `tab =`) leaves it unnamed, and the tab bar shows its
# live title. Indenting the lines under a tab is optional. path, env and carry belong to
# the whole workspace wherever they appear.
#
# Opening a workspace starts its tabs in order. In each tab the first pane runs `main`,
# the drawers named by `right`/`bottom` open running their commands, and `env` is injected
# into every pane and drawer. A worktree of the workspace starts the same tabs.
#
# One `focus` line for the whole workspace says where the cursor lands: the tab it sits
# in, and the region it names. With more than one, the last wins. Every other tab opens
# with its main pane focused. Without one, focus lands on the first tab's main pane.
#
# A section with no `tab` line is one unnamed tab: its main / right / bottom / focus lines
# describe it directly. This is how files written before tabs existed read, and a
# workspace with a single unnamed tab is still written that way. A section with only a
# `path` is the minimal default: one tab, one shell pane, both drawers closed, cwd = the
# workspace dir.
#
# Fields:
# [Title] (required) section header → picker label + sidebar name
# path (required) working directory (`~` is expanded)
# tab (optional) starts a tab; `tab = name` names it, a bare `tab` does not
# main (optional) the tab's first-pane program; absent, empty or `shell` →
# a plain shell
# right (optional) the tab's right-drawer command; absent or empty → closed;
# `shell` → open with a plain shell
# bottom (optional) the tab's bottom-drawer command; absent or empty → closed;
# `shell` → open with a plain shell
# focus (optional) main | right | bottom, in the tab it sits in (default main,
# first tab)
# env (optional) KEY=VALUE, repeatable; injected into every surface
# carry (optional) a path to copy into a new worktree, repeatable
#
# A value with spaces may be quoted: bottom = "npm run dev", tab = "dev server".
#
# About carry: a git worktree gets everything git tracks and nothing it ignores, so a
# fresh one has no .env, no node_modules and no virtualenv, and cannot run the project
# until you install it again. Name here what a worktree of this workspace needs to run.
# On APFS the copy shares its disk blocks with the original, so carrying a node_modules
# costs close to no space and close to no time. On another volume it is a real copy,
# which still works and takes longer. Paths are relative to the workspace and may not
# leave it. A nested path works: config/credentials/development.key is copied there, and
# any directory it needs on the way is created.
#
# What carry will not do: a tracked file is refused, because copying one into a worktree
# makes git report a change that never goes away. A folder git tracks something inside is
# not refused: its ignored content comes across and the tracked files stay behind. An entry the worktree
# already has is left alone. An entry this workspace does not have is skipped, so one
# section can cover a repo before and after its first install.
#
# A carried install is the one from this checkout. Open a worktree on a branch that
# changed a lockfile and the install is still the old one until you run your package
# manager in it.
#
# Setting carry: the workspace form (Settings → Workspaces, or Add Workspace… in the ⌘P
# picker) calls this "Copy into new worktrees". It lists what git ignores in the folder
# and you tick what to copy. Editing this file by hand does the same thing.
#
# Making a worktree: ⌥⏎ over a row in the ⌘P picker, or the + that shows when you hover
# a workspace in the sidebar. Name the branch, pick where it starts, and the carry
# entries below come across with it. The worktree opens nested under its workspace.
#
# Example: one tab with an editor, an agent and a shell (uncomment and edit, or write
# your own):
#
# [ZenTerm]
# path = ~/Dev/zen-term
# main = nvim
# right = claude
# bottom = shell
# focus = main
# env = NODE_ENV=development
# env = PORT=3000
# carry = .env
# carry = node_modules
# carry = config/credentials/development.key
#
# Example: three tabs, with focus on the agent in the first:
#
# [Web]
# path = ~/Dev/web
# env = LOG_LEVEL=debug
#
# tab = code
# main = nvim
# right = claude
# focus = right
#
# tab = server
# main = "npm run dev"
#
# tab
# main = lazygit