Project configuration
Run lazyshell in a directory holding a lazyshell.yml and it starts the
sessions that file declares — each in its own directory, with its own environment and command —
instead of coming up empty.
The idea
lazyshell init writes a commented starting point in the current directory. Sessions
start in file order, and the first one is selected. An entry that does not validate (empty or
duplicate name, missing cwd) is skipped and reported in the status
bar — the others still start.
Example
# ./lazyshell.yml
# Optional: overrides the user config's shell, for this project only.
shell: /bin/zsh
# Optional: .env files loaded for every session below, in order — a later
# file overrides a key set by an earlier one.
env_files:
- .env
- .env.local
# Optional: declares the groups this project uses, and — the reason to write
# this block at all — the order their headers appear in the sessions panel.
groups:
- name: services
- name: agents
sessions:
- name: api
# Optional: the group this session starts in. Change it later with `g`.
group: services
# Relative to *this file*, not to where you launched lazyshell from.
# `~` is expanded. Left out, it means this file's own directory.
cwd: ./services/api
# Typed into the shell once it is up, not exec'd in its place: when the
# command exits (or you Ctrl-C it), the shell is still there.
command: make dev
env:
PORT: "3000"
# Optional: on top of env_files above, for this session only.
env_files:
- .env.api
# Optional: notify when a line matches. Toggle one on the fly with `v`.
watch:
- pattern: "ERR!"
notify: true
# Optional: never (default) | on-failure | always. Restarts the shell
# automatically when the command exits, with a delay that doubles each
# consecutive attempt (1s, 2s, 4s... capped at 60s) and resets once a
# restarted run stays up 10s. "R" (or "W" for the group) restarts right
# away, bypassing the wait.
restart: on-failure
# Optional, false by default. When the command exits non-zero, kill the
# session outright instead of leaving the shell open underneath — the
# opposite of the default just above. Has no effect without a command:
# there is nothing to watch.
stop_on_failure: false
# Optional. A session declaring a `command:` starts *locked* — you see its
# output, your keys do not reach it, so a stray Ctrl-C cannot kill it. Set
# it explicitly to override: `false` to be able to type in it right away,
# `true` to lock a plain shell.
locked: false
- name: web
group: services
cwd: ./web
command: npm run dev
- name: shell # no group: shown under "ungrouped", at the bottom
A session that declares a command: starts locked, unless it says
locked: false. Locked means the output panel shows it but does not forward your
keystrokes to it: you can scroll, search and copy, and a mistyped q or a
Ctrl-C meant for something else cannot kill the command. i or
Enter takes the keyboard back, the prefix key (Ctrl-O) or
Esc Esc gives it up again — and lazyshell remembers, per session,
whichever you chose last, so moving through the list with j/k lands you
in the state each session was left in. A bare shell declares no command and so starts ready to
type in.
Groups are a display grouping — see Groups. Declaring one here is
optional and only fixes the order of the headers: a session may name a group this block does not
list, and it simply sorts after the declared ones. A bad groups: entry (empty or
duplicate name) is dropped and reported like a bad session, and the rest still apply.
Field reference
| Key | Type | Default | Effect |
|---|---|---|---|
shell | string | "" | Overrides the user config's shell, for this project only. |
env_files | []string | [] | .env-style files loaded, in order, for every session this project declares — before each session's own env_files, and before its inline env. |
no_default_env | bool | false | Disables the automatic <session cwd>/.env lookup for every declared session, unless a session's own no_default_env overrides it back on. |
groups | list of {name} | [] | Declares this project's groups and the order their headers appear in the sessions panel. A session may name a group not listed here; it simply sorts after the declared ones. |
sessions | list of session entries | [] | Started in file order; the first one is selected. |
sessions[].name | string, required | — | Must be non-empty and unique; an invalid entry is skipped and reported, the others still start. |
sessions[].group | string | "" (ungrouped) | Need not be declared in groups:. |
sessions[].cwd | string | this file's own directory | Resolved relative to this file, not to where you launched lazyshell from. ~ is expanded. |
sessions[].command | string | "" (bare shell) | Typed into the shell once it is up, not exec'd in its place: when it exits (or you Ctrl-C it), the shell is still there. |
sessions[].env | map[string]string | {} | Always wins, over every .env file layer (see below). |
sessions[].env_files | []string | [] | On top of the project's own env_files, for this session only. |
sessions[].no_default_env | bool | inherits the project's setting | Overrides it for this session only, in either direction. |
sessions[].watch | list of {pattern, notify} | [] | A regex evaluated against each output line, and whether a match notifies. Toggle one on the fly with v. |
sessions[].restart | never | on-failure | always | never | Restarts the shell automatically when the command exits, with a delay that doubles each consecutive attempt (1s, 2s, 4s… capped at 60s), reset once a restarted run stays up 10s. R (or W for the group) restarts right away, bypassing the wait. |
sessions[].stop_on_failure | bool | false | Kills the session outright as soon as command: exits non-zero, instead of leaving the shell open underneath. An explicit kill always wins over a pending restart:. No effect without a command:, reported on stderr if declared that way. |
sessions[].locked | bool | true if command: is declared, else false | An explicit value always wins over the heuristic. |
What is read, what is not
Only shell, env_files, no_default_env,
groups and sessions are read from a project file. theme,
keybindings, prefix_key and the rest stay under your control alone: a
repository you cloned must not be able to remap your keyboard. Other keys are ignored, with a
warning on stderr.
A group declares a name and nothing else — no colour, no glyph, no key. Same rule: a repository
says what exists, not what your interface looks like. A session's watch: entries
follow it too — a pattern and whether it notifies, nothing about how a match is shown. So does
restart: — a policy and nothing else, no per-policy tuning of the backoff or a
maximum-attempts knob. stop_on_failure: is the one exception to "the shell is still
there" documented above: paired with restart:, an explicit kill always wins over a
pending automatic restart, the same rule systemctl stop gives
Restart=on-failure. locked: is the one key that touches the interface at all,
and it is allowed because what it protects is the declared process itself: the worst a file you
cloned can do with it is make you press i.
.env files
Every session — declared in a project file or not — automatically loads a .env from
its own working directory, if there is one. Layered on top, each overriding a key the previous
layer set:
<session cwd>/.env, automatic, unless disabled (see below)--env-file <path>(repeatable, applies to every session this run starts)- the project's own
env_files:(applies to every declared session) - a session's own
env_files:(that session only) - that session's
env:map — always wins, over every file
To stop the automatic <cwd>/.env lookup:
--no-env-file— every session this run starts;no_default_env: trueat the top of a project file — every session it declares;- or on one session entry — that session only, overriding the project's own setting in either direction.
Which file is used
--config-file <file>(-f)$LAZYSHELL_PROJECT_CONFIG./lazyshell.yml./.lazyshell.yml
Only the current directory is searched — no walking up to a repository root, so the file that runs is always the one you can see.
Approving a project file
A lazyshell.yml is versioned in a repository, so it would otherwise run arbitrary
commands the moment you cd into a clone. lazyshell asks once, before the interface
opens, and remembers the answer per file — and asks again as soon as the file's content changes.
$ lazyshell allow # approve the current directory's file, launch nothing
$ lazyshell allow ./x.yml # approve a specific file
$ lazyshell --no-autostart # open the interface without starting anything
$ lazyshell --env-file .env.prod # extra .env file, for every session this run starts
$ lazyshell --no-env-file # skip the automatic "<cwd>/.env"
Approvals live in trust.yml next to your user config. When stdin is not a terminal,
approval is refused rather than assumed.
Layout persistence
When you quit, lazyshell saves each session's name, group, working directory and launch command
for the current directory to ~/.config/lazyshell/state/<hash-of-the-directory>.yml —
not anything live inside the shell, just the recipe it was started with. Launch it again from that
same directory with no lazyshell.yml present, and it offers to restore that layout
instead of starting the usual lone default session.
restore_layout controls what "offers"
means: ask (the default) shows a confirmation popup naming the sessions it would
recreate; always restores them with no prompt; never never offers it,
though the layout keeps being saved regardless — so switching back to ask/always
later still finds it. Declining the popup leaves the session list empty, the same place
--no-autostart leaves you, with n right there to start one by hand.
A lazyshell.yml in the directory always wins: its declared sessions are what starts,
and the saved layout is never even read, only kept up to date in case the file is later removed.