Projects
Opening projects, cloning repos, configuring worktree behavior, and tracking health with Project Pulse.
Projects
A project in Canopy is a Git repository. Every directory you open needs to be a Git repo — if it isn't, Canopy will walk you through initializing one.
Internally, projects are identified by a SHA-256 hash of the git root path. All project data (settings, recipes) is stored per-project in Canopy's app data directory. When in-repository settings are enabled, a shareable subset of that data is also written to a .canopy/ directory in the repo itself.
Opening a Project
You can open a project a few ways:
- File > Open Directory (Cmd+O / Ctrl+O) to pick a folder from your filesystem
- The project switcher in the toolbar (see below)
- File > Open Recent for previously opened projects
- Clone Repository from the project switcher or File menu to clone a Git repository directly
If the directory you open isn't a Git repo, Canopy walks you through initializing one.
The Project Switcher
The project switcher is how you navigate between projects in Canopy. Click the project name button in the toolbar to open the full dropdown, or use the keyboard shortcut for a quick-switch modal.
Temporal sections
The sidebar dropdown organizes projects into time-based sections so your most relevant work stays near the top. The currently active project always appears first with a subtle background tint. Below that, you'll see:
- Pinned — projects you've pinned via right-click, always visible regardless of when they were last opened
- Today — projects opened today
- This Week — projects opened earlier this week
- Older — everything else
Empty sections are skipped entirely. When you type in the search box, temporal sections collapse into a flat ranked list.
Project rows
Each project in the list shows four elements: a status dot, a project icon (your chosen emoji), the project name, and secondary text. The secondary text shows context about the project's current state:
- "Agent working…" (accent color) when agents are actively running
- "Needs review" (amber) when an agent is waiting for your input
- A relative timestamp (e.g., "2 hours ago") showing when you last opened the project
- The directory name as a fallback
Status dots
The colored dot next to each project gives you a quick read on what's happening:
| Status | Description |
|---|---|
| Pulsing green | Agents are actively working in this project. |
| Amber | An agent is waiting for your review or input. |
| Solid green | Background processes are running, but no agent action needed. |
| Hollow circle | Idle. No running processes. |
The sidebar trigger button itself also shows a badge dot when background projects need attention: pulsing green for active agents, amber for agents waiting on review.
Managing projects
Right-click any project row to open the context menu:
- Open in new window — launches the project in a separate Canopy window
- Pin / Unpin project — keeps the project in the Pinned section at the top
- Copy path — copies the project's directory path to your clipboard
- Stop all agents — terminates all running agent processes (only shown when agents are active)
- Remove project — removes it from the list (does not delete files from disk). For the active project, this becomes Close project, which closes it and returns you to the Welcome Screen.
If a project's directory is missing from disk, the row shows "Directory not found" in amber and the context menu offers Locate folder to point Canopy at the new location.
Keyboard hints
The footer of the dropdown shows modifier-reactive hints that update as you hold keys:
- ↵ Switch to the selected project
- ⌘+↵ Open in a new window
- ⌘+⌫ Remove from list
- Enter Switch to the selected project
- Ctrl+Enter Open in a new window
- Ctrl+Backspace Remove from list
- Enter Switch to the selected project
- Ctrl+Enter Open in a new window
- Ctrl+Backspace Remove from list
Sorting order
Projects are sorted by how often and how recently you've opened them. Canopy uses a frecency algorithm (frequency combined with recency) where the score decays over time with a 5-day half-life. New projects start with a boost so they don't immediately sink below older ones.
The active project is intentionally placed last in the quick-switch modal. This means pressing Cmd+Alt+P followed by Enter always takes you to the project you used most recently before the current one, making two-project toggling fast.
Action buttons
At the bottom of the dropdown palette, four action buttons give you quick access to:
- Project Settings — opens settings for the current project
- Add Project — opens a directory picker to add an existing folder
- Clone Repository — opens the clone dialog (see below)
- Create New Folder — creates a new project directory
Welcome Screen
When no project is currently open, Canopy shows a Welcome Screen instead of the usual workspace. If you've used Canopy before, you'll see your recent projects listed (sorted by how frequently and recently you've opened them), making it quick to jump back in.
New users see a Getting Started checklist that walks through the basics: opening a project, asking an AI agent for help, and starting a parallel task with worktrees. The checklist can be dismissed once you're comfortable.
Quick action buttons on the Welcome Screen let you Open Folder, Create Project, Clone Repository, or Launch Agent without needing to navigate through menus.
Cloning a Repository
If you don't have a repository on your local machine yet, you can clone one directly from within Canopy. This runs git clone under the hood and adds the cloned repo as a project automatically.
There are four ways to open the clone dialog:
- File > Clone Repository...
- The project switcher in the toolbar (or the global project switcher modal) has a Clone Repository... button at the bottom of the palette
- The Welcome Screen shows a Clone Repository button when no project is open
- The Action Palette includes a "Clone Repository" action
The clone dialog has four fields:
- Repository URL — accepts HTTPS URLs (
https://github.com/user/repo.git), SSH URLs (git@github.com:user/repo.git), or GitHub shorthand (owner/repo) - Parent Directory — click Browse to choose where the cloned folder should go
- Folder Name — auto-derived from the URL (strips the
.gitsuffix), but you can change it - Shallow Clone — check this to pass
--depth 1, which is useful for large repositories where you don't need the full history
owner/repo shorthand is a quick way to clone GitHub repositories. Canopy expands it to https://github.com/owner/repo automatically. For repositories hosted elsewhere (GitLab, Bitbucket, self-hosted), use the full HTTPS or SSH URL.Once you click Clone, a progress log streams the output in real time. You can cancel at any point during the clone, and Canopy will clean up any partially cloned files. On success, the dialog closes automatically, the project is added to Canopy, and Canopy switches to it.
Canopy uses your system's Git credentials for authentication. HTTPS clones rely on your credential helper (macOS Keychain, Windows Credential Manager, etc.) and SSH clones use your system SSH agent and keys from ~/.ssh. If authentication fails, the clone stops immediately rather than waiting for input. For help setting up SSH keys, see GitHub's SSH documentation.
Project Status
Each project has one of three states:
| Status | Description |
|---|---|
| Active | The currently visible project in the foreground. |
| Background | Open with running processes, but not displayed. Handy when you want to switch projects without killing terminals. |
| Closed | No running processes. Fully dormant. |
When closing a project, Canopy asks whether to kill terminals (fully close) or keep them running in the background.
Per-Project Settings
Each project stores its own configuration in a settings.json file, including:
- CopyTree settings — include/exclude patterns, format, and limits
- Run command — the dev server command for Dev Preview
- Environment variables — project-scoped env vars (with encrypted storage for secrets)
- Default recipe — which recipe auto-runs when creating a new worktree
To share settings and project identity with your team via version control, see The .canopy/ Directory below.
Worktree Mood
Each worktree has a mood indicator — a quick visual summary of its current state:
| Mood | Condition |
|---|---|
| Stable | Clean working tree with no uncommitted changes and recent activity. |
| Active | Has uncommitted changes — someone (or something) is working. |
| Stale | No changes and the last commit is more than 7 days old. |
| Error | Git problem detected (merge conflict, corrupted state, etc.). |
Worktree Path Patterns
You can configure where worktree directories go in Settings > Worktree Paths. The path pattern uses template variables:
| Variable | Description | Example |
|---|---|---|
{base-folder} | Repository folder name | my-project |
{branch-slug} | Sanitized branch name | feature-login |
{repo-name} | Alias for base-folder | my-project |
{parent-dir} | Parent directory of repo | /Users/you/Projects |
Presets
Three built-in presets are available:
- Subdirectory (default):
{parent-dir}/{base-folder}-worktrees/{branch-slug}— creates a-worktreessibling folder next to your project - Sibling Folder:
{parent-dir}/{base-folder}-{branch-slug}— creates sibling folders with branch suffixes - Flat Sibling:
{parent-dir}/{branch-slug}— creates branch-named sibling folders
If the generated path already exists, Canopy appends numeric suffixes (_2, _3, etc.) to avoid collisions.
GitHub Issue Linking
Worktrees can be linked to GitHub issues. This happens automatically when your branch name includes an issue number (e.g., feature/issue-42-fix-header), or you can attach one manually from the worktree's context menu.
When linked, the issue title becomes the worktree card's primary headline, with the branch name moving to a secondary row below. See GitHub Integration for more details.
Pinning Worktrees
Pin important worktrees to keep them at the top of the sidebar regardless of sort order. Just click the pin icon on any worktree card. Pinned state persists across restarts.
Project Pulse
Project Pulse is an activity heatmap that gives you a personal view of your commit rhythm. It appears on the empty panel grid when no terminals are open for the active worktree, showing your git commit density over time alongside streak tracking and optional GitHub health signals.
Pulse reads directly from your local git log, so your commit history never leaves your machine. The optional GitHub Health Signals row does make API requests when a token is configured (see below), but the heatmap itself is entirely local.
The feature is enabled by default. To turn it off, go to Settings > General and toggle "Show activity heatmap on the empty panel grid". See Settings for more on that toggle.
Activity Heatmap
The heatmap renders your commit history as a grid of color-coded cells, arranged left-to-right from oldest to newest. Each cell represents one day, with four heat levels indicating commit density: empty (no commits), low, medium, high, and full activity.
Three range options are available via the pill toggle in the card header: 60 days (default), 120 days, and 180 days. Switching the range clears cached data and refetches from the git log.
Days before the repository's first commit are filtered out entirely, so the heatmap starts from the actual beginning of the project rather than showing empty cells for dates that predate it. The most recent day with commits gets a subtle accent ring to help you orient quickly. Hovering any cell shows a tooltip with the date and commit count.
You may notice some zero-commit days highlighted with a red tint. These are missed day markers, which appear when a day with no commits is surrounded by activity within a 4-day window on either side. They highlight gaps in otherwise active periods, not days you were simply away from the project.
Coach Line
A single line of motivational text appears below the heatmap, adapting to your current activity state:
| Condition | Message |
|---|---|
| Commits made today | "3 commits today — nice." (actual count) |
| Active streak, no commits yet today | "One small commit today keeps your streak going." |
| Active in last 7 days, no streak | "Momentum's building: 4 active days this week." |
| Fallback | "Make a tiny win: ship one small change today." |
Summary Row
Below the coach line, a stats row shows key metrics for the selected range:
- Commit count in the selected range
- Active days ratio (e.g. "47/60 active days")
- Streak with flame icon and day count (when streak is longer than 1 day)
- Uncommitted files with insertions/deletions count (when there are uncommitted changes)
- Branch delta vs base branch showing ahead/behind commits, files changed, and insertions/deletions (only on non-default branches)
Streak
The streak counter tracks consecutive days with at least one commit. When your streak reaches 2 or more days, a flame icon appears in the summary row alongside the day count. The flame color shifts through tiers as your streak grows:
| Days | Color |
|---|---|
| 1–7 | Amber |
| 8–14 | Light orange |
| 15–29 | Orange |
| 30–59 | Red |
| 60–119 | Darker red |
| 120–239 | Violet |
| 240+ | Accent (legendary) |
At milestone thresholds (30, 60, 120, and 240+ days), a brief animated flame effect plays the first time you see the card that day. The animation respects prefers-reduced-motion and falls back to a static glow.
The streak is a motivational nudge, not a target. It tracks your natural rhythm and is visible only to you. If the gamification aspect isn't your thing, turning off Project Pulse in settings removes the streak display along with the rest of the card.
GitHub Health Signals
When the active project has a GitHub remote configured and a personal access token added in Settings > GitHub, a row of status chips appears on the Pulse card showing live project health data:
- CI status (passing, failing, pending, or no CI configured)
- Open issues count
- Open PRs count
- Latest release tag with relative time
- Security alerts count (Dependabot)
- Merged PR velocity within the selected time range
Each chip links to its corresponding GitHub page. If no GitHub remote is configured, a hint prompts you to connect one. For token setup, see GitHub Integration.
Loading and Error States
While fetching data, the card shows a skeleton with shimmer animation. If the repository has no commits yet, a placeholder message reads "New repository — make your first commit to start tracking activity."
On fetch errors, Pulse retries automatically up to 3 times with exponential backoff. A manual refresh button in the top-right corner of the card lets you re-fetch both the heatmap data and GitHub health signals at any time.
The .canopy/ Directory
Canopy supports an optional .canopy/ directory at the root of your repository — similar to .vscode/ or .devcontainer/ — for storing project configuration alongside your code. When present, Canopy reads these files on project open so any team member who clones the repo gets the same project identity, settings, and recipes automatically. Environment variables and secrets are never written to these files and must be entered locally on each machine.
Enabling In-Repository Settings
To start writing settings into .canopy/, open Project Settings > General (right-click the project name in the sidebar, or use the gear icon in the project toolbar) and scroll to the In-Repository Settings section. Toggling on Store settings in repository expands a confirmation panel listing the files Canopy will create. Click Confirm and enable to proceed.
Once enabled, Canopy writes .canopy/project.json and .canopy/settings.json immediately and keeps them in sync as you change settings. Existing project recipes are synced to .canopy/recipes/. These files are separate from the settings.json Canopy maintains in its app data directory — the in-repo files contain only the shareable subset of your project configuration.
Disabling in-repository settings is non-destructive. Canopy stops writing to .canopy/, but the existing files stay in the repo. A teammate cloning the repo will still have the identity and settings applied on open.
Files Overview
| File | Contains | Safe to commit? |
|---|---|---|
.canopy/project.json | Project name, emoji, and color | Yes |
.canopy/settings.json | Shareable project settings (run commands, worktree path pattern, terminal settings, etc.) | Yes |
.canopy/recipes/ | Per-project recipes as individual JSON files (env var values are redacted; keys preserved). Synced from local recipe storage when in-repo mode is active. | Yes |
.canopy/config.json | Worktree lifecycle scripts (setup and teardown commands) | Yes |
.canopy/*.local.json | Machine-local overrides (reserved for future use) | No — add to .gitignore |
What's Shared vs. Machine-Local
When Canopy writes .canopy/settings.json, it deliberately excludes machine-specific values. The following fields are never written to the in-repo file:
- Environment variables and secrets — project-scoped env vars stay in encrypted local storage only
- Shell executable path — your local shell binary is not shared
- Dev server auto-detection state — a per-machine flag
- Project icon (SVG) — stored locally, not in the repo
- Notification overrides — per-machine preferences
Everything else — run commands, dev server command and timeout, CopyTree settings, excluded paths, worktree path pattern, and terminal settings (shell args, default working directory, scrollback lines) — is written to settings.json and shared.
.gitignore Guidance
The enable confirmation panel shows a snippet you can copy into your .gitignore — Canopy never modifies .gitignore automatically. The snippet has two parts:
- Safe-to-commit files (
project.json,settings.json) — listed as a reference. Do not add these lines to your.gitignoreif you want to commit them to the repo; the whole point of enabling in-repository settings is to track these files in git. - Machine-local files (
*.local.json) — these should always be in.gitignore.
The only line you must add to .gitignore is:
.canopy/*.local.json .canopy/recipes/ is also safe to commit — recipe env var values are redacted before writing, so no secrets are stored in those files.
Worktree Lifecycle Scripts
The .canopy/config.json file lets you define shell commands that run automatically when worktrees are created or deleted. This is useful for running npm install, seeding a database, or tearing down containers as part of your worktree workflow. Lifecycle scripts run automatically for every worktree with no user prompt. If you want to launch specific agents or panels conditionally, use Recipes instead.
Create .canopy/config.json manually in your repo. This file is independent of the in-repository settings toggle:
{"setup": [
"npm install",
"cp .env.example .env"
],
"teardown": [
"docker compose down"
]} Setup commands run asynchronously after a new worktree is created, with the worktree root as the working directory. You can switch to and use the worktree immediately, but tools that depend on setup completing (like npm-installed CLIs) won't be available until the spinner clears. A spinner appears on the worktree card while commands run, showing the name of the active command. Commands run sequentially, and if one fails the rest are skipped. Teardown commands run before git worktree remove. Normal deletion allows up to 120 seconds for teardown to complete; force-delete shortens this to 15 seconds. Teardown failures are logged but never block deletion.
Script output is not visible in the Canopy UI. While running, the worktree card shows the active command name. On failure, you'll see a red "Setup failed" or "Setup timed out" label on the card. To capture output for debugging, redirect it to a log file in your commands:
npm install >> .canopy-setup.log 2>&1 For teardown scripts, log to a path outside the worktree (e.g. $CANOPY_PROJECT_ROOT/.canopy-teardown.log) since the worktree directory is removed after teardown completes.
Script Priority Order
Canopy checks three locations for config.json and uses the first one found:
~/.canopy/projects/<project-id>/config.json— user-level override (machine-local, not in repo)<worktree-path>/.canopy/config.json— worktree-level config<project-root>/.canopy/config.json— main repo config (shared with team)
The user-level location is useful when you need machine-specific setup steps (e.g., activating a local virtualenv) without touching the shared repo config.
Injected Environment Variables
Canopy injects the following variables into every lifecycle script command:
| Variable | Value |
|---|---|
CANOPY_WORKTREE_PATH | Absolute path to the worktree directory |
CANOPY_PROJECT_ROOT | Absolute path to the main repository root |
CANOPY_WORKTREE_NAME | The branch/worktree name |
CI | true |
NONINTERACTIVE | 1 |
GIT_TERMINAL_PROMPT | 0 |
DEBIAN_FRONTEND | noninteractive |
Your project's environment variables are not injected into lifecycle scripts.
Timeouts and PATH
On macOS and Linux, scripts run with a minimal PATH (/usr/local/bin:/usr/bin:/bin). On Windows, the full system PATH is inherited along with PATHEXT, SystemRoot, USERPROFILE, and TEMP/TMP. On all platforms, shell profile files (.bashrc, .zshrc) are not loaded because commands spawn in a non-interactive shell. This means shell aliases and version manager shims (nvm, pyenv, rbenv) are not available. Use absolute paths or install tools in standard locations.
Setup commands share a 120-second timeout for the entire command set. Teardown uses the same 120-second limit for normal deletion, but only 15 seconds when force-deleting a worktree. If the timeout is hit, Canopy terminates the process group (SIGTERM followed by SIGKILL after 5 seconds on macOS/Linux; taskkill /F /T on Windows). Design scripts to complete well within these windows.
Config Validation
If config.json contains invalid JSON or doesn't match the expected schema (only setup and teardown string arrays are recognized), Canopy logs a warning and skips that config file. The next location in the priority chain is tried instead. If all three levels are invalid or missing, no lifecycle scripts run.
When scripts don't execute as expected, check Canopy's log file (Settings > Troubleshooting > Open Log File) for config parse warnings.
New Worktrees Inherit .canopy/
When Canopy creates a new worktree, it copies the .canopy/ directory from the main repo into the new worktree directory. This seeds the worktree with the shared config so lifecycle scripts and identity are available immediately. Existing files in the destination are never overwritten — if a worktree already has a .canopy/config.json, it is preserved as a worktree-level override.
This also means that once a worktree has been created, future changes to the main repo's config.json won't automatically apply to it — the copied worktree-level file takes priority per the order above. Delete the worktree-level .canopy/config.json if you want the worktree to inherit updated repo-level scripts.
For creating, switching, deleting, and comparing worktrees, and for details on the worktree card UI, see Worktrees.