lpm config reference
Every key in a project config file, how lpm writes the first one for you, and how the project, repo, and global layers fit together.
What a project config looks likeTry it: edit a config, watch the app
Add or clone a folder and lpm fills in its dev servers. Change them here, add a profile or an action, and the preview updates as you type.
name: myapproot: ~/Projects/myapp# Long-running servicesservices: api: cmd: python manage.py runserver cwd: ./backend port: 8000 frontend: cmd: npm run dev cwd: ./frontend worker: celery -A backend worker# Named subsets of servicesprofiles: default: [api, frontend] full: [api, frontend, worker]# One-shot commands and terminalsactions: test: pytest migrate: cmd: python manage.py migrate cwd: ./backend confirm: true deploy: ./scripts/deploy.sh claude: cmd: claude label: Claude Code type: terminalname: myapproot: ~/Projects/myapp# Long-running servicesservices: api: cmd: python manage.py runserver cwd: ./backend port: 8000 frontend: cmd: npm run dev cwd: ./frontend worker: celery -A backend worker# Named subsets of servicesprofiles: default: [api, frontend] full: [api, frontend, worker]# One-shot commands and terminalsactions: test: pytest migrate: cmd: python manage.py migrate cwd: ./backend confirm: true deploy: ./scripts/deploy.sh claude: cmd: claude label: Claude Code type: terminalcwd, port and env.ProfilesNamed sets of services. Pick one from the Start menu to boot just that set.ActionsOne-shot commands that show up as buttons — tests, migrations, deploys.TerminalsAn action with type: terminal opens a tab that stays open: Claude Code, Codex, a REPL or a log tail.On this page
Project
A project is one app you work on with lpm — a website, an API, a blog, anything you’d normally start in a terminal. Each one is a file in ~/.lpm/projects/, and it starts with the folder the code lives in.
Edit the YAML — the preview updates live.
No active terminals
Click Start to run myapp.
name: myapp
root: ~/Projects/myapp
services:
web: npm run dev
You don’t have to write this from scratch
Click + in the sidebar and pick a folder, or clone a repository. lpm creates this file for you and fills in the services it finds in the project’s own files — see automatic service detection. Edit it later in the app, as a form or as YAML.
namestringThe project’s name — what the sidebar shows unless you set a
label, and what thelpmCLI calls it. Defaults to the config file’s name, somyapp.ymlis the projectmyapp. Changing it in the app’s config editor renames the file.root*stringThe folder on your computer where this project lives. Every relative path in the config (like
cwd) is resolved from here.~is a shortcut for your home directory (e.g.~/Projects/myapp). Required for local projects; an SSH project has ansshblock instead.labelstringThe name the app shows for the project, when you want something friendlier than
name. Renaming a project in the app changes this, not the file name.sshmapMakes this an SSH project: services, actions, and terminals run on a remote machine instead of this Mac.
hostanduserare required;portdefaults to 22;keypoints to an identity file (leave it out to use ssh-agent or your~/.ssh/config);diris the remote working folder. More in SSH projects on a Mac.claudeAccountstringId of the Claude account this project’s terminals, actions, and AI features use. Accounts are managed in the desktop app under Settings → AI & Integrations, which also writes this field for you via the project form. Omit it to use your main Claude login. Not applied to SSH projects — the remote host has its own Claude login. See multiple Claude Code accounts.
| Field | Type | Description |
|---|---|---|
name | string | The project’s name — what the sidebar shows unless you set a label, and what the lpm CLI calls it. Defaults to the config file’s name, so myapp.yml is the project myapp. Changing it in the app’s config editor renames the file. |
root* | string | The folder on your computer where this project lives. Every relative path in the config (like cwd) is resolved from here. ~ is a shortcut for your home directory (e.g. ~/Projects/myapp). Required for local projects; an SSH project has an ssh block instead. |
label | string | The name the app shows for the project, when you want something friendlier than name. Renaming a project in the app changes this, not the file name. |
ssh | map | Makes this an SSH project: services, actions, and terminals run on a remote machine instead of this Mac. host and user are required; port defaults to 22; key points to an identity file (leave it out to use ssh-agent or your ~/.ssh/config); dir is the remote working folder. More in SSH projects on a Mac. |
claudeAccount | string | Id of the Claude account this project’s terminals, actions, and AI features use. Accounts are managed in the desktop app under Settings → AI & Integrations, which also writes this field for you via the project form. Omit it to use your main Claude login. Not applied to SSH projects — the remote host has its own Claude login. See multiple Claude Code accounts. |
* required
SSH projects. Swap root for an ssh block and the same services, actions, and terminals run on a remote machine. Adding an SSH host in the app writes this for you, with one shell service that opens a login shell on the host. lpm doesn’t scan remote folders for services, so add your own.
name: staging
ssh:
host: staging.example.com
user: deploy
dir: ~/apps/shop
services:
shell: exec "$SHELL" -l # what adding an SSH host writes
api: npm run start
actions:
logs:
cmd: tail -f log/production.log
type: terminal
Automatic service detection
When you add a folder or clone a repository, lpm reads the project’s own files and writes a service for each thing it can run. Nothing is sent anywhere and no AI is involved, so adding a project stays instant. The app then tells you what it found, like “Found 2 services: web, api”.
Where it looks. The project root first, then every workspace member declared in package.json or pnpm-workspace.yaml, then these subfolders if they exist: api, app, backend, client, frontend, server, ui, web, www, and every folder under apps/ or services/.
What it recognizes. Node commands use the package manager your repo declares or locks. Python commands run through the folder’s .venv (or venv), uv, Poetry, or Pipenv when it has one.
- ProcfileProcfile.dev, else ProcfileOne service per process line; release is skipped
Named the process name · port from the command
- Node.jspackage.json dev, start, or serve script
npm run devor pnpm, yarn, bun — whichever the repo declares or locksNamed web, api, or app · port the script's, else the framework's: Next.js 3000, Vite 5173, Astro 4321…
- Denodeno.json dev, start, or serve task
deno task devNamed app · port from the task
- Djangomanage.py
python3 manage.py runserverNamed web · port 8000
- FastAPIfastapi dependency + an app module like main.py
uvicorn main:app --reloadNamed api · port 8000
- Flaskflask dependency + app.py or wsgi.py
flask run --debugNamed web · port 5000
- RailsGemfile with rails + bin/rails
bin/develse bin/rails serverNamed web · port 3000
- Laravelartisan
php artisan serveNamed web · port 8000
- Phoenixmix.exs with :phoenix
mix phx.serverNamed web · port 4000
- Spring BootSpring Boot in pom.xml or build.gradle
./mvnw spring-boot:runmvn spring-boot:run without mvnw; ./gradlew bootRun for GradleNamed web · port 8080
- .NETa single .csproj
dotnet watch runNamed app · port —
- Gogo.mod
go run .air with an .air.toml; go run ./cmd/<name> per command when there's no main.goNamed app, or the command's name · port from the command
- RustCargo.toml with a binary
cargo runcargo run -p <crate> for each workspace binaryNamed app, or the crate's name · port from the command
- Docker Composecompose.yml or docker-compose.yml
docker compose upNamed compose · port —
- make / justMakefile or justfile, when nothing else matched
make devor just dev; serve, run, and start targets work tooNamed app · port —
| Stack | Runs | Named | Port |
|---|---|---|---|
| ProcfileProcfile.dev, else Procfile | One service per process line; release is skipped | the process name | from the command |
| Node.jspackage.json dev, start, or serve script | npm run devor pnpm, yarn, bun — whichever the repo declares or locks | web, api, or app | the script's, else the framework's: Next.js 3000, Vite 5173, Astro 4321… |
| Denodeno.json dev, start, or serve task | deno task dev | app | from the task |
| Djangomanage.py | python3 manage.py runserver | web | 8000 |
| FastAPIfastapi dependency + an app module like main.py | uvicorn main:app --reload | api | 8000 |
| Flaskflask dependency + app.py or wsgi.py | flask run --debug | web | 5000 |
| RailsGemfile with rails + bin/rails | bin/develse bin/rails server | web | 3000 |
| Laravelartisan | php artisan serve | web | 8000 |
| Phoenixmix.exs with :phoenix | mix phx.server | web | 4000 |
| Spring BootSpring Boot in pom.xml or build.gradle | ./mvnw spring-boot:runmvn spring-boot:run without mvnw; ./gradlew bootRun for Gradle | web | 8080 |
| .NETa single .csproj | dotnet watch run | app | — |
| Gogo.mod | go run .air with an .air.toml; go run ./cmd/<name> per command when there's no main.go | app, or the command's name | from the command |
| RustCargo.toml with a binary | cargo runcargo run -p <crate> for each workspace binary | app, or the crate's name | from the command |
| Docker Composecompose.yml or docker-compose.yml | docker compose up | compose | — |
| make / justMakefile or justfile, when nothing else matched | make devor just dev; serve, run, and start targets work too | app | — |
Names and ports. At the root, a service is named for its role — web, api, app, or the Procfile’s process name. In a subfolder, a lone service takes the folder’s name and a cwd pointing there; several share the folder name as a prefix, like backend-web. A port is written when the command names one (--port 3001, PORT=3001, localhost:5173) or the framework has a usual dev port. Adding a folder with a Compose file at the root, a Django app with its own .venv in backend/, and a Vite app using pnpm in frontend/ writes:
name: shop
root: ~/Projects/shop
services:
compose:
cmd: docker compose up
backend:
cmd: .venv/bin/python manage.py runserver
cwd: backend
port: 8000
frontend:
cmd: pnpm dev
cwd: frontend
port: 5173
Limits. Detection stops at 20 services. In a monorepo, the root’s own script (say, turbo dev) is left out once the workspace members turn up services of their own, so nothing starts twice. It runs once, when the project is added, and never rewrites a config you’ve edited.
When nothing is found
lpm writes one placeholder service, dev: echo 'configure me', for you to replace — or let an agent draft the whole file with Generate with AI. SSH projects are never scanned: a new one gets a shell service that opens a login shell on the host. Adding a folder that’s already a project just points you to that project.
Services
Services are the long-running processes that make up your project — your dev server, an API, a background worker, anything you’d normally leave running in a terminal tab. lpm starts them together when you click Start and stops them when you click Stop. Most projects have at least one; a project with none still works as a home for terminals and agents — it just has no Start button.
If a command runs continuously, it’s a service. If it finishes and exits — tests, a build, a migration — it belongs in Actions instead. Each service can be written as a one-line shorthand (just the command) or as the full form when you need cwd, port, or env.
No active terminals
Click Start to run myapp.
name: myapp
root: ~/Projects/myapp
services:
# shorthand — key is the name, value is the command
server: node server.js
# full form — use this when you need cwd, port, env, or dependsOn
web:
cmd: npm run dev
cwd: ./web # run from a subfolder (great for monorepos)
port: 3000 # a label: shown in the Start menu, used by Open in browser
dependsOn: [server] # start "server" first, pulled in automatically
env: # extra env vars just for this service
API_URL: http://localhost:4000
cmd*stringThe shell command that starts the process — exactly what you’d type into a terminal yourself, like
npm run devornode server.js. lpm runs it in the background, where it keeps running even after you quit the app, and streams its output into a read-only pane. lpm doesn’t restart a command that exits on its own.cwdstringStart the service from a different folder than the project root — handy for monorepos where each app lives in its own subfolder like
./apps/web. Relative paths resolve fromroot; absolute paths are used as-is. See Path resolution for~and SSH projects.portintThe port this service listens on. It’s a label and a conflict check, not an assignment: lpm shows it next to the service in the Start menu, offers it under Open in browser until it sees the port the service actually opened, and checks that nothing else holds it when you press Start. lpm doesn’t pass it to your command — put the port in
cmdorenvyourself. Give each service its own port —lpm config validateflags two services that share one.portConflictstringWhat to do when
portis already taken at Start.ask(the default) asks before freeing it,freefrees it without asking, andfailrefuses to start. Freeing stops the other lpm project holding the port, or ends the process that holds it.envmapExtra environment variables to set just for this service — things like
API_URLorNODE_ENV. Useful when you don’t want to commit them to a.envfile.dependsOn[]stringOther services this one needs — name them here and lpm starts them first, pulling them in automatically whenever you start this service (even if you only picked this one). It’s ordering only: the dependencies launch ahead of this service, but lpm doesn’t wait for them to finish booting or start listening before it moves on. Also accepted as
depends_on.
| Field | Type | Description |
|---|---|---|
cmd* | string | The shell command that starts the process — exactly what you’d type into a terminal yourself, like npm run dev or node server.js. lpm runs it in the background, where it keeps running even after you quit the app, and streams its output into a read-only pane. lpm doesn’t restart a command that exits on its own. |
cwd | string | Start the service from a different folder than the project root — handy for monorepos where each app lives in its own subfolder like ./apps/web. Relative paths resolve from root; absolute paths are used as-is. See Path resolution for ~ and SSH projects. |
port | int | The port this service listens on. It’s a label and a conflict check, not an assignment: lpm shows it next to the service in the Start menu, offers it under Open in browser until it sees the port the service actually opened, and checks that nothing else holds it when you press Start. lpm doesn’t pass it to your command — put the port in cmd or env yourself. Give each service its own port — lpm config validate flags two services that share one. |
portConflict | string | What to do when port is already taken at Start. ask (the default) asks before freeing it, free frees it without asking, and fail refuses to start. Freeing stops the other lpm project holding the port, or ends the process that holds it. |
env | map | Extra environment variables to set just for this service — things like API_URL or NODE_ENV. Useful when you don’t want to commit them to a .env file. |
dependsOn | []string | Other services this one needs — name them here and lpm starts them first, pulling them in automatically whenever you start this service (even if you only picked this one). It’s ordering only: the dependencies launch ahead of this service, but lpm doesn’t wait for them to finish booting or start listening before it moves on. Also accepted as depends_on. |
* required
Services that depend on each other. When one service builds on another — a web app that talks to an API, an API that reads from the database — list the ones it needs in dependsOn. Start web here and lpm pulls in api and db for you and launches them in order: db, then api, then web. It starts them in that order but doesn’t wait for each one to be ready before moving to the next, so a service that connects on boot should retry until its dependency answers.
No active terminals
Click Start to run myapp.
name: myapp
root: ~/Projects/myapp
services:
db: docker compose up postgres
api:
cmd: npm run api
dependsOn: [db] # lpm starts db before api
web:
cmd: npm run dev
dependsOn: [api] # lpm starts api before web
Starting and stopping
Click Start in the project toolbar and lpm brings your services up together; click Stop and they all come down. To run just one, switch it on in the Start button’s dropdown — lpm pulls in whatever it depends on. Selecting a project in the sidebar never starts it; a double-click can, if you turn on Double-click to start/stop in Settings.
If you usually want a subset — say, the web app without the worker — define a profile and pick it from the same dropdown.
Actions
Actions are the commands you run once in a while — your test suite, a database migration, a deploy script. Services run continuously; actions fire once, do their job, and show you the result. Each one is a button in the project toolbar — or in the footer bar under the terminal — and one click runs it.
Try it — click test or Deploy to Production in the preview below. Actions with confirm: true ask before running; everything else just runs.
No active terminals
Click Start to run myapp.
name: myapp
root: ~/Projects/myapp
services:
web: npm run dev
actions:
test: npm test # shorthand
deploy: # full form
cmd: ./scripts/deploy.sh
label: Deploy to Production # display name in the UI
confirm: true # ask before running
display: header # main button row (default; omit to get the same)
env:
NODE_ENV: production
cmdstringThe shell command to run — whatever you’d type into a terminal yourself. Required unless the action groups child actions with
actionsbelow.labelstringThe friendly name shown on the button. If you skip it, lpm shows the action’s key as-is —
db-resetstaysdb-reset, whilelabel: Reset DBreads better.emojistringA single emoji shown in front of the label, so a crowded toolbar stays scannable. It’s prefixed to whatever the label resolves to —
emoji: 🚀withlabel: Deployrenders as🚀 Deploy.cwdstringRun the command from a different folder than the project root — useful for monorepos or when the action lives in a subfolder. Relative paths resolve from
root; absolute paths are used as-is (see Path resolution). Nested actions inherit this from their parent.envmapExtra environment variables to set just for this action — handy for one-off flags like
NODE_ENV=production. Nested actions inherit these from their parent.confirmboolShow a confirmation dialog before running. Turn this on for anything you’d regret clicking by accident — deletes, resets, production deploys.
displaystringWhere the action appears. The default,
header, pins it to the project toolbar so it’s always one click away — omitdisplayentirely to get the same result.footertucks it into the status bar below the terminal — handy for tight, always-visible controls.menustill works as a legacy value (overflow menu) but is no longer suggested by the editor.typestringHow the action runs. The default pops open a modal and streams output while the command runs.
terminalopens a persistent interactive pane (see the Terminals section below).backgroundruns the command hidden behind a small live card that counts the time, keeps the output a click away with Cancel and Copy output, and shows success or failure when it finishes — perfect for slow, boring commands whose only interesting signal is “did it succeed.”commandtypes the command into the terminal you have focused.actionsmapGroup related commands under this action. They show up as a dropdown. If the parent also has a
cmd, it renders as a split button — clicking the main part runs the parent, clicking the chevron opens the group.primarystringOnly for actions with nested
actions: which child the main half of the split button runs. Name a child’s key (primary: staging) to pin it, or uselast-usedso the button follows whichever child you ran most recently — remembered per project on that Mac. It takes precedence over the parent’s owncmd.
| Field | Type | Description |
|---|---|---|
cmd | string | The shell command to run — whatever you’d type into a terminal yourself. Required unless the action groups child actions with actions below. |
label | string | The friendly name shown on the button. If you skip it, lpm shows the action’s key as-is — db-reset stays db-reset, while label: Reset DB reads better. |
emoji | string | A single emoji shown in front of the label, so a crowded toolbar stays scannable. It’s prefixed to whatever the label resolves to — emoji: 🚀 with label: Deploy renders as 🚀 Deploy. |
cwd | string | Run the command from a different folder than the project root — useful for monorepos or when the action lives in a subfolder. Relative paths resolve from root; absolute paths are used as-is (see Path resolution). Nested actions inherit this from their parent. |
env | map | Extra environment variables to set just for this action — handy for one-off flags like NODE_ENV=production. Nested actions inherit these from their parent. |
confirm | bool | Show a confirmation dialog before running. Turn this on for anything you’d regret clicking by accident — deletes, resets, production deploys. |
display | string | Where the action appears. The default, header, pins it to the project toolbar so it’s always one click away — omit display entirely to get the same result. footer tucks it into the status bar below the terminal — handy for tight, always-visible controls. menu still works as a legacy value (overflow menu) but is no longer suggested by the editor. |
type | string | How the action runs. The default pops open a modal and streams output while the command runs. terminal opens a persistent interactive pane (see the Terminals section below). background runs the command hidden behind a small live card that counts the time, keeps the output a click away with Cancel and Copy output, and shows success or failure when it finishes — perfect for slow, boring commands whose only interesting signal is “did it succeed.” command types the command into the terminal you have focused. |
actions | map | Group related commands under this action. They show up as a dropdown. If the parent also has a cmd, it renders as a split button — clicking the main part runs the parent, clicking the chevron opens the group. |
primary | string | Only for actions with nested actions: which child the main half of the split button runs. Name a child’s key (primary: staging) to pin it, or use last-used so the button follows whichever child you ran most recently — remembered per project on that Mac. It takes precedence over the parent’s own cmd. |
Other fields. The table covers what the examples on this page use. Actions also accept color (an accent for the button and the tab it opens), shortcut (a key combo that runs the action while the project is open), inputs (values lpm asks for before running and fills into the command), prompt (text sent to the agent a terminal action opens, once it’s ready), reuse (use the terminal that’s already open), port and portConflict (ports that must be free first, as a number, a range like "3002-3010", or a list), and on SSH projects mode (remote or sync). The app’s action editor sets most of them for you.
Shorthand. If all your action needs is a command, write it as a single line — the key becomes the label and you skip the nested form entirely. Great for everyday dev commands:
No active terminals
Click Start to run myapp.
name: myapp
root: ~/Projects/myapp
services:
web: npm run dev
actions:
test: npm test
lint: npm run lint
build: npm run build
typecheck: npx tsc --noEmit
format: npx prettier --write .
Destructive actions. For anything you don’t want to click by accident — cache wipes, rollbacks, production deploys — add confirm: true to get a confirmation dialog, and pair it with display: header (the default) so the action lives in the toolbar where you’ll find it:
No active terminals
Click Start to run myapp.
name: myapp
root: ~/Projects/myapp
services:
web: npm run dev
actions:
reset-cache:
cmd: rm -rf .next node_modules/.cache
label: Reset Cache
confirm: true
display: header
rollback:
cmd: ./scripts/rollback.sh
label: Rollback Deploy
confirm: true
env:
NODE_ENV: production
Grouping related actions. Give a parent action both a cmd and nested actions and lpm renders it as a split button: the main part runs the parent’s command, the chevron opens a menu with the children. Use this when there’s a sensible default plus a few alternatives — like “Deploy staging” with production and preview tucked behind it:
No active terminals
Click Start to run myapp.
name: myapp
root: ~/Projects/myapp
services:
web: npm run dev
actions:
deploy:
cmd: ./deploy.sh staging # split button — main click runs this
label: Deploy
emoji: 🚀 # icon shown next to the label
display: header
confirm: true
actions: # chevron opens these
production:
cmd: ./deploy.sh production
label: Production
emoji: 🔴
confirm: true
preview:
cmd: ./deploy.sh preview
label: Preview
emoji: 👁️
Remembering your last choice. Add primary: last-used instead of a parent cmd and the main part of the split button becomes whichever option you clicked last — deploy to staging once, and the button reads “Staging” until you pick something else. Set primary to a child’s name (like primary: staging) to pin one option as the default instead:
No active terminals
Click Start to run myapp.
name: myapp
root: ~/Projects/myapp
services:
web: npm run dev
actions:
deploy:
primary: last-used # main click repeats the last used option
actions: # chevron opens these
staging:
cmd: ./deploy.sh staging
label: Staging
emoji: 🟢
production:
cmd: ./deploy.sh production
label: Production
emoji: 🔴
confirm: true
Dropdown-only groups. Drop the parent’s cmd and the whole button becomes a dropdown. Good for a set of related commands with no obvious default — like a database toolkit (Migrate, Seed, Reset):
No active terminals
Click Start to run myapp.
name: myapp
root: ~/Projects/myapp
services:
web: npm run dev
actions:
db:
label: Database
emoji: 🗄️
display: header
cwd: ./backend
actions:
migrate:
cmd: python manage.py migrate
label: Migrate
emoji: 📦
seed:
cmd: python manage.py seed
label: Seed
emoji: 🌱
reset:
cmd: python manage.py flush
label: Reset
emoji: 💣
confirm: true
Background actions. For slow, boring commands where the only thing you care about is whether they succeeded — builds, migrations, docker pulls, git fetch — add type: background. The command runs hidden while a small live card counts the time and keeps its output a click away, with Cancel and Copy output, then shows success or failure when it’s done. No modal to dismiss, no terminal tab to clean up, and you can fire several in parallel while you keep working:
No active terminals
Click Start to run myapp.
name: myapp
root: ~/Projects/myapp
services:
web: npm run dev
actions:
db-reset:
cmd: npm run db:reset && npm run db:seed
label: Reset DB
type: background # runs hidden, with a live progress card
confirm: true # pair with confirm for destructive ones
A note on inheritance
Nested actions inherit cwd and env from their parent unless they override them. Set cwd: ./backend on the parent once and every child runs from there — no need to repeat yourself.
Terminals
Terminals are persistent interactive shells you open from the desktop app with a single click — a live log tail, a Node or Python REPL, or an AI coding agent like Claude Code. Unlike a service, a terminal isn’t something lpm starts and stops with the project; unlike an action, it doesn’t run once and exit. It stays open and you type in it until you close it.
A terminal is just an action with type: terminal — same fields as any action, it simply opens in a pane and stays open instead of running once. A standalone terminals: section still works as a deprecated alias, but new configs should use type: terminal.
Declaring a terminal. Give the entry a cmd and set type: terminal so it opens in a pane and stays open instead of running once. Reach for a friendlier label, tuck it into the footer with display: footer, or set a cwd or env when you need them:
No active terminals
Click Start to run myapp, or open a terminal.
name: myapp
root: ~/Projects/myapp
services:
web: npm run dev
actions:
codex: # minimal — key becomes the label
cmd: codex
type: terminal
claude: # full form
cmd: claude
label: Claude Code # nicer name than the key
display: header # pin to the toolbar, one click away (default)
type: terminal
cmd*stringThe shell command that starts the terminal — usually something interactive you want to keep around, like
claude,node, ortail -f ./logs/dev.log. lpm opens it in a real PTY so prompts, colors, and arrow keys all work.type*stringMust be
terminal— this is what makes the entry open in a persistent pane and stay open, instead of running once like a plain action.labelstringThe friendly name shown on the button. If you skip it, lpm uses the terminal’s key — so
claudejust shows up asclaude.emojistringSame as on actions: a single emoji prefixed to the label, handy for telling a row of agent terminals apart at a glance —
emoji: 🤖withlabel: Claude Coderenders as🤖 Claude Code.cwdstringOpen the terminal in a different folder than the project root — useful for monorepos or when your agent should start inside a specific package. Relative paths resolve from
root; absolute paths are used as-is (see Path resolution).envmapExtra environment variables to set just for this terminal — handy for picking a model with
ANTHROPIC_MODELor pointing a REPL at a staging database. These only apply inside this shell, nothing else on your system is touched.displaystringSame rules as actions. The default,
header, pins the terminal to the project toolbar — omitdisplayentirely to get the same result.footertucks it into the status bar below the terminal.menustill works as a legacy value (overflow menu) but is no longer suggested by the editor.
| Field | Type | Description |
|---|---|---|
cmd* | string | The shell command that starts the terminal — usually something interactive you want to keep around, like claude, node, or tail -f ./logs/dev.log. lpm opens it in a real PTY so prompts, colors, and arrow keys all work. |
type* | string | Must be terminal — this is what makes the entry open in a persistent pane and stay open, instead of running once like a plain action. |
label | string | The friendly name shown on the button. If you skip it, lpm uses the terminal’s key — so claude just shows up as claude. |
emoji | string | Same as on actions: a single emoji prefixed to the label, handy for telling a row of agent terminals apart at a glance — emoji: 🤖 with label: Claude Code renders as 🤖 Claude Code. |
cwd | string | Open the terminal in a different folder than the project root — useful for monorepos or when your agent should start inside a specific package. Relative paths resolve from root; absolute paths are used as-is (see Path resolution). |
env | map | Extra environment variables to set just for this terminal — handy for picking a model with ANTHROPIC_MODEL or pointing a REPL at a staging database. These only apply inside this shell, nothing else on your system is touched. |
display | string | Same rules as actions. The default, header, pins the terminal to the project toolbar — omit display entirely to get the same result. footer tucks it into the status bar below the terminal. menu still works as a legacy value (overflow menu) but is no longer suggested by the editor. |
* required
Header or footer?
Header is the default — every terminal lands in the toolbar unless you say otherwise. Move the tightest, always-one-click controls to the footer with display: footer so they sit beside the branch switcher without crowding the main button row.
AI coding agents, one click away. This is where terminals really shine. List the agents and REPLs you actually use and they’re waiting as one-click buttons in the project toolbar every time you open the project — each one starts in the project’s folder, so there’s no hunting for the right window or directory:
No active terminals
Click Start to run myapp, or open a terminal.
name: myapp
root: ~/Projects/myapp
services:
web: npm run dev
actions:
claude:
cmd: claude # AI pair programmer
type: terminal
codex:
cmd: codex # another AI agent, swap at will
type: terminal
node:
cmd: node # quick REPL for poking at things
type: terminal
logs:
cmd: tail -f ./logs/dev.log # live-tail your dev server logs
type: terminal
Profiles
Profiles group services into named workflows so you don’t have to spin up everything every time. Working on a CSS tweak? Start just the frontend. Building a feature end-to-end? Fire up the full stack. Pick a profile from the Start button’s dropdown in the project toolbar and lpm starts those services right away, plus anything they depend on.
Start small. Even two profiles pay off right away — a lightweight one for quick UI fixes and a full one for feature work. Here’s the smallest useful setup: a frontend and a backend, with a frontend profile that skips the API when you don’t need it:
No active terminals
Click Start to run myapp.
name: myapp
root: ~/Projects/myapp
services:
web: npm run dev # frontend UI
api:
cmd: node server.js
port: 4000 # backend API
profiles:
# Just the frontend — fastest startup, good for UI-only fixes
frontend: [web]
# Full stack — frontend + backend for feature work
full: [web, api]
Every name in a profile list must match a service defined above in services. Services can appear in as many profiles as you like — overlap is fine and expected.
Multiple profiles for different modes. Once your project grows a third or fourth service — a background worker, a queue, a second frontend — a single profile isn’t enough. Define one profile per workflow you actually use, so you can jump between “just the UI”, “normal dev”, and “everything running” without touching the config:
No active terminals
Click Start to run shop.
name: shop
root: ~/Projects/shop
services:
web: npm run dev # React frontend
api:
cmd: python -m api.server
port: 5000 # Flask backend
worker: celery -A tasks # background jobs
profiles:
# Quick UI fixes — no backend needed
frontend: [web]
# Normal day-to-day development — web + api
local: [web, api]
# Everything, including background workers
full: [web, api, worker]
What does a plain Start run?
With no profiles, Start runs every service. Once a project has profiles, Start runs the profile you last started since opening the app — otherwise the first profile in alphabetical order, which is how the dropdown lists them. In the examples above, that’s frontend. To choose what a plain Start runs from the beginning, give that profile a name that sorts first.
Config layers
A project’s settings can come from three files. lpm merges them every time it loads the project, so your team can share one setup while you keep personal tweaks out of the repo. Higher layers win.
- Your project file
~/.lpm/projects/<name>.ymlEverything: root or ssh, services, actions, terminals, profiles. Personal — stays outside the repo. The User tab in the app’s config editor.
- Repo config
<project root>/.lpm.ymlServices, actions, terminals, profiles. Shared — commit it with the code. The Repo tab in the app’s config editor.
- Global config
~/.lpm/global.ymlActions and terminals. Every project on this Mac. The Global tab in the app’s config editor.
Sharing a setup with your team. Commit a .lpm.yml at the root of the repository. Anyone who adds the folder to lpm gets its services, actions, terminals, and profiles — no name or root needed, since those come from each person’s own project file:
services:
web:
cmd: npm run dev
port: 3000
api:
cmd: npm run api
cwd: ./server
port: 4000
actions:
test: npm test
migrate:
cmd: npm run db:migrate
label: Migrate DB
confirm: true
profiles:
frontend: [web]
full: [web, api]
Overriding one field. Entries with the same key merge field by field: whatever the higher layer sets wins, and anything it leaves out comes from the layer below. To run the shared web service on another port, override its command and port in your own file — everything else about it still comes from the repo:
name: shop
root: ~/Projects/shop
services:
web:
cmd: npm run dev -- --port 3100 # wins over the repo's cmd
port: 3100 # and its port; the rest is inherited
Good to know
Maps like env are replaced whole, not merged key by key — if you override env, repeat every variable you still need. An overridden service doesn’t inherit the lower layer’s dependsOn, a profile with the same name replaces the lower one outright, and an action’s confirm: true can’t be switched off from a higher layer.
The repo layer applies to local projects only; SSH projects have no .lpm.yml. A duplicate also inherits from the project it was copied from, which sits between your file and the repo layer.
Global config
Some things aren’t tied to any one codebase — system maintenance, utilities, your favorite agent. Put those in ~/.lpm/global.yml and they show up in every project automatically. If a project defines an action with the same key, the project-level entry wins. Edit it from Settings → Global Config, or the Global tab of any project’s config editor.
What a fresh install starts with. On a fresh install, lpm writes a global.yml with two terminal actions, claude and codex, so Claude Code and Codex are one click away in every project. Edit or remove them like any other entry. A minimal file of your own might add a Docker cleanup and a system monitor — notice there’s no name or root:
No active terminals
Open a terminal to get started.
actions:
docker-prune:
cmd: docker system prune -f
label: Docker Prune
confirm: true # asks before wiping images and caches
htop:
cmd: htop # live system monitor, one click away
type: terminal
System-wide utilities. A fuller example: prune merged git branches, upgrade Homebrew, and keep a few system monitors one click away. These all live above individual projects — click them from any project and they just work.
No active terminals
Open a terminal to get started.
actions:
prune-branches:
cmd: git branch --merged main | grep -v main | xargs git branch -d
label: Prune merged branches
confirm: true # deletes local branches — ask first
brew-upgrade:
cmd: brew update && brew upgrade
label: Brew upgrade # keep Homebrew packages fresh
htop:
cmd: htop # live CPU and memory
type: terminal
btop:
cmd: btop # prettier process viewer
type: terminal
ncdu:
cmd: ncdu ~ # explore what's eating your disk
type: terminal
Only actions
Global config supports actions — including type: terminal shells — and nothing else. No services, no profiles. Long-running processes and profile groupings always belong to a specific project, so they live in a project file or its .lpm.yml.
Editing in the app
You rarely need a text editor. Open a project’s config in lpm and you get one tab per layer — User, Repo, and Global — with three ways to change it.
Form
Your project file opens as a form: name, folder, and Claude account, then services, actions, terminals, and profiles as fields you fill in.
Source
One click switches to the YAML, with schema hints as you type. ⌘S saves, and invalid YAML is refused. Repo and Global always open here.
Generate with AI
Pick an installed AI CLI — Claude Code, Codex, Gemini, or OpenCode — and it drafts a config from your project. The draft lands unsaved, so you review it first.
From the terminal. lpm config validate <file> checks a project, repo, global, or template file (see Validation). Turn on Agent tools in Settings and coding agents like Claude Code and Codex can write and apply configs for you — see lpm skills for Claude Code and Codex.
Recipes
Full working configs you can copy and adapt. The sections above each show one concept in isolation; the recipes here stitch services and actions together into configs that mirror how a real project looks on day one. Find the one closest to your stack, paste it into a new project, and tweak from there.
Minimal blog. Start here if you just want one dev server and nothing else — a personal blog, a tiny side project, the “hello world” version of lpm. One service, no actions, no ceremony.
No active terminals
Click Start to run blog.
name: blog
root: ~/Projects/blog
services:
web: npm run dev # the only thing you need to hit Start
Blog with tests and linting. Add this when your tests, linter, or build start taking long enough that retyping them feels wasteful. Same dev server as above, plus three one-click buttons in the toolbar.
No active terminals
Click Start to run blog.
name: blog
root: ~/Projects/blog
services:
web: npm run dev
actions:
# one-click buttons for the chores you used to retype
test: npm test
lint: npm run lint
build: npm run build
Next.js plus a Node API. For the classic two-process web app: a Next.js front-end in the project root and a Node backend in ./server. Shows how to set cwd per service, label a port, add a guarded deploy, and pin a log tail to its own terminal tab.
No active terminals
Click Start to run webapp, or open a terminal.
name: webapp
root: ~/Projects/webapp
services:
web: npm run dev # Next.js front-end
server:
cmd: node server.js # API the front-end talks to
cwd: ./server # lives in a subfolder
port: 4000 # label for the Start menu and Open in browser
actions:
deploy:
cmd: ./scripts/deploy.sh
confirm: true # ask before shipping
logs:
cmd: tail -f ./logs/server.log # keep server logs one click away
type: terminal
Next.js with dev env vars. Pick this when your app needs a handful of environment variables to boot locally and you’re tired of remembering them. lpm sets them every time the service starts — keep real secrets in your own .env file, not here.
No active terminals
Click Start to run webapp.
name: webapp
root: ~/Projects/webapp
services:
web:
cmd: npm run dev
port: 3000
env:
# dev-only values — real secrets belong in your own .env
API_URL: http://localhost:4000
NEXTAUTH_SECRET: dev-secret
NODE_ENV: development
Monorepo with an app and docs. For a repo that holds more than one thing you want running at once — say an app in apps/web and a docs site in apps/docs. Both services live under one project and start together, each from its own folder.
No active terminals
Click Start to run mono.
name: mono
root: ~/Projects/mono
services:
web:
cmd: npm run dev
cwd: ./apps/web # one app in the monorepo
port: 3000
docs:
cmd: npm run dev -- --port 3001 # lpm doesn't pass the port for you
cwd: ./apps/docs # another app, started together
port: 3001
How to use a recipe
Copy the one closest to your stack, change name and root to match your project, then swap in your own commands. If a piece looks unfamiliar, jump back to the matching section above and tinker with its playground — your edits stay live until you reload.
Path resolution
~expands to your home directory inroot- Relative
cwdpaths resolve relative toroot - Absolute paths are used as-is
- On a local project,
~is not expanded incwd— a~/…working folder is read as a folder named~inside the project, so use a relative or absolute path there - On an SSH project, a relative
cwdresolves fromssh.dir, and~means your home folder on the remote host
Validation
The app refuses to save invalid YAML and underlines schema problems as you type. For a full check, run lpm config validate <file>, which looks at the project with its layers applied and verifies that:
- At least one service is defined (duplicates are exempt — they inherit their parent’s)
- Every
cmdis non-empty - Ports are whole numbers up to 65535, and no two services share one
- Every
cwdpoints to an existing folder - Profiles and
dependsOnonly name services that exist, with no dependency cycles - There are no unknown fields
When you press Start, the app itself stops on a dependency cycle or on a dependsOn or profile entry that names a missing service, and tells you which.
Keep reading
Multiple Claude Code accounts
Pin a Claude login to each project — the claudeAccount key, set from the project form.
SSH projects
Run a remote machine's services, actions, and terminals from the same sidebar.
Skills for Claude Code and Codex
Let your coding agents write, validate, and apply lpm configs for you.
Git worktrees for AI agents
Give each agent its own copy of a project that inherits this config.
You’ve seen what a config can do.
Let lpm write your first one.
Download lpm for macOS and point it at a project folder. It writes the file for you with the services it finds, and Claude Code and Codex are one click away in every project.