Projects: .limabean.yaml¶
A repository can describe the VM it needs in a .limabean.yaml file
(.limabean.yml works too) at its root: the tools, the size, what it may
reach, which secrets it uses and how to set it up. Then anyone with
limabean gets that VM, ready to work in, with one command from their
checkout:
Your first project file¶
The quickest start is to let limabean write it from what's in the repository:
cd ~/src/myapp
limabean init --project # shows what it found, asks, writes .limabean.yaml
limabean init --project --print # or just show it
It looks at the repository's root and the directories directly below it,
so a monorepo's web/ and api/ count too:
| It finds | It writes |
|---|---|
go.mod, package.json, pyproject.toml, requirements.txt, Cargo.toml, Gemfile, mix.exs, pom.xml, build.gradle, a .csproj |
the pack, and a setup command that fits the lockfile: npm ci, pnpm install --frozen-lockfile, uv sync, go mod download, bundle install and so on |
*.tf, Chart.yaml, kustomization.yaml, skaffold.yaml |
the infra or kubernetes pack |
| Vite, Next.js, Astro, Rails, Django, FastAPI, Flask, Phoenix or Spring Boot | the port it serves on |
compose.yaml |
start: docker compose up -d |
.tool-versions, mise.toml |
the pack with that tool, or the tool under tools if no pack has it |
.env.example |
its credentials (*_KEY, *_TOKEN, …) as a commented-out secrets: line to set up first |
It only reads files, never runs anything from the repository, and doesn't
follow symlinks out of it. The file it writes passes the same checks as
up; read it before you bring the VM up, since a guess can be wrong.
Or start from a recipe, a commented starter file for a kind of project (a Go service, a web frontend, a Rails app, a Claude sandbox and more):
Or write one by hand. This is enough for a Node app that serves on port 3000:
Run limabean up anywhere in the checkout. It finds the file here or in a
parent directory, shows what it's about to do, and asks:
myapp (from ~/src/myapp/.limabean.yaml)
env: scratch
packs: core, node
clone: github.com/acme/myapp
setup, run in the VM:
npm ci
Create myapp? Yes
Then it:
- creates a VM named after the directory (
myapp), with the packs the file lists and your shared secrets; - clones the checkout's
origininto the VM, at~/code/github.com/acme/myapp; - runs
setupthere, once; - prints how to reach it:
myapp is ready
ssh lima-myapp or: limabean ssh myapp
limabean code myapp /home/you.linux/code/github.com/acme/myapp
http://localhost:3000
The VM is a normal limabean VM: limabean ssh myapp, limabean code myapp,
Claude Desktop and MCP all work with it. Commit the file so everyone on the
project gets the same VM.
limabean doctor in the checkout checks the file against this Mac: that
its packs, network presets and secrets exist here, and whether the VM
matches it.
In your editor¶
Your editor can complete the file's keys and flag mistakes as you type: see
Editor support. Files written by limabean recipes
already have the line that turns it on.
Every day¶
Like docker compose, commands work from the checkout, wherever you are
in it, without naming the VM:
limabean up # create the VM, or start it and apply the file's changes
limabean ssh # a shell, in the VM's clone of the repository
limabean ssh -- make test # a command, run there
limabean code # VS Code, Cursor or another editor, over SSH
limabean ps # is it running, is start, which ports, what changed
limabean logs --start -f # follow what start is running
limabean remote # the VM's clone as a git remote, for git fetch
limabean down # stop it; everything in it is kept
ssh, code, get, info, remote, open, ports, network ls,
snapshot save and snapshot ls use the project's VM when you don't name one; name another
VM to use it instead. rm, rebuild and snapshot restore always need
the name, so nothing is deleted or rolled back by accident.
In the checkout, ssh works in the VM's clone (~/code/<host>/<owner>/<repo>)
rather than the home directory, so limabean ssh -- go test ./... runs the
tests there. The clone is separate from your checkout on the Mac: changes
made in one stay there until they're fetched or pushed
(Review on your Mac).
In the VM, limabean-info shows the
project's repository, clone, ports and logs, which up writes there.
down only stops the VM. limabean rm myapp deletes it.
Review on your Mac¶
Work done in the VM's clone, by you or by Claude, can come to your Mac's
checkout without going through GitHub. limabean remote adds the clone as
a git remote named limabean, and git fetches from it over SSH:
limabean remote # once per checkout (again after a rebuild)
git fetch limabean # the VM's branches, as limabean/<branch>
git log --oneline main..limabean/zig-pack
git diff main...limabean/zig-pack # review, in your own tools
git switch -c zig-pack limabean/zig-pack # take it over on the Mac
It goes the other way too: git push limabean my-branch hands a branch to
the VM (any branch but the one checked out there).
- It reaches the VM the way
limabean sshdoes, so it works whatever the VM's network allows: a locked VM that can't reach GitHub can still hand you its work. - Fetching copies git objects into your checkout and runs nothing from the VM. The remote fetches branches only, never tags, so nothing in the VM can add a tag to your repository.
limabean remote other --name otheradds another VM's clone;limabean remote --rmremoves the remote. A remote of that name that points anywhere but a VM is left alone.
Changing the file¶
Edit the file and run limabean up again. It compares the file with the VM
as it is, lists what differs and asks before changing anything (--yes
skips the question; --preview only lists it):
myapp (changes from ~/src/myapp/.limabean.yaml)
packs: core, node → core, node, rust (rebuild, keeping ~/code)
cpus: 2 → 4
network: stop allowing yarnpkg.com
Apply these changes to myapp? Yes
| Change | What up does |
|---|---|
packs, tools, apt, env |
Rebuilds the VM, as limabean rebuild does: ~/code and its repositories are kept on their own disk, the rest of the system is fresh |
cpus, memory |
Stops the VM, resizes it and starts it again |
disk |
Grows the disk the same way; Lima can't shrink one, so a smaller number is only a warning |
secrets |
Gives the VM the secrets it doesn't have yet |
vars |
Writes the new variables in the running VM |
claude |
Adds the plugins to the VM's Claude settings |
network: fewer domains, locked: true |
Applied straight away |
network: more domains, no allowlist, no locked |
Not applied. A file from a repository can't widen what a VM reaches; up prints the limabean network … command for you to run if you agree |
setup |
Runs only when the VM is created; limabean up --setup runs it again in the clone |
start |
Shows the new commands and asks; once approved, they run after this and every later up |
name |
Makes a different VM; the old one stays until you limabean rm it |
A VM without its own ~/code disk (made with CODE_DISK="") loses its
repositories in a rebuild, so up refuses one while any of them has
unpushed work.
Reference¶
Every key is optional.
| Key | Type | Default | What it does |
|---|---|---|---|
name |
string | the directory's name | The VM's name: letters, digits and dashes |
env |
string | scratch |
The environment template |
repo |
string | the checkout's origin |
What to clone: owner/repo, host/owner/repo or a URL |
packs |
list | none | The VM's packs besides core; the PACKS setting doesn't apply, so everyone gets the same VM |
tools |
map | none | The project's own mise tools, tool: version, for what no pack has |
apt |
list | none | The project's own Ubuntu packages |
cpus |
number | CPUS, or 2 |
CPUs |
memory |
number | MEMORY, or 4 |
GiB of memory |
disk |
number | DISK, or 10 |
GiB of system disk, which holds the tools and Docker images; ~/code has a disk of its own |
network.allow |
list | open | Domains and presets the VM may reach (sandboxes) |
network.locked |
bool | false |
Take sudo and Docker away from the VM's user, so the allowlist holds |
vars |
map | none | Plain variables for the VM's shells, setup and start; details |
secrets |
list | none | Names of secrets the VM needs, besides the shared ones |
ports |
list | none | Ports the project serves, shown as http://localhost:<port> |
setup |
string | none | Shell commands run once in the clone, as you, with bash -e |
start |
string | none | Shell commands run in the clone, in the background, after every up |
claude.marketplaces |
list | none | Claude plugins for the VM: each entry a marketplace (GitHub owner/repo or an https git URL) and the plugins to turn on from it, as plugin@marketplace |
Unknown keys, malformed names and out-of-range values are errors, so a typo doesn't go unnoticed. A complete file:
# .limabean.yaml
name: myapp
env: scratch
packs: [node, rust, claude]
cpus: 4
memory: 8
disk: 30
network:
allow: [github, npm, rust]
locked: true
secrets: [OPENAI_API_KEY, NPM_TOKEN]
ports: [3000, 5173]
setup: |
npm ci
cargo fetch
claude:
marketplaces:
- marketplace: rossedman/guardrails
plugins: [guardrails@guardrails]
Tools and apt¶
When a project needs a tool or a system package that no pack has, list it in the file instead of making a pack on every Mac:
tools: # mise tools, tool: version
terraform: 1.9.8
"npm:@biomejs/biome": 2.1.2 # any mise backend: aqua:, npm:, pipx:, cargo:, go:
apt: [libpq-dev, graphviz] # Ubuntu packages
- They're installed in the VM when it's created, the same way packs are,
so a VM copied from an image gets them
too. A change to them rebuilds the VM, keeping
~/code, as a change topacksdoes;limabean rebuildkeeps them. - Pin exact versions, as packs do, so everyone gets the same tools.
limabean bumpdoesn't update these; edit the file. - A tool a pack already pins is best left to the pack.
- Like packs, they're installed at the VM's first boot, before its
network policy applies, so a
lockedVM still gets them. - Names and versions are checked: only letters, digits and
._@/:+-for tools, Debian package names forapt.
Vars¶
vars sets plain variables in the VM, for its shells, setup and
start:
- They're in the file, and so in the repository: anything secret goes
under
secretsinstead, which only names it. upwrites them to~/.config/limabean/project.envin the VM, which every shell reads. A change to them is applied to the running VM, no rebuild needed; shells already open see it in their next login.- A project file can't set
PATH,HOME,SHELLand the like, or the dynamic linker'sLD_*variables, and a name can't be both a variable and a secret. Secrets are read after these, so they always win.
Secrets¶
The file names secrets; the values stay on each person's Mac. Set a secret
up once, for this project's VM or for every VM, and up finds it:
up gives the VM the secrets it names that are set up on your Mac, for any
VM, plus your shared secrets as always. It lists any it can't find, and
creates the VM without them; add them later with
limabean secrets set NAME --vm myapp, which installs them in the running
VM.
Network¶
With network.allow, the VM can only reach those domains, and that already
applies when setup runs: allow whatever setup downloads from (npm,
python, go, rust…), and github for the clone. If you don't know the
list, find it out once:
limabean up trial --learn
limabean ssh trial 'cd ~/code && git clone https://github.com/acme/myapp && cd myapp && npm ci'
limabean network log trial # everything it looked up
and put the domains in the file (learn mode).
locked: true also applies before setup, so setup can't use sudo or
Docker. If it needs either, leave locked out and lock the VM afterwards
with limabean network lock myapp.
Setup¶
setup runs with bash -e, so it stops at the first failing command. It
runs in the clone (in your home directory if nothing was cloned), as the
VM's user, with your secrets in the environment. If it fails, up stops and
keeps the VM so you can look: limabean ssh myapp, or read what it printed
with limabean logs --setup. Fix the setup, then run it again with
limabean up --setup.
Start¶
start is for what should be running whenever the VM is up: services in
Docker, a dev server, a watcher. It runs after every up, in the clone,
with bash -e and your secrets, in the background, so up returns while
it keeps going:
- If the last
startis still running,upleaves it alone rather than starting a second one. - It stops with the VM:
limabean down, and it runs again at the nextup. limabean logs --start -ffollows its output;startkeeps only the latest run's.startcommands come with the repository and run on everyup, soupremembers the ones you approved (on your Mac) and asks again whenever they change, as after agit pullthat edits them. Until you approve, the old ones don't run either.
Claude plugins¶
claude turns on plugins for Claude Code in the VM, whether it runs from
the claude pack or from
Claude Desktop over SSH. Skills, agents, hooks and
MCP servers all come inside plugins:
claude:
marketplaces:
- marketplace: rossedman/guardrails # GitHub owner/repo
plugins: [guardrails@guardrails]
- marketplace: https://git.example.com/platform/plugins.git # any git host, over https
plugins: [review@platform, deploy@platform]
- Plugins are
plugin@marketplace, where the marketplace is the name in itsmarketplace.json(often the repository's name). The plugins in one entry share it; another marketplace is another entry. upadds each marketplace to the VM's~/.claude/settings.jsonunderextraKnownMarketplaces, and each plugin underenabledPlugins. Settings already there are kept. Claude Code adds the marketplace and installs the plugins when a session starts.- Claude Code fetches them inside the VM, with the VM's own git
credentials (a private repository works if
limabean github logingave VMs access). A VM with a network allowlist needsgithub, or the marketplace's host. - To add plugins, edit the file and run
limabean up. To turn one off in a VM, set it tofalsein that VM's~/.claude/settings.json; removing it from the file doesn't turn it off.
A repository can also keep its own .claude/settings.json with
enabledPlugins and extraKnownMarketplaces; Claude Code reads that in
the clone after you trust the folder. Use claude: in .limabean.yaml for
what the VM should have whichever folder Claude opens, and for VMs that
Claude only uses headless.
Editor support¶
limabean publishes a JSON Schema for the project file. With it, your editor:
- completes keys, and offers the built-in packs and network presets as values;
- shows what each key does when you hover over it;
- flags what
upwould refuse while you type: an unknown key such asenviroment:, a misspelled or reserved variable (PATH), a port out of range, a malformed tool version or package name, a marketplace without plugins.
The schema is generated from the same checks limabean up runs, so the two
agree. A few things only up can check, because they depend on your Mac:
that a secret is set up, that a pack of your own exists (the schema accepts
any pack name, so yours aren't flagged), and that a repository can be
cloned.
One file: the comment line¶
Editors that use the YAML language server read a comment at the top of the
file. limabean recipes writes it for you; add it to a file you wrote by
hand:
That works in VS Code and Cursor (with the
YAML extension),
Zed, Neovim with yamlls, Helix, and any other editor running the YAML
language server. Committed with the file, it works for everyone on the
project.
Every project: editor settings¶
To get it in every .limabean.yaml without the comment, map the schema to
the file name in your editor's settings.
VS Code and Cursor: install the
YAML extension,
then add to your settings.json:
"yaml.schemas": {
"https://rossedman.io/limabean/limabean.schema.json": ["**/.limabean.yaml", "**/.limabean.yml"]
}
Zed: in settings.json:
"lsp": {
"yaml-language-server": {
"settings": {
"yaml": {
"schemas": {
"https://rossedman.io/limabean/limabean.schema.json": ["**/.limabean.yaml", "**/.limabean.yml"]
}
}
}
}
}
Neovim with nvim-lspconfig
and yaml-language-server installed:
require("lspconfig").yamlls.setup({
settings = {
yaml = {
schemas = {
["https://rossedman.io/limabean/limabean.schema.json"] = { "**/.limabean.yaml", "**/.limabean.yml" },
},
},
},
})
Helix: in languages.toml:
[language-server.yaml-language-server.config.yaml.schemas]
"https://rossedman.io/limabean/limabean.schema.json" = ["**/.limabean.yaml", "**/.limabean.yml"]
JetBrains IDEs (GoLand, IntelliJ, PyCharm and the rest): open
Settings → Languages & Frameworks → Schemas and DTDs → JSON Schema
Mappings, add a mapping with the schema's URL, and add the file patterns
.limabean.yaml and .limabean.yml.
When it doesn't work¶
- Nothing is completed or flagged. Check that the YAML language server is running for the file: in VS Code and Cursor, the YAML extension must be installed, and the language mode in the status bar must say YAML.
- The editor can't fetch the schema. It downloads it from the docs site. Offline, or behind a proxy that blocks it, save the schema next to your project and point the comment or the mapping at the file's path instead of the URL.
- A pack of yours is underlined. It shouldn't be: any pack name is accepted. If your editor shows one anyway, it has an old copy of the schema; restarting the language server, or the editor, refreshes it.
Examples¶
The recipes are complete examples, one per kind of VM: a Go
service with Postgres, a web frontend, Python APIs and notebooks, Rust,
Java, Rails, gRPC, Kubernetes, Terraform, a locked-down Claude Code
sandbox, a VM for reviewing untrusted code, and a minimal one. Start from
one with limabean recipes <name>.
Trust¶
The file comes with the repository, and its setup runs in a VM that gets
your shared secrets. So up never acts on it blindly:
- In a terminal, it shows what the file asks for, setup commands included, and asks before creating anything.
- Without a terminal it shows the same and stops, unless you pass
--yes. - Claude plugins are fetched and run inside the VM, never on your Mac, and the plan lists every one of them.
- The file can only name secrets. It can't say where a value comes from,
so it can't run a command on your Mac (as a
cmd:reference would) or read a Keychain item you didn't already give VMs.
Read a file from someone else's repository before saying yes, as you would
a Makefile.
Speed¶
The first VM of a kind (its environment and packs) is provisioned from
scratch, and an image of it is built in the
background. After that, up for any project with the same packs only has
to boot a copy. To have the image ready before the first up, build it:
limabean images build --with rust,claude.
Commands¶
| Command | Does |
|---|---|
limabean up |
Creates the VM from .limabean.yaml (or .yml) here or in a parent directory, or starts it |
limabean up --yes |
The same without asking, for a file you trust |
limabean up --file <path> |
Uses another file |