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 expensivenpm 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.

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:start fails with port is already allocated.
  • If worktrees instead share one running instance, a migration or seed:skills in 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.local is copied at creation time only. Adding a new env var to main/.env.local later doesn’t propagate to existing worktrees. Copy manually when this matters.
  • node_modules is per-worktree. Each new worktree needs its own npm 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 kanvas brings 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 you mv the repo root, run git -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


This site uses Just the Docs, a documentation theme for Jekyll.