Keep every branch checked out. Switching is just cd.
sync-worktrees turns each Git branch, in every repo you declare, into a folder on disk, kept in sync with the remote. A new machine is one config file.
Git history is stored once. Work in progress is never touched; unpushed commits are never discarded.
One config
// sync-worktrees.config.js
/** @satisfies {import("sync-worktrees").SyncWorktreesConfig} */
const config = {
repositories: [
{ name: "frontend", repoUrl: "[email protected]:company/frontend.git", worktreeDir: "./worktrees/frontend" },
{ name: "backend", repoUrl: "[email protected]:company/backend.git", worktreeDir: "./worktrees/backend" },
],
};
export default config; Declare your repos once. Commit the file to a small workspace repo of its own, and every teammate and every new machine rebuilds the same workspace.
The problem
Stash. Switch. Repeat.
Every extra branch is another checkout and another path to remember.
- Stash half-finished work to glance at another branch
- Hunt for where you cloned that sibling repo last month
- Switch branches in five repos for one cross-cutting feature
- Onboard new hires through a brittle copy-paste setup doc
- Clean, fully pushed branches fast-forward themselves on a schedule, with no manual
git fetch - Switching branches becomes
cd - Every repo at a predictable path, declared once in config
- Open the same branch in
frontend/andbackend/at the same time: one folder per branch, at the same path in every repo - New machine: one config, one command, the whole workspace appears
See it in 20 seconds
From git stash to
cd.
The solution
Declare the workspace once. Get every selected branch at a stable path.
One config. It clones what's missing, makes a worktree per selected branch, and keeps each at a predictable path.
Create sync-worktrees.config.js
The config decides which repo branches get a folder.
// sync-worktrees.config.js
/** @satisfies {import("sync-worktrees").SyncWorktreesConfig} */
const config = {
defaults: {
branchInclude: ["main", "feature/*", "release/*"],
branchExclude: ["feature/wip-*"],
},
repositories: [
{
name: "frontend",
repoUrl: "[email protected]:company/frontend.git",
worktreeDir: "./worktrees/frontend",
},
{
name: "backend",
repoUrl: "[email protected]:company/backend.git",
worktreeDir: "./worktrees/backend",
branchMaxAge: "6m",
},
],
};
export default config; Selected branches only
Keep main, feature, and release branches while excluding wip branches or stale branches by age.
Stable paths
Every branch has a folder of its own, at the same path on every machine: the default branch under its own name, the rest named after the branch with a short suffix that keeps similar names apart. cd into one instead of checking out and stashing.
Same layout on every machine
Commit the config to a small workspace repo of its own (a small .gitignore keeps out the .bare/ and worktree folders the tool writes beside it). A new laptop or a new teammate gets the same folders, same names.
Worktree mode
Every remote branch that passes your filters gets its own directory, all sharing one Git database. History is stored once, but each branch still costs a working tree. That is what the filters are for.
Clone mode
When one checkout is enough, get a single branch checked out at a fixed path: no .bare/, no per-branch subfolders. Use it for a dependency sibling that expects a fixed relative path.
Interactive TUI
Ink-based terminal UI with wizards for opening worktrees in editor or terminal, creating branches, and inspecting multi-repo status.
Filtering
Glob include/exclude, age-based pruning, sparse checkout for monorepos, per-repo overrides.
Safety by default
It won't touch work in progress: a worktree with uncommitted changes is never removed or replaced, and a sync leaves it alone instead of updating it. Nothing is merged or rebased for you, and removals are recoverable by default.
Optional MCP server
A second binary, sync-worktrees-mcp, speaks MCP over stdio if a client wants it. The folders are ordinary directories either way.
Quick start
Three commands to a synced workspace.
- 1
Install
Global npm install. Node 24+, Git (and
git-lfsfor LFS repos), macOS or Linux;tmuxonly for the TUI's open-in-terminal wizard.terminalnpm install -g sync-worktrees - 2
Run the init wizard
Writes
sync-worktrees.config.jswhere you run it, with your first repository in it. Add more by editing the file.terminalcd ~/projects/my-sync-dir sync-worktrees init - 3
Launch the TUI, or sync once
Plain command = TUI that syncs now, then hourly.
--runOncefor CI or a cron/systemd timer.terminal# interactive TUI + cron sync-worktrees # one-shot sync-worktrees --runOnce
When not to use it
Skip it for simple Git setups.
It pays off once you work across many branches and repos. Skip it if:
- – One repo, one branch you ever touch
Plain git worktree, or a single clone, is enough.
- – Tiny repos where re-cloning is instant
The disk and bookkeeping savings don't matter.
- – You don't want many persistent branch directories
sync-worktrees keeps a folder per branch on disk by design.
Optional MCP
The folders are real. MCP is extra.
Anything that can cd already works. A second binary, sync-worktrees-mcp, speaks MCP over stdio if a client wants to list, create, or sync worktrees without guessing paths.
Works from any managed worktree; sync and initialize also need your config, found from a parent directory or loaded at runtime.
Use the Claude Code CLI. --scope user makes the server available in every worktree (the default scope is the one directory you ran it from). It finds your config by walking up from the working directory.
claude mcp add --scope user sync-worktrees -- npx -y -p sync-worktrees sync-worktrees-mcp Paste into ~/.cursor/mcp.json (global) or .cursor/mcp.json (per-project). Use the global file if you work across worktrees; a per-project file lives in one checkout.
{
"mcpServers": {
"sync-worktrees": {
"command": "npx",
"args": ["-y", "-p", "sync-worktrees", "sync-worktrees-mcp"]
}
}
} Add to your Windsurf MCP config.
{
"mcpServers": {
"sync-worktrees": {
"command": "npx",
"args": ["-y", "-p", "sync-worktrees", "sync-worktrees-mcp"]
}
}
} Edit claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/), paste the standard config, then restart Claude Desktop. macOS and Linux only; Windows is not supported.
{
"mcpServers": {
"sync-worktrees": {
"command": "npx",
"args": ["-y", "-p", "sync-worktrees", "sync-worktrees-mcp"]
}
}
} Uses VS Code's built-in MCP support (Copilot Chat). Or put the same command and args under the servers key in .vscode/mcp.json (or the user-level mcp.json: MCP: Open User Configuration).
code --add-mcp '{"name":"sync-worktrees","command":"npx","args":["-y","-p","sync-worktrees","sync-worktrees-mcp"]}' Use the Codex CLI, or add the equivalent [mcp_servers.sync-worktrees] block to ~/.codex/config.toml.
codex mcp add sync-worktrees -- npx -y -p sync-worktrees sync-worktrees-mcp Edit cline_mcp_settings.json. See Cline's MCP configuration docs.
{
"mcpServers": {
"sync-worktrees": {
"type": "stdio",
"command": "npx",
"args": ["-y", "-p", "sync-worktrees", "sync-worktrees-mcp"],
"disabled": false
}
}
} Edit ~/.config/opencode/opencode.json.
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"sync-worktrees": {
"type": "local",
"command": ["npx", "-y", "-p", "sync-worktrees", "sync-worktrees-mcp"],
"enabled": true
}
}
} Settings → AI → Manage MCP Servers → + Add. Or run /add-mcp in the prompt.
{
"mcpServers": {
"sync-worktrees": {
"command": "npx",
"args": ["-y", "-p", "sync-worktrees", "sync-worktrees-mcp"]
}
}
} Follow the Gemini CLI MCP install guide and use the standard config.
{
"mcpServers": {
"sync-worktrees": {
"command": "npx",
"args": ["-y", "-p", "sync-worktrees", "sync-worktrees-mcp"]
}
}
}
Open backend's release/next worktree while you sit in frontend/main.
Both are directories. An editor, a shell, or an MCP client can use them. If you do wire up MCP, the tools look like this:
- 1
detect_context: config, repos, worktrees, and what works from here - 2
list_worktrees: status across repos - 3
get_worktree_status: one branch in detail - 4
create_worktree: branch a fix - 5
sync: fetch, create, prune, update
What an agent cannot do through it
- –Delete anything directly.
syncis the only tool that removes a worktree, with the same checks as the CLI, and it is flagged destructive so clients can prompt. - –Move an existing remote branch. Pushes are create-only.
- –Overwrite an existing directory.
- –Create a branch your filters would remove again, unless it passes
force: true.
Setup for every client, what the server sees from where it is launched, every tool, safety and a parallel-agents recipe: docs/mcp.md.
-
detect_contextCall first. Works out which config and repo a path belongs to, lists the worktrees around it, and reports which tools work from there. -
list_worktreesEvery worktree with its status, whether it is safe to remove, and when it last synced, across all repos or one. -
get_worktree_statusPer-worktree detail: dirty files, unpushed commits, stashes, operation in progress. -
create_worktreeWorktree mode only. Check out a branch in its own directory; a new branch is pushed by default, create-only, so an existing remote branch is never moved. -
update_worktreeFast-forward one worktree to match upstream. Worktree mode only. -
syncFull sync cycle: fetch, create, prune, update. Needs a loaded config; flagged destructive so clients can prompt. -
initializeClone the bare repo and create the default-branch worktree. Needs a loaded config. -
load_configLoad or switch the active config at runtime. -
set_current_repositoryPick the active repo when several are configured.
FAQ
Common questions
Is it safe with uncommitted work?
Yes: uncommitted and untracked work is never touched. A clean tree with nothing unpushed can fast-forward. Committed work that has drifted from upstream is set aside, not silently overwritten; force-push or deleted upstream covers that case. If you keep your own folders in the workspace, leave recovery on.
What's the difference between git worktree and sync-worktrees?
git worktree adds one extra folder. This makes every selected branch a folder. Those folders stay in sync from one config.
Why use this instead of cloning each branch separately?
Cloning each branch copies history every time. Here history is stored once, and each branch is only a working tree. This creates, removes, and refreshes those folders for you. You can limit which branches.
What happens when a branch is force-pushed or deleted upstream?
If you have unique commits and upstream has new ones, the folder is set aside and a fresh upstream checkout takes its place. That work is never merged or overwritten, and you can get it back. If you have no unique commits, a clean tree is brought up to date in place. A branch deleted upstream is removed only if the tree is clean; otherwise it stays.
See trash and recovery.
Can I run it continuously or on a cron?
Leave the TUI open and it syncs on a schedule, hourly by default. Headless, put --runOnce on a cron job or a timer. Overlapping runs are skipped.
Does it work with monorepos?
Yes. Include/exclude and sparse checkout if you only want a slice.
See sparse checkout.
Do I need MCP or an AI client?
No. The folders are ordinary directories, so cd already works, and MCP is an optional extra.
See docs/mcp.md.
Every branch, checked out and kept in sync.
Point it at your repos. Stop stashing to look at another branch.