_____
/ ____|
| | __ _ _ __ ___ _ __ _ _
| | / _` | '_ \ / _ \| '_ \| | | |
| |___| (_| | | | | (_) | |_) | |_| |
\_____\__,_|_| |_|\___/| .__/ \__, |
| | __/ |
|_| |___/
TUI for managing git worktrees with paired tmux sessions and per-project setup hooks.
Status: v0.14, daily-driven by the author. APIs and on-disk state may still shift before v1.
canopy new and ten seconds later you're attached to a tmux session with nvim, claude, and a shell, all on a fresh git worktree against an isolated database with its own port. Reboot your laptop, canopy switch <name>, and you're back exactly where you left off — claude conversation included.
AI-paired development means many parallel branches in flight at once: one agent refactoring auth, another fixing the timezone bug, plus the feature you're driving by hand. Raw git worktree + ad-hoc tmux new-session doesn't scale past three. Canopy is the missing orchestrator: per-workspace ports, per-workspace databases via scripts.setup, per-workspace tmux sessions with the same layout every time, and a TUI that shows which one is hot. See docs/landscape.md for where canopy sits next to Conductor, tmuxinator, raw git worktree, and the agent CLIs it hosts.
From inside any project that has a canopy.json:
canopy new # creates a workspace with a random name (e.g. bold-falcon)
canopy new --name fix-bug # explicit name
canopy main # opens a tmux session in the project root (no worktree)
canopy ls # workspaces in the current project
canopy ls --all # workspaces across every project (also implicit when run outside any project)
canopy switch <name> # attach (resurrect first if stopped; auto-reconciles status)
canopy rm <name> # tear down (archive script + tmux + git + branch)
canopy reconcile # update workspace statuses to match disk + tmux realityEach workspace gets a 3-pane tmux session: nvim top-left, claude top-right (with --continue on resurrect so prior conversation history resumes), and a shell full-width on the bottom. scripts.run (your dev server) launches on demand via canopy run rather than auto-starting — that way a stopped workspace resurrects to the same layout without a port collision.
Workspaces live at ~/.canopy/workspaces/<project>/<name> — canopy owns the storage so your source repo stays clean — and each one gets a unique TCP port via CANOPY_PORT.
canopy with no args launches a Bubbletea TUI — the same workspace list with arrow-key navigation, enter to attach, n to create, d to delete (with confirmation), U to upgrade canopy itself when a newer version is available, ? for help. CLI subcommands work alongside it; both call into the same workspace.Manager underneath.
Plus operational glue:
canopy init— onboard a project (createscanopy.json+ stubbin/canopy-*scripts; detects existingconductor.jsonand mirrors its schema)canopy version— version, commit, build datecanopy --debug— DEBUG-level JSON logs to~/.canopy/log/canopy.log(auto-rotated: 10 MB / 3 backups / 28 days / gzip)
Every workspace gets a unique TCP port via CANOPY_PORT, allocated through a Conductor-style block plan:
- Each project's first workspace lands on
base_port(default 3000). - Subsequent workspaces in the same project step up by
workspace_stride(default 10): 3000, 3010, 3020, ... - A new project's first workspace lands
project_stridehigher than the previous project (default 1000): cravd → 3000, brain → 4000, hey-cli → 5000.
Project-to-base assignments are first-come-first-served and persisted in state.json, so a workspace's port is stable across reboots.
Defaults are tweakable via ~/.canopy/config.json (optional file):
{
"ports": {
"base": 3000,
"project_stride": 1000,
"workspace_stride": 10
}
}Partial overrides are fine — any field you skip stays at the default.
If you live in tmux, two extra subcommands turn canopy into an always-one-keystroke-away workspace switcher and a glanceable status widget. Both are inside-tmux-only.
One-shot install:
canopy install tmux # writes managed block to ~/.tmux.conf (with backup)
tmux source-file ~/.tmux.confThat's it. The installer is idempotent (refuses if already present; --force replaces in place), backs up ~/.tmux.conf before any change, and writes a clearly-marked managed block so you can see what canopy added:
# canopy:start (managed by `canopy install tmux` — edit only outside markers)
bind g run-shell "canopy popup"
set -ag status-right " #(canopy statusline --format=current) "
# canopy:endWhat you get:
canopy popup—<prefix>gopens the global TUI in a tmux floating popup. Two tabs: Local (current project's workspaces, the default if you launched from inside a project) and Global (everything).Tabswitches tabs,/enters fuzzy search,Enterswitches to the selected workspace. Requires tmux 3.2+.canopy run—<prefix>rexecsscripts.run(e.g.bin/dev) from the nearestcanopy.jsonin a tmux popup. InheritsCANOPY_PORTand friends from the workspace tmux session. One keystroke instead of typingbin/dev.canopy statusline --format=current— appended tostatus-right, showscanopy: <name> <glyph> :<port>when you're attached to a canopy workspace's tmux session, and empty otherwise. Errors never propagate to stdout — your status bar stays clean even if state.json is corrupt or canopy crashes.
Manual install (if you prefer to keep your tmux config hand-curated): paste the block above into ~/.tmux.conf and source-file it.
curl -fsSL https://raw.githubusercontent.com/avinashjoshi/canopy/main/install.sh | shThat clones canopy to ~/.canopy/src, runs make install (which writes the binary to ~/.local/bin/canopy.bin and symlinks ~/.local/bin/canopy at it), and prints a PATH hint if ~/.local/bin isn't on your shell's PATH.
Idempotent: re-running on a machine that already has canopy installed prints "looks like canopy is already installed, run canopy upgrade instead" and exits 0.
| Tool | Version | Why |
|---|---|---|
git |
2.x+ | worktree creation per workspace |
tmux |
3.2+ | display-popup support (canopy popup keybind needs this) |
go |
1.22+ | canopy is built from source on install |
make |
any | drives the install pipeline |
install.sh enforces these — if any are missing, it prints the exact install command for your OS and exits cleanly. Per-platform install lines:
- Arch / CachyOS / Omarchy:
sudo pacman -S git tmux neovim go - Debian / Ubuntu:
sudo apt-get install git tmux neovim golang-go make - macOS:
brew install git tmux neovim go - Windows: canopy needs tmux, which doesn't run natively. Use WSL2 and run the Debian/Ubuntu line inside the Linux shell.
Canopy also expects nvim and claude (Claude Code) for the default tmux-pane layout — but workspaces can run anything, so these aren't checked at install time.
canopy upgradeThat fetches the latest VERSION from main, compares with what you're running, prints the CHANGELOG diff, and runs git pull --ff-only && make install in ~/.canopy/src. Refuses cleanly if you're on a dev binary (canopy use release first) or if ~/.canopy/src is missing/corrupt (re-run install.sh).
Three flags:
canopy upgrade --check— compare versions without upgradingcanopy upgrade --force— rungit pull+make installeven when versions matchcanopy upgrade --dismiss— silence the in-TUI upgrade pill until the next release ships
Canopy also auto-checks once every 6 hours in the background. When a newer release is out, the TUI's top-bar version pill mutates from v0.14.0 to v0.14.0 ⇑ v0.14.1 (yellow arrow), and canopy ls ends with one dim hint line. Press U inside the TUI to read the changelog in a scrollable viewport and run the upgrade without leaving canopy. Press D to dismiss the current available version.
make -C ~/.canopy/src uninstall # remove ~/.local/bin/canopy{,.bin}
rm -rf ~/.canopy # remove the source clone, workspaces, state, and logsThe second line is destructive — it nukes every workspace on disk too. Only run it when you really mean to wipe canopy entirely.
canopy versionOutput looks like:
canopy v0.14.0+abc1234
binary: /home/you/.local/bin/canopy -> canopy.bin
commit: abc1234
built: 2026-04-30T12:34:56Z
mode: release
If you see command not found, ~/.local/bin isn't on your PATH:
export PATH="$HOME/.local/bin:$PATH"Run canopy init from your project root:
cd ~/Work/your-project
canopy initThat drops a canopy.json plus three stub scripts at bin/canopy-{setup,run,archive} for you to fill in. Edit the scripts, commit them, then run canopy new.
If the project already has a conductor.json (Conductor's config — same schema), canopy init detects it and copies the script paths verbatim. Your existing bin/conductor-* scripts keep working; just remember to switch any CONDUCTOR_* env-var references in your scripts and config files to the CANOPY_* equivalents.
{
"scripts": {
"setup": "bin/canopy-setup",
"run": "bin/dev",
"archive": "bin/canopy-archive"
}
}Three script paths. Each script gets the same env vars when canopy invokes it:
| Var | Meaning |
|---|---|
CANOPY_WORKSPACE_PATH |
absolute path to the workspace dir |
CANOPY_ROOT_PATH |
absolute path to the original repo root |
CANOPY_PORT |
allocated TCP port for this workspace (3000-3999) |
setup runs once at workspace creation. run is the long-running server command, launched on demand by canopy run (or <prefix>r inside tmux). archive runs at workspace removal.
Bug reports and PRs welcome — see CONTRIBUTING.md for setup, code conventions, and PR flow.
If you're hacking on canopy itself, you'll have multiple worktrees in flight (one per feature). The active canopy on PATH is a symlink — flip it between the released binary and any in-flight feature build with one command, no rebuild on the way back.
canopy use # show current target + list available
canopy use release # symlink → ~/.local/bin/canopy.bin
canopy use feature-A # symlink → workspace feature-A's ./canopy
canopy use --build feature-A # build feature-A's ./canopy first, then switchmake dev (from inside a worktree) is the muscle-memory wrapper for "build this and make it active"; make release flips back to the released binary without a rebuild. The active binary shows up as a DEV: <workspace> pill in the TUI top bar and a [DEV:<workspace>] suffix in the tmux statusline.
Convention: only run make install from main. From feature branches, make dev is the right tool — it doesn't touch the released canopy.bin, so parallel agents in other worktrees aren't affected.
Full make-task list:
make build # build ./canopy in the worktree (no install, no symlink change)
make install # build with ldflags, install to ~/.local/bin/canopy.bin, symlink canopy → canopy.bin
make dev # build + flip ~/.local/bin/canopy at this worktree's ./canopy
make release # flip ~/.local/bin/canopy back at canopy.bin (no rebuild)
make test # fast unit tests
make test-e2e # full E2E suite (real tmux, scratch repo, slow)
make lint # golangci-lint if installed
make uninstall # remove ~/.local/bin/canopy and canopy.bin
make clean # remove ./canopy in the worktree
User-facing guides:
docs/getting-started.md— 5-minute tour: install, init, first workspacedocs/landscape.md— where canopy fits next to Conductor, tmuxinator, rawgit worktree, and the agent CLIs it hostsdocs/canopy-json.md— schema reference +~/.canopy/config.jsonsettingsdocs/migrate-from-conductor.md— step-by-step for projects withconductor.jsondocs/troubleshooting.md— common problems and fixesCHANGELOG.md— release notes
For contributors:
CONTRIBUTING.md— setup, code conventions, PR flowdocs/architecture.md— codebase layout, dependency direction, where to add thingsdocs/design/v0-canopy.md— design doc with premises, state machine, error conventionsdocs/reviews/v0-test-plan.md— test coverage plan and critical concurrency testsTODOS.md— deferred work, organized by milestone
