Worktrees & tmux
The local dev workflow for working on multiple branches in parallel: bare-repo git layout + tmux for session isolation. Designed to keep multiple in-flight branches (and their Claude Code sessions) alive at the same time without stashing, rebuilding, or losing context.
The shell helper that drives this workflow —
kanvas— has its own reference doc: Kanvas CLI.
Why
A typical setup puts you on one branch at a time. Switching means stashing or committing WIP, checking out another branch, often re-running npm install and migrations. Painful here because:
- Supabase branching — every PR gets its own preview DB and migrations roll forward per branch. Multiple branches in flight = multiple Supabase preview DBs already; the bottleneck is your single local checkout. Worktrees fix that.
- Claude Code context — each session accumulates context per worktree. Switching branches in one checkout kills it. Worktrees keep parallel Claude sessions alive.
- Cold starts are expensive —
npm install+ Next dev server warmup + supabase start adds up. Worktrees pay this cost once per branch.
Every branch lives in its own filesystem directory (a “worktree”), each with its own node_modules, dev server, and Claude session. You switch by switching tmux windows, not by switching git state.
The bare-repo layout
~/Workspace/kanvas/
├── .bare/ bare clone — shared object DB and refs
├── .git one-line file: "gitdir: ./.bare"
├── main/ worktree on the `main` branch
└── KANV-1234/ worktree on the `KANV-1234` branch (created by kanvas helper)
The repo root (~/Workspace/kanvas/) is not a checkout — it’s a parent directory holding the bare clone and all worktrees. The main branch is just another worktree. This pairs naturally with tmux: one tmux session per repo, one window per worktree.
Setup (one-time)
1. Migrate your local clone to the bare layout
If your ~/Workspace/kanvas is currently a normal checkout:
# Build the new layout side-by-side
NEW=~/Workspace/kanvas-new
git clone --bare git@github.com:oceanic-horizon/kanvas.git $NEW/.bare
printf 'gitdir: ./.bare\n' > $NEW/.git
# Fix the bare clone's refspec so all remote branches show up
git -C $NEW/.bare config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'
git -C $NEW fetch origin
# Materialize main as a worktree
git -C $NEW worktree add main main
git -C $NEW/main branch --set-upstream-to=origin/main main
# Copy untracked files (.env.local, etc.) into the new main worktree
cp ~/Workspace/kanvas/.env.local $NEW/main/
# Verify, then swap
mv ~/Workspace/kanvas ~/Workspace/kanvas-old
mv $NEW ~/Workspace/kanvas
# Worktrees store ABSOLUTE paths; repair after the move
git -C ~/Workspace/kanvas/.bare worktree repair ~/Workspace/kanvas/main
Verify:
cd ~/Workspace/kanvas/main
git status # On branch main, clean
git log --oneline -3 # matches what your old checkout had
git -C ~/Workspace/kanvas worktree list
Once you’re happy, delete the backup: rm -rf ~/Workspace/kanvas-old.
2. Install the kanvas shell helper
See the Kanvas CLI page for installation and command reference. Briefly: save the function to ~/.config/shell/kanvas.sh and source it from ~/.zshrc.
3. Recommended ~/.tmux.conf
The walkthrough below uses these keybindings. If you skip this, the default tmux prefix is Ctrl-b and windows are numbered from 0.
# Prefix on Ctrl-Space (easier than the default Ctrl-b)
unbind C-b
set -g prefix C-Space
bind C-Space send-prefix
# Windows and panes start at 1
set -g base-index 1
setw -g pane-base-index 1
set -g renumber-windows on
# Mouse: scroll, click panes, drag-resize
set -g mouse on
# Large scrollback
set -g history-limit 50000
# Reload config
bind r source-file ~/.tmux.conf \; display "Reloaded"
# Vim-style pane navigation
bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R
# Sensible splits — open in the current pane's path
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
Then tmux kill-server (if a server is running) and start fresh.
tmux essentials
Prefix key: Ctrl-Space (the recommended config). All chords are: press Ctrl-Space, release, then press the next key.
| Key | Action |
|---|---|
Ctrl-Space d | Detach. Claude keeps running in the background. |
Ctrl-Space w | Window picker — interactive list of every open worktree. The killer feature. |
Ctrl-Space 1..9 | Jump to window by number. |
Ctrl-Space n / p | Next / previous window. |
Ctrl-Space , | Rename current window. |
Ctrl-Space [ | Scrollback mode. Arrows / PgUp / PgDn. q to exit. |
Ctrl-Space ? | Show every keybinding. |
Ctrl-Space s | Session picker (when you have multiple repos open). |
Ctrl-Space r | Reload ~/.tmux.conf. |
Reattach from any terminal: tmux attach -t kanvas. List active sessions: tmux ls.
The session lives as long as it has at least one window. When kanvas --clean removes the last feature window, the session auto-destroys — no orphan sessions.
A walkthrough
# Start
cd ~/Workspace/kanvas
kanvas KANV-1001 # tmux session 'kanvas', window 'KANV-1001', Claude running
# inside tmux:
# ... work happens ...
# Ctrl-Space d # detach
# Back in your shell. Start a second branch in parallel:
kanvas KANV-1002 # adds window 'KANV-1002' to the same session
# Inside tmux:
# Ctrl-Space w # picker shows both windows
# ... work on KANV-1002 ...
# Ctrl-Space p # jump back to KANV-1001
# Hours later, in any terminal:
tmux attach -t kanvas # everything still running
For the actual kanvas command reference (--clean, --all-merged, --force, etc.) see the Kanvas CLI page.
Supabase branching lifecycle
The Supabase GitHub integration manages preview DBs per PR branch:
- First push of a branch → Supabase creates a preview branch + runs migrations.
- PR merges → GitHub deletes the remote branch → Supabase tears down the preview.
kanvas --clean <name>only handles local cleanup — it never touches the Supabase preview branch.
Don’t delete the remote branch manually before the integration has caught up; let the PR-merge flow do it.
Local Supabase per worktree
supabase/config.toml is committed, so every worktree inherits the same local ports (api 54421, db 54422, studio 54423, inbucket 54424, analytics 54427, shadow 54420). Consequences when 2+ worktrees want a local Supabase at once:
- Only one worktree can bind the ports — a second
npm run supabase:startfails withport is already allocated. - If worktrees instead share one running instance, a migration or
seed:skillsin one branch silently mutates the DB the other branch develops against (schema drift, stale seeds).
Opt-in per-worktree isolation. Set KANVAS_SUPABASE_WORKTREE_ISOLATION=1 in the worktree’s .env.local. When set, /kenny and /build setup (see the /kenny skill’s Autonomous worker loop → Worktree-safe local Supabase section) detect a port collision and, if one exists, spin up an isolated instance for this worktree on a free alternate port block, repointing .env.local’s NEXT_PUBLIC_SUPABASE_URL at it. The mechanism uses git update-index --skip-worktree supabase/config.toml so the local port edit stays invisible to git status (keeping the tree clean for /ship) while the Supabase CLI still reads the alternate ports. Container names are already unique per worktree (Supabase derives the project id from the directory name); only the ports need isolating.
Unset (the default) → worktrees share the one instance, as before. Remote (dev/preview/staging/prod) Supabase is never touched — that’s owned by the Supabase GitHub integration above. Free the ports when done with npm run supabase:stop.
Gotchas
.env.localis copied at creation time only. Adding a new env var tomain/.env.locallater doesn’t propagate to existing worktrees. Copy manually when this matters.node_modulesis per-worktree. Each new worktree needs its ownnpm install. Worth it for isolation — different branches may need different deps.- First detach feels scary because Claude appears to vanish. It’s still running.
tmux attach -t kanvasbrings it back. Detach often. - macOS: install a recent tmux (3.x).
brew install tmux. The system tmux on older macOS misbehaves with mouse mode and modern terminals. - Worktree paths are absolute in
.bare/worktrees/<name>/. If youmvthe repo root, rungit -C <root>/.bare worktree repair <root>/main(and any other worktrees) to fix.
Where it all lives
| Thing | Path |
|---|---|
| Repo + worktrees | ~/Workspace/<repo>/ |
kanvas shell function | ~/.config/shell/kanvas.sh |
| Shell sourcing | ~/.zshrc |
| tmux config | ~/.tmux.conf |
These are personal dotfiles — each team member maintains their own copy. The kanvas repo itself doesn’t depend on any of this; you can use the project with a normal checkout and no tmux. But once you have 2+ branches in flight, this workflow pays for itself fast.
Further reading
git worktree— official docs for the underlying git command.- tmux wiki — terminal multiplexer reference.
- Supabase Branching — how preview databases work for each PR.
- Kanvas CLI — the
kanvasshell helper that drives this workflow.