✗ Error: listen EADDRINUSE: address already in use :::3000
:3000 ✗
$ git stash
$ git stash pop
teammatecan you push it to staging so I can see?
pando
One repo. Every branch alive.
Every branch you are working on, running at once: each in its own git worktree, with its own dev server on its own ports, its own logs, its own database if you want one, and a public URL when you want to show it. One terminal screen. Nothing written into your checkout.
One grove of quaking aspen, Populus tremuloides, on a hillside in south-central Utah.
It is one tree.
Every stem grows from the same root system. The grove is a single organism.
47,000 trunks. One root.
About 47,000 genetically identical stems on about 43 hectares. About 6,000 metric tons: the heaviest known living thing. The root system is between 9,000 and 16,000 years old; each stem lives 100 to 130 years and is replaced from the roots.
Pando
Named in 1993. Latin for “I spread”.
Every trunk, a branch.
Git has shipped git worktree since version 2.5: any number of checkouts of one repository, side by side, each on its own branch, in its own directory. pando makes one for every branch you work on, and treats it as a complete copy of your app that can run.
One root: your repository.
Worktrees are not clones. They share the one .git: no second clone, no second fetch, and a commit made in one is visible from all of them the moment it exists. The root persists; stems come and go.
In one checkout, branches take turns.
To look at another branch you stash your work, stop the dev server, switch, reinstall, restart, and find the port still taken. Then you do it all again to come back.
And nobody else can see it.
A teammate wants to see your branch on their phone. The branch has to go to a shared staging server first.
So I built pando. For myself.
Every branch, alive.
Every branch you are working on runs at the same time, side by side, from one terminal screen.
04·pando// your turn
›This is pando. Try it.
a replica of the TUI, live in your browser · your keyboard drives it
n
It's live. Press n.Your keyboard drives this screen; no click needed. Or tap the keys below.Tap n in the keys below to begin.
▶ playing a demo · press any key to take over
›
chat · a teammate
you
https://quiet-aspen-grove.trycloudflare.com
teammate
typing…
It behaves like the real one, down to the ports, which are the ones pando would give this pretend project, my-app. The grove under it is the same state drawn as trees: a trunk per worktree, green while it runs, and a pocket of its own when it runs isolated. The pull requests are made up, and nothing here reaches a network.
Reading the list
●running: the dev server is up
◌starting, or something is being done to it
✗failed: l shows the log that says why
○stopped: s starts it
⌂the main checkout, always the first row new in 0.5.0
▣isolated: private copies of the services
◧namespaced: a database and a slot of its own (experimental)
The ⌂ row, the ▣ and ◧ marks and this column layout are new in 0.5.0. A key that would interrupt something running asks for a second press within three seconds: xx stops it, and rr restarts it. Sharing, removing, stopping everything and switching a running worktree's mode ask in a dialog instead. On a stopped worktree, x, r and P act at once. The screen keeps up by polling: every quarter second for the screen, every second for processes, every five seconds for the worktree list and the selected worktree's git state, and every thirty for the others'. It fits a tmux split: below 72 columns the table and the detail pane stack.
05·How it works// a stem
›A stem is a real git worktree.
a checkout of its own, in a directory of its own, on a branch of its own
pando does not invent a new kind of checkout. It drives git worktree, and keeps everything around it in its own home, ~/.pando: the ports, the processes, the logs, the private services, the record of what runs where.
A worktree is an ordinary directory. You can cd into it, run git in it, open it in your editor, commit from it. pando manages every worktree git worktree list reports, so the ones you made by hand are listed, started and stopped too. They are marked adopted. pando never writes into one: the files provision names are not given to it, and start and doctor say which it lacks, with the command that supplies each: a copy, or a link where provision_mode links.
A worktree is named by its branch, feat/login, or by its directory, feat+login: a slash cannot be in a directory name, so it becomes a plus. Every command takes either spelling. Inside a worktree, start, stop, restart, logs, open, share and unshare need no name at all.
~/code/acme-shop/← the main checkout│ .git/━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓│ one object store, one history┃│┃~/.pando/projects/acme-shop-2bdf70d7/┃└─ worktrees/┃ ├─ feat+checkout/ ● :23001━━━━━━━━━━┫ │ branch feat/checkout┃ ├─ fix+login-loop/ ● :22553━━━━━━━━━━┫ │ branch fix/login-loop┃ └─ feat+search/ ○ stopped━━━━━━━━━┛branch feat/search
the stems live in pando's home; the root is your repository's own .git
What pando new does
Finds the branch. A local branch is checked out as it is. Otherwise origin/feat/login, fetched if it is missing, never prompting for a password. Otherwise a new branch, forked from the base: --base, then a matching [branches].rules glob, then [project].base, then origin/HEAD, main, master.
Checks it out.git worktree add into ~/.pando/projects/<id>/worktrees/feat+login. The project id is the checkout's directory name and eight hex characters of a hash of its path.
Provisions. The gitignored files you listed in provision, such as .env, are linked in from the main checkout. They are symlinks by default; provision_mode = "copy" gives each worktree its own copy. A path the project does not gitignore is refused. A worktree made before provision was answered, or one that lost a file since, gets it at its next start, never over a file that is there.
Installs. The project's install step, such as pnpm install --frozen-lockfile, runs in the new worktree, so its dependencies are its own.
a real run, trimmed, on one of pando's test fixtures
$ pando new feat/checkout
pando: checking out feat/checkout
pando: provisioning
pando: installing
created feat+checkout at ~/.pando/projects/mono-web-api-3cb31588/worktrees/feat+checkout
$ pando start feat/checkout
pando: using postgres as this project's services (detected: docker-compose.yml, postgres:16 → DATABASE_URL)
pando: starting api
pando: starting web
started feat+checkout — http://localhost:22809$ pando ls
NAME STATUS URL PORTS GIT
main stopped - - clean main checkout
fix/login-loop stopped - - clean
feat/checkout running http://localhost:22809 api:22808 web:22809 clean
The fixture is built with scripts/fixture-repo.sh mono-web-api --listener, which puts two stand-in dev processes into pando's home. The services line is detection at work: pando read the compose file and the env example and chose. pando doctor says what it found and where.
If anything fails after the checkout, new undoes it. The worktree is removed, and so is the branch if this new created it. A failed install is the exception: it keeps the worktree, and the next start tries the install again. Removing a worktree with rm always keeps its branch.
Ports come from two names
Ports are chosen at start, and never at random. pando hashes the project id and the worktree's name into one of 1,971 windows of eight ports between 17000 and 32767. That range is below the ephemeral ports macOS (49152 up) and Linux (32768 up) hand out.
Each role takes the next port in its window. A role is a name like web, api, or a private postgres. Processes come first, then services, so switching a worktree to isolated keeps its web port whenever the ports after it are free.
The ports are recorded, so a bookmarked URL keeps working across stops, restarts and reboots. They move when something else took one, or when you add or remove a process, and pando says so: the ports feat/login had were taken; it moved to new ones.
try it the same hash pando uses, in your browser
id = directory name and the first 8 hex of md5(the checkout's path) · base = md5(id ␟ worktree)[0..4] mod 1971 × 8 + 17000
Detached processes, and no daemon
Every process runs as bash -lc '<cmd>' in its own session, which gives it its own process group and no terminal. Its input is /dev/null, and its output is appended to logs/<worktree>/<process>.log. Close the terminal or quit the TUI, and it keeps running.
There is no pando daemon. What runs where is kept in state.json, and it is brought up to date whenever something reads it (ls, status, the TUI, a start that waits) from ps and lsof scans of each group.
A worktree can run several processes ([processes.api], [processes.web]). Each has its own command, directory, environment, ports, readiness and log. --only web starts, stops or restarts one of them while the others keep their ports.
stop sends SIGTERM to the whole group, meaning the server and everything it forked. It waits up to five seconds, then sends SIGKILL. A backgrounded server whose parent already died is found and stopped too.
Ready means listening
On a terminal, start waits until each process's own process group is listening on its port. The default limit is 30 seconds, and ready.timeout_s changes it. A process with no port must stay up for five seconds. This is a socket check, not an HTTP health check, and pando never binds the port itself to test it.
When a process does not come up, you get the last lines of its log and the reason. From a script or an agent, start returns as soon as everything is spawned; --wait makes it wait there too.
when one does not come up
$ pando start feat/login
pando: starting api
pando: starting web
pando: waiting for api, web to be ready
pando: api is ready (1.9s)
pando: the last lines of the web log:Error: Cannot find module 'next'pando:web failed: process exited with status 1 — `pando logs feat/login --source web` has the rest$ echo $?
1
Environment variables, never files
pando never edits a .env, a framework config or a package.json to set a port. Ports and service addresses reach your app as environment variables, or as {port:role} in its command.
One process can learn another's port the same way. Here the web app gets the API's address, whatever port the API was given:
{port}, {port:api}, {name}, {branch}, {worktree}, {root}, {project}, {log}. An unknown placeholder is a config error, never an empty string.
Every process also gets PANDO_NAME, PANDO_BRANCH, PANDO_WORKTREE, PANDO_ROOT and PANDO_PROJECT. What pando check runs also gets PANDO_CHECK=1, so a process that reaches outside its worktree can leave that out of a test.
eval "$(pando status --env feat/login)" puts that same environment in your own shell.
# ~/.pando/projects/acme-shop-2bdf70d7/pando.toml[project]
provision = [".env"] # gitignored; linked in
install = "pnpm install --frozen-lockfile"[processes.api]
cmd = "pnpm --filter api dev"
cwd = "apps/api"
ports = { PORT = "api" }
[processes.web]
cmd = "pnpm --filter web dev --port {port:web}"
cwd = "apps/web"
ports = ["web"]
env = { VITE_API_URL = "http://localhost:{port:api}" }
ready = { timeout_s = 90 }
[[hooks]]
name = "migrate"
after = "services"
on = "isolated"# only on data of its own
cmd = "pnpm prisma migrate deploy"
fingerprint = ["prisma/migrations/**"]
A URL, and a public one
Each worktree has one local URL, http://localhost:<port>, for its web role, or else for the first role that owns a port. pando open opens it and prints it, so it works over SSH too.
pando share feat/login publishes it through a cloudflared quick tunnel. It needs no account, and it prints one line: https://<random>.trycloudflare.com. Every share gets a new host. It closes on unshare, stop, rm, and a restart of the whole worktree; restart --only keeps it.
Public means public. Anyone with the link reaches the app, and the TUI asks before it shares. If your app needs a login, [share].auth_cmd runs a command in the worktree, and its output becomes a Cookie header. A small proxy, bound to 127.0.0.1, puts that header on every visitor's request in place of any cookies their browser sent, so the link opens already logged in, as the same user for everyone. That logs visitors in; it does not keep anyone out.
Pull requests become worktrees
In the TUI, p lists the repository's open pull requests through the GitHub CLI. Typing narrows them by number, title, branch or author. ⏎ makes a worktree for one, or goes to the worktree it already has.
A pull request from a fork has no branch on origin, so pando fetches refs/pull/<n>/head into a branch of its own, pr-<n>/<branch>. One whose branch cannot be found is refused, rather than started as an empty branch with the same name.
╭ open pull requests ────────────────────────────╮│ filter ▏││││▸#131 fix VAT rounding @sam││#128 cart drawer @alex││#124 search filters has a worktree││││⏎ worktree ↑↓ choose esc cancel│╰────────────────────────────────────────────────╯
It is TUI-only for now, and needs gh signed in.
The main checkout runs too new in 0.5.0
pando start main, or pando start with no name from inside it, runs the main checkout's processes on ports pando allocates. It is listed first, marked ⌂. The checkout is yours and you set it up, so pando runs its processes and nothing else: no install, no hooks, and always on the project's own services. rm never removes it. Careful: pando stop with no name inside the main checkout stops every worktree, not only the main one.
06·Data// three modes
›Its own database, if you want one.
the code is always its own; the mode decides where its data lives
In every mode, a worktree's code, processes, dependencies, ports and logs are its own. The mode decides one thing: which database and cache it talks to. You choose per worktree, with ⏎ in the TUI or a flag on start, and pando remembers it: a plain start keeps the mode it last ran in.
shared the default
start --shared · S
The main checkout's servers, and its data. The worktree's processes get the main checkout's own values for the database and cache variables, read from its .env or env example. Nothing starts and nothing waits.
owncode · processes · ports · logs
main'sdatabase · cache · their data
namespaced experimental
start --namespaced · ⏎ chooser
The main checkout's servers, with a database and a Redis slot of the worktree's own inside them: shop__feat_login beside shop, and slot 3 beside slot 0. The database starts empty and is built by the branch's own schema step.
own… + a database, a Redis slot
main'sthe servers themselves
isolated
start --isolated · i
Servers of its own, on ports of its own. They are containers from your compose file, in a compose project of their own, or native servers started from a recipe when you would rather not run Docker.
own… + every server, every byte of data
main'snothing
How the app finds its database
Through its environment, never by writing a file. For each variable the config maps to a service, such as DATABASE_URL = "postgres", pando reads the value your env files already have and changes only what the mode needs:
# isolated: the same URL, its own port.env DATABASE_URL=postgres://acme:secret@localhost:5432/acme
feat/login DATABASE_URL=postgres://acme:secret@localhost:30010/acme
.env REDIS_URL=redis://localhost:6379feat/login REDIS_URL=redis://localhost:30011
isolated: only the port changes
# namespaced: main's server, its own databasemain DATABASE_URL=mysql://app:pw@localhost:3306/shopfeat/login DATABASE_URL=mysql://app:pw@localhost:3306/shop__feat_loginmain REDIS_URL=redis://localhost:6379
feat/login REDIS_URL=redis://localhost:6379/3
namespaced: host, port and login stay main's; only the name changes
Isolated, with your compose file
An isolated start runs your compose file under a compose project named for the worktree. The containers, the network and the named volumes are therefore separate for every worktree. A generated override, kept under ~/.pando, publishes each service on a port pando chose, bound to 127.0.0.1. Your compose file is never edited.
pando refuses to isolate a service, and names it, when its data could land in the repository or be shared between worktrees. That covers bind mounts, external: or name-pinned volumes, driver_opts, a depends_on outside include, and a service with no port pando can find.
$ docker compose -p pando-shop-0b790793-feat-login-0499 \
-f docker-compose.yml \
-f ~/.pando/projects/shop-0b790793/compose/feat+login.override.yml \
up -d postgres redis
# the override, generated on every isolated startservices:postgres:
ports: !override ["127.0.0.1:30010:5432"]
container_name: !reset
redis:
ports: !override ["127.0.0.1:30011:6379"]
container_name: !reset
Isolated, without Docker
A native service is a recipe: a TOML file that says which binaries it needs, how to start one server on a given port and data directory, and how to tell when it is ready. Four are built in: postgres, mariadb, redis, and mongodb, which is marked untested. A file with the same name in ~/.pando/recipes/ replaces a built-in outright.
Each server gets ~/.pando/projects/<id>/data/<worktree>/<service>, is initialised once, and binds 127.0.0.1 only. The built-in servers have no passwords: nothing off your machine can reach them. pando never installs an engine. If one is missing, it prints the install command.
# src/recipes/builtin/redis.toml (abridged)
kind = "service"
name = "redis"
binaries = ["redis-server", "redis-cli"]
install = "brew install redis (or your distribution's redis-server package)"
notes = "no password, bound to 127.0.0.1 — nothing off this machine can reach it"[service]
port_env = "REDIS_URL"
cmd = "exec redis-server --port {port} --bind 127.0.0.1 --dir {datadir} --daemonize no"
ready = "redis-cli -h 127.0.0.1 -p {port} ping"[namespace]# what namespaced mode means on redis
kind = "slot"
slots = 16
Namespaced, and careful about it
A namespaced start writes into a server that belongs to you, which pando's rule about your repository does not cover. So the mode has rules of its own:
It logs in the way your app does, with the user and password in the main checkout's .env. If there are none, it asks once. The answer is kept in pando's own config, with file mode 0600, and passed to the database client in its environment, never on a command line.
That login must be allowed to create databases named after the main one, and only those. The first start that is not allowed stops with nothing made, and prints the grant to run once as an administrator.
A Redis slot is given only where the app reads a slot setting (REDIS_DB, or the path of its URL), and only if the slot is empty. Keys pando did not put there belong to someone else. Slot 0 and main's own slots are never given out.
mariadb on localhost:3306 does not let the login from the URL in
DATABASE_URL make or drop shop__feat_login — run this once as an
administrator of that server:GRANT ALL ON `shop\_\_%`.* TO 'app'@'localhost';It lets that login make and drop databases named shop__… and
nothing else. Nothing was made.
Every drop goes through one check. A record must say pando made that database for that worktree. The name must never be main's, must carry the __ marker, and must not be named by any other record.
doctor lists a leftover shop__… database with the command that drops it, and never drops it itself.
Today, namespaced mode makes MariaDB and MySQL databases and Redis slots. Every other service stays shared, and the start says so for each one. What a namespace is on an engine is the [namespace] table of its recipe, so another engine is a recipe rather than a release.
What happens to the data
mode
stop
switch mode
rm
shared
nothing: the data is main's
nothing
nothing
namespaced
kept
kept, for when it comes back
dropped, only if pando's records say pando made it
isolated
kept: containers stopped, servers stopped
kept
removed: docker compose down -v, data directory deleted
07·The promise// invariant 1
›Never a byte in your repository.
not a config file, not a gitignore line, not a lockfile change
Invariant 1: pando never writes into your repository
pando's own files go to two places, apart from a socket directory in $TMPDIR for native Postgres and MariaDB (below):
Its own home, ~/.pando/
The worktrees it creates, which live under that home by default
Inside .git, only git itself writes: its records of the worktrees, branches and fetches pando asks for.
Your checkout is read-only to pando.
Inside a worktree, pando may create or link a file only if the project's own gitignore already ignores that path. .env and .env.local pass. Anything that would show up as untracked in git status is refused.
~/.pando/├── config.toml your choices: theme, appearance├── recipes/themes/your native services, colour themes└── projects/acme-shop-2bdf70d7/ ├── pando.toml the whole config for one project ├── state.json what runs where, on which ports ├── worktrees/feat+login/a stem: the branch's own checkout ├── logs/feat+login/every process's log ├── data/feat+login/its private services' data └── compose/generated compose overrides~/code/acme-shop/untouched
everything pando learns and runs lives under its own home; $PANDO_HOME moves it
How the promise is kept
Every path is computed. Every location pando writes is a function of its home and a project id, and a test checks that none of them is inside the repository. A home or a worktrees_dir inside the checkout is refused before anything is made.
git check-ignore runs before every write into a worktree. It runs in the main checkout first, then again inside the new worktree right before each file is written, because a branch can carry an older gitignore. A refusal found after the worktree was made undoes it.
Hooks and installs are watched.git status is taken before and after them, and pando warns about anything new that appeared.
Every command is checked by a test. After each one, the test suite checks that git status has nothing to report, and that every path in the repository outside .git has the same type, size and symlink target as before.
What that does and does not mean. Git itself records worktree admin files in .git/worktrees, and new branches and fetched refs in .git. A worktree gets the gitignored files you listed linked into it, and its own dependencies installed, such as node_modules; the worktree itself lives under ~/.pando. Native Postgres and MariaDB put a socket in $TMPDIR/pando-…, because a socket path longer than 104 bytes does not work on macOS. Namespaced mode, when you choose it, makes databases in your own server. Nothing pando writes shows up in your checkout's git status.
Invariant 2: the runtime never calls AI
Discovery may be fuzzy. Starting processes may not.
Detection is the only thing that writes pando.toml, whether init runs it or new, start and the TUI ask it just in time. Spawning, stopping, sharing and removing only ever read it. There is no model SDK and no HTTP library in the binary. pando itself talks only to this machine; what reaches the network are the programs it runs: git, gh, cloudflared, docker, your install and hook commands, and in namespaced mode the database clients.
A wrong guess costs one edit to a file you can see. It never costs a mysterious failure at start time.
08·First run// detection
›It reads your repository first.
the lockfile, the dev script, the env example, the version file, the compose file
You do not write a config file to start. Before it asks anything, pando reads the main checkout: package.json scripts, Makefile and justfile targets, lockfiles, workspace markers, version files, the first env example, framework markers, compose files, and the gitignored files at the root. A root with no manifest of its own, such as backend/ beside frontend/, is read one directory down and in apps/* and packages/*: each app's scripts, lockfile, version file and env files, each installed and run in its own directory. Nothing starts while it reads.
From that it proposes answers to ten questions, always in the same order, each with the evidence behind it:
On a terminal, new, start and the TUI take the rules' first choice for anything they have one for, and print what they took:
What the rules know, as data, one row per fact:
package managers
pnpm, npm, yarn, bun, uv, poetry, pipenv, bundler, composer, mix, go, cargo: one row each with its lockfile, and a frozen install for the nine that need one
pando: process list: using "npm run dev"(package.json scripts.dev, which starts the
workspace's apps itself; API_PORT and WEB_PORT in the env example), pando's first
choice, over 1 other option — change it in ~/.pando/projects/<id>/pando.toml
One file, with its reasons in it
The answers go to ~/.pando/projects/<id>/pando.toml, never into your repository. Each line pando writes carries its reason: # detected: pnpm-lock.yaml, or # answered: 2026-09-27. The file is yours to edit. pando adds keys that are missing, and changes an answer only when asked to, with init --answers - --replace.
pando init puts every open question to you in one pass instead of taking defaults. pando doctor says what was detected, from where, and what is missing. A question pando has no option for at all is still asked, and so is the one real choice isolation brings: which services get private copies.
Config comes in layers, highest first: pando's own project file, then a machine-wide ~/.pando/config.toml. For now, a pando.toml committed in the repository is read beneath them and never written; whether to honour one is still an open decision.
# pando.toml — pando's own config for this project.
# Lines marked "# detected:" or "# answered:" were written by pando.
# Everything here is yours to edit; pando only ever adds keys it is missing.[project]
install = "pnpm install --frozen-lockfile"# detected: pnpm-lock.yaml
provision = [".env", ".env.local"] # detected: gitignored and present in the main checkout[runtime]
version_files = [".nvmrc"] # detected: .nvmrc[dev]
cmd = "pnpm dev"# detected: package.json scripts.dev
ports = { PORT = "web" } # detected: the Next.js convention[[services]]# detected: docker-compose.yml, postgres:16 → DATABASE_URL
kind = "compose"
file = "docker-compose.yml"
include = ["postgres", "redis"]
env = { DATABASE_URL = "postgres", REDIS_URL = "redis" }
Every project is a little different, so the surest start is to let your coding agent look at it. It sets pando up, tests it, and tells you when you're ready. It never changes a file in your project.
1 Copy this prompt:
Set up pando here: run `pando init --agent` and follow what it says.
2 Paste it into Claude Code or Codex, opened in ~/code/acme-shop
3 Come back here: this screen turns green by itself when it's done
⠋waiting for the setup…
a copy the prompt⏎ let pando try on its ownesc just manage worktrees? help
The first time you run pando in a project with nothing to run, it opens a setup screen instead of the list, drawn over a grove of its own.
Copy one line with a and paste it into your own coding agent, opened in the project.
pando init --agent prints the job for this project and this version: what pando already sees, the questions only the project can answer, and the steps.
The agent answers through pando init --answers - on stdin, never through a file in your repository. --dry-run previews and --replace corrects an answer.
pando check proves it. See below.
The screen turns green by itself. From then on, pando opens the list.
No agent? Press ⏎ and pando tries its own guess, tested by the same check. esc skips the setup entirely; nothing is ever gated on it. A project configured before this existed is never sent there.
pando check: proof in a throwaway worktree new in 0.5.0
pando check makes a detached worktree of the commit a new branch would fork from, so there is no branch. It installs it, starts every process, waits until they are ready, and asks the process that owns the URL for /. Then it stops and removes the worktree, keeps its logs, and records the result in check.json.
A status of 500 or higher, a refused connection or silence is a failure. Any other answer passes: a login redirect is a working app. It runs on the shared services and skips the hooks that run after them, so it never migrates your own data. When a namespaced login already exists, it proves the schema step in a namespace of its own.
pando check
pando: testing acme-shop's setup at a1b2c3d (origin/main), in a throwaway worktree (no branch)
pando: installing
pando: starting api, web
pando: waiting for api, web to be ready
pando: asking web for its first page on :29849
pando: installed 14.2s · api ready :29848 · web ready :29849 (HTTP 200)
pando: removed the test worktree; nothing left behind✓ acme-shop is ready: `pando` opens it
Built for agents too
An agent reads pando the way a script does. stdout carries only the answer, and stderr carries the narration, each message starting with pando: . ls, status, logs, check and doctor print JSON with --json, and signals always prints JSON, in shapes that are versioned and documented in agent/json.md.
exit
means
0
ok
1
an error
2
a usage mistake
3
pando has a question, printed on stderr. It is not a failure. An agent answers it from the project's own evidence, or puts it to the developer, and never adds --yes to make it go away.
The repository includes a Claude Code plugin and Codex skills, each a thin wrapper around one brief (agent/brief.md). The binary carries the brief as well: pando init --agent --reference brief. When the check passes, the agent offers to save a short note on how to run this project with pando, in its own memory and never in your repository. It saves the note only if you say yes, and later sessions then start and stop worktrees through pando instead of by hand. new in 0.5.0
A worktree is named by its branch (feat/login) or its directory (feat+login). Inside a worktree, start, stop, restart, logs, open, share and unshare need no name. stdout carries only the answer; narration goes to stderr.
pando
Open the TUI for the repository you are in. The first time, in a project with nothing to run, the setup screen new in 0.5.0.
new <branch>
Create a worktree and branch from the default base: check out, link the provisioned files, run the install step.
--base <ref> fork from here · --yes take pando's first choice for any question
start [name]
Start its processes on its ports. On a terminal, wait until each is listening on its port.
Stop its processes, its private services and its public URL. Ports, logs and data are kept.
--only <process> · --all every worktree (and the main checkout new in 0.5.0)
restart [name]
Stop and start again, keeping the ports.
--isolated · --namespaced (experimental) · --only <process> · --wait / --no-wait · --yes (the way back to shared is start --shared)
ls
Every worktree, the main checkout first new in 0.5.0: status, URL, ports, git. The columns fit the terminal.
-l adds HEAD and path · --json
status [name]
What runs where, per process and per service, with its reason when it failed.
--json · --env prints export lines: eval "$(pando status --env feat/login)"
logs [name]
Its log: one process, or all of them merged, with the process name in front of each line.
-s, --source <process> · -n, --tail N (50) · -f, --follow · --json one object per line
open [name]
Print its URL and open it in the browser ($BROWSER, or the desktop's opener). A worktree that serves no page, only an app a phone runs, has the app opened instead: on the booted iOS simulator, a connected Android device or emulator, or a simulator it starts.
--public the shared URL · --app its app, even beside a page
share [name]
Publish a running worktree through a cloudflared quick tunnel and print only the public URL.
unshare [name]
Take the public URL down.
path <name>
Print the worktree's absolute path: cd "$(pando path feat/login)".
rm <name>
Stop everything, remove the worktree, wipe its logs and data. The branch is kept, and the main checkout is never removed.
--force even with uncommitted changes · --yes a worktree pando did not create
init
Answer every setup question now, in one pass, instead of taking defaults as you go.
--yes · --answers <file|-> · --dry-run · --replace, --agent the job for a coding agent, --reference brief|json|memorynew in 0.5.0
check
Test the setup in a throwaway worktree: install, start, ask for the page, remove. new in 0.5.0
--base <branch> test the commit this branch is at, for this run only · --json
doctor
What pando found and where, and what is missing: project, config, runtime, tools, worktrees, services, hooks. Read-only.
--json · --adopt <old-id> after you moved the repository
signals
The detection evidence as JSON: what the repository says about how to run itself. Two runs print the same thing.
completions <shell>
A completion script for bash, zsh, fish, elvish or PowerShell; in bash, zsh and fish it completes worktree names too.
TUI keys
jk↓↑move down / up · gG first / last
⏎choose its mode (shared, namespaced, isolated) and start it, or switch it when it runs
sstart it, in the mode it last ran in
iSstart it isolated / shared (asks when it runs otherwise)
xXstop it (x twice when it runs) / stop everything (asks)
rPrestart it (r twice when it runs) / restart only the ▸ process
oOopen its URL / its public URL
cCycopy its local URL / public URL / path
tshare it publicly, or stop sharing (asks first)
!ea shell in it (a tmux window inside tmux) / open it in your editor
lthe log viewer · tab previews the next process's log
ndnew worktree / remove it (never the main checkout)
popen pull requests: ⏎ makes a worktree for one
/filter by branch or name
bsort the list: by PR, newest first, last run first, by name (saved)
mmessages: what pando said, in full
avcopy the setup prompt / test the setup with pando checknew in 0.5.0
Tpick a colour theme, previewed live
R?qrefresh now / help / quit
The log viewer
1…9a source: all first, then one per process, hook and service
/nNsearch, next / previous match · & only the matches, like grep
flevel filter: all, warnings and up, errors
eEnext / previous error
⏎Jinspect the line: JSON, pretty-printed
gGtop / follow the live tail
wyYwrap long lines / copy the line / copy the URL on it
Levels are read from keywords in the line, so treat the filter as a guide: a crash line without the word error in it can be filtered out. When the viewer opens, the all tab shows each process's earlier lines one process after another rather than interleaved by time; lines that arrive after that follow in order.
pando.toml, every section
It lives at ~/.pando/projects/<id>/pando.toml, file mode 0600, and pando writes it for you. You never have to write one; this is what you can change.
show the full shape
[project]
base = "main"# what new branches fork from
provision = [".env", ".env.local"] # gitignored files each worktree gets
provision_mode = "copy"# default: a symlink to the main checkout's
install = "pnpm install --frozen-lockfile"[runtime]
prelude = ""# e.g. a version manager's init, before every command
version_files = [".nvmrc"]
[dev]# one process called dev; or [processes.<name>] for several
cmd = "pnpm dev"
cwd = "."
ports = { PORT = "web" }
env = { NODE_ENV = "development" }
ready = { role = "web", timeout_s = 30 }
page = true # false: no browser opens it, so it is never the URL (Expo's Metro by default)[[services]]
kind = "compose"
file = "docker-compose.yml"
include = ["redis"]
env = { REDIS_URL = "redis" }
[[services]]
kind = "native"
name = "postgres"
preset = "postgres"# a recipe; the lines below override it
port_env = "DATABASE_URL"
init = "initdb --pgdata {datadir}"
cmd = "exec postgres -D {datadir} -p {port} -k {socket_dir}"
ready = "pg_isready -h 127.0.0.1 -p {port}"[[hooks]]
name = "migrate"
after = "services"# create · install · services · dev
on = "isolated"# isolated (the default with [[services]]) · always · never
fingerprint = ["prisma/migrations/**"] # runs again only when these change
cmd = "pnpm prisma migrate deploy"[[probes]]# checked just before processes spawn
name = "native-abi"
cmd = "node -e 'require(\"better-sqlite3\")'"
match = "NODE_MODULE_VERSION"
hint = "Rebuild native modules under the dev runtime."[branches]
rules = [{ match = "*-beta", base = "beta" }]
[share]
provider = "cloudflared"
auth_cmd = "./scripts/dev-cookie.sh"# its stdout becomes the visitor's Cookie header
Machine config and themes
~/.pando/config.toml holds what belongs to the machine rather than to a project: which isolation to prefer, and how the TUI looks. T in the TUI lists the themes with a swatch each, repaints as you move, and saves the one you keep.
Built in: pando's own, Catppuccin, Flexoki, GitHub (default, dimmed, high contrast, colorblind), Gruvbox, Kanagawa, Monokai Pro, One Dark, Rosé Pine, Tokyo Night, VS Code and Zenwritten, each with a dark and a light half that follows the system. A theme is a background, a foreground and seven accents for each half, and every other colour is derived from those. A file in ~/.pando/themes/ adds one, or replaces a built-in with the same name.
# ~/.pando/config.toml[isolation]
prefer = "native"# or "compose"[ui]
theme = "catppuccin"# appearance = "dark" # "light", or "auto"
# sort = "run" # the list's order: "pr", "newest", "run" or "name"
# theme_from = "~/.config/theme-switcher/current"
# its first line names a theme; pando follows it
What it needs
for
you need
worktrees
git
isolated services
Docker with compose, or the engine a recipe names (postgres, mariadb, redis-server…). pando prints the install command and never runs it.
namespaced mode
a MariaDB/MySQL or Redis server your main checkout already uses, and its client (mariadb, redis-cli)
sharing
cloudflared. No account.
pull requests
the GitHub CLI, gh, signed in
pando doctor checks all of them and says which command needs which.
Questions developers ask
How is this different from plain git worktree?
git worktree add ../x feat/x gives you a second checkout and nothing else. pando new feat/x runs that same git command, but puts the checkout under ~/.pando instead of beside your repository. It links in the gitignored files the project needs, such as .env, runs the project's own install inside the checkout, and records it. pando start then adds what makes it run: ports that do not collide, detached processes, logs and readiness. Optionally it adds private or namespaced services and a public URL, all on one screen. Worktrees you made by hand are managed too.
Does every worktree get its own node_modules? Its own .env?
It gets its own dependencies. pando runs the project's frozen install inside each worktree and never links dependency directories. A later start runs the install again only when a lockfile, a version file or the prelude changed. Local config works differently: gitignored files like .env are symlinked to the main checkout's copy by default, so editing one edits main's. provision_mode = "copy" gives each worktree its own copy.
What survives closing the terminal? A reboot?
Closing the terminal, killing the tmux pane or quitting the TUI leaves everything running: each process has its own session and no terminal. A reboot stops them, as it stops anything. pando notices the new boot and forgets the old process ids, but keeps each worktree's ports and mode, so the next start comes back on the same URLs. Nothing restarts by itself: there is no daemon and no supervisor.
My Node from nvm (or fnm, rvm…) is not found.
pando starts commands with bash -lc, a login bash, which reads ~/.bash_profile and never ~/.zshrc. A version manager set up only in .zshrc does not exist there. pando doctor shows which binary each language resolved to. The fix is a one-line [runtime].prelude for your machine, kept in ~/.pando/config.toml rather than in the repository.
I moved my repository and pando forgot it.
A project's pando data is keyed by the folder's name and a hash of its path, so after a move pando sees a new project. pando doctor finds the old one, and pando doctor --adopt <old-id> moves its config, state and worktrees over.
Does pando new fetch the newest base first?
No. A new branch forks from the last-fetched origin/<base>, so run git fetch first when you need the newest commit. An existing branch is looked for locally first, then on origin, where it is fetched if missing.
How many worktrees can run at once?
As many as your machine can run. Every running worktree runs its own dev servers, and in isolated mode its own databases too. The ceilings in the code are 1,971 port windows, and 15 Redis slots per Redis server for namespaced worktrees. The TUI is built for dozens of rows.
Is a share link private?
No. Anyone with the link reaches the app until you unshare, stop or remove the worktree. The link does not expire, and every share gets a new random host. With auth_cmd, every visitor gets the same session, because the proxy replaces their Cookie header with the one your command printed.
Logged in on one worktree, logged in on all of them?
Browsers keep cookies per host, not per port, so every http://localhost:<port> shares them. That is how the web works, and pando does not change it. When two branches' sessions get in each other's way, use a private window for one of them.
Linux? Windows?
macOS is where pando is developed. On Linux, CI runs the whole test suite for every change and it must pass, but nobody has used pando there for real work yet. It does not compile on Windows, because it uses Unix process APIs throughout.
Does pando use AI, or phone home?
No and no. Detection is rules and data tables, and the runtime never calls a model. The agent in the first run is your own coding agent, which pando never launches: the setup screen only copies a prompt for you to paste. There is no telemetry in the code. pando itself connects only to this machine: pando check asks your app for / on localhost, and the share proxy forwards to it. What reaches the network are the programs it runs: git, gh, cloudflared, docker, your install and hook commands, and in namespaced mode the database clients.
10·Install// one line
›Install it. Run it.
a ready-made binary for macOS (Apple silicon and Intel) and Linux (x86_64 and arm64): no Rust needed
1Install it
With Homebrew, on macOS or Linux:
Homebrew
$ brew install mertkaradayi/tap/pando
Or with nothing else installed, the install script:
the install script
$ curl --proto '=https' --tlsv1.2 -LsSf https://github.com/mertkaradayi/pando/releases/latest/download/pando-cli-installer.sh | sh
The script puts pando in ~/.local/bin and adds it to your PATH; run it with PANDO_CLI_NO_MODIFY_PATH=1 to leave your shell files alone. Every download has a checksum and a GitHub attestation. pando --version says which version you have.
2Run it in your project
Open a terminal in any git repository you work on, your own project, not pando's, and type:
$ pando
The first time, it opens the setup screen. After that, the list of every worktree. There's nothing to configure first, and nothing is written into your checkout. pando completions zsh (or bash, fish…) prints a completion script.
Building it yourself? It needs Rust 1.88 or newer; with no Rust on the machine, rustup installs it first:
Want to see it before you point it at your own project? In a clone of pando's repository, its test suite can build repositories of every shape it knows. One of them is a good place to start:
$ scripts/fixture-repo.sh --list
$ scripts/fixture-repo.sh mono-web-api --listener
# builds one in a temporary directory,
# and prints how to run pando in it
There is no crate on crates.io yet.
Where it stands, honestly
What there is
Every command on this page is implemented on main and covered by tests. 0.6.2 is the last release; what is marked new came in 0.5.0
Tests that read no developer's shell profile and need none of their tools; databases in the tests are throwaway servers the tests start themselves
Developed on macOS. For every change, CI checks formatting and runs clippy and the whole test suite on macOS and on Linux, and all of it must pass
What there is not, yet
No crate on crates.io yet (the binaries, the install script and the Homebrew tap are here)
Nobody has used pando on Linux for real work yet: the suite that passes there runs on fixtures. Windows is not a target
Almost every worktree pando has made was in a generated fixture. The one real project it met found three bugs in an afternoon, all fixed since
Namespaced mode is experimental
A tool this heavily tested against situations it invented still breaks on first contact with one it did not. If pando breaks on your project, that is the most useful issue you can open. Next, roughly in order: a published crate, a JSON schema for pando.toml, and a recipe directory.
Contributing
pando is small enough to read and strict about how it grows. Most of what it knows about the ecosystem is data, so many useful changes are a single row or a single TOML file:
you know…
your contribution
a database or cache it should run natively
a recipe: src/recipes/builtin/*.toml
a framework it misreads
a rule: src/catalog/frameworks.rs
a package manager or lockfile
a row: src/catalog/package_managers.rs
a language or version manager
a row: src/runtime/languages.rs
a colour scheme you love
a theme: src/theme/builtin/*.toml
a project shape it breaks on
a fixture and a failing test
Every change passes cargo test, cargo clippy --all-targets -- -D warnings and cargo fmt --check.
security problems through SECURITY.md, never a public issue
Licence
pando is free software under the GNU Affero General Public License v3.0 only. You may use, study, change and share it. If you distribute it, or a program built from it, or let people use a modified version over a network, you must offer them its complete source under the same licence.