mcp server
sync-worktrees
Auto-sync Git worktrees with remote branches. Includes an MCP server so Claude Code, Cursor & other AI agents can inspect and manage worktrees.
Set up
Runs on your computer. Pick your agent and operating system.
Before you start
- Node.js 20 or newer
{
"mcpServers": {
"sync-worktrees": {
"command": "npx",
"args": [
"-y",
"sync-worktrees"
]
}
}
}same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12
Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs
README
synced Oct 14, 2026sync-worktrees
Keep every branch checked out. Switching is just
cd.
sync-worktrees turns each Git branch, in every repository you declare, into a folder on disk and keeps it in sync with the remote. Git history is stored once per repository; dirty trees are never touched and unpushed commits are never discarded. A new machine is one config file.
Before: git stash && git checkout feature/login, rebuild, and back again later. After:
cd frontend/feature-login, while every other branch stays checked out and built.
Contents: What you get · Why · How it works · What it will never do · Install and quick start · Configuration · Interactive TUI · Optional MCP server · CLI reference · Documentation · Contributing · License
What you get
npm install -g sync-worktrees # Node 24+, macOS or Linux
sync-worktrees init # wizard → writes sync-worktrees.config.js
sync-worktrees # TUI: syncs now, then hourly; add --run-once for a one-shot
With one repository declared and init's default worktreeDir (./<repo>), the directory holding the config becomes:
.
├── sync-worktrees.config.js
├── .bare/
│ └── my-repo/ # Git history, stored once
└── my-repo/ # worktreeDir
├── main/ # the default branch
├── feature-login/ # feature/login
└── feature-2/ # feature-2
Every remote branch that passes your filters is a real checkout you can cd into, build in and open in an editor.
A folder is named after its branch with / turned into -; a short hash is appended only when that name would be
ambiguous (two branches that flatten to the same name, names differing only in case, something already in the way)
(naming rules).
On each sync, a branch that appeared upstream gets a folder, the folder of a branch deleted upstream moves to a
reversible .trash/, and folders that are clean and fully pushed are fast-forwarded. The layout is the same on every
machine that runs the same config.
Why sync-worktrees
One checkout per repo costs you four things:
- Stashing half-finished work just to check out another branch
- Minutes lost hunting for where you cloned a sibling repo
- Switching branches in five repos because one feature spans them all
- Walking a new hire through a day-one cloning checklist
sync-worktrees removes all four. It keeps the whole branch and repo layout you work in on disk: one directory per
branch, kept in sync with the remote. Switching branches becomes cd. Searching across repos becomes grep -r. A
build, an editor or an AI agent reads the same directories you do.
Why not plain git worktree? git worktree add gives you one directory, by hand. This tool is the script you
would write around it: mirroring the remote's branch set, pruning without deleting someone's uncommitted work,
fast-forwarding only clean trees, doing all of that for twelve repositories from one file, on a schedule.
It also bootstraps a dev environment: one config file describes every repo, branch and folder layout your team works in. Commit it, and a fresh laptop builds the whole workspace in one command. See Team workspace.
When not to use it
- 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. A folder per branch on disk is the design, not an option.
How it works
The default, worktree mode, gives every remote branch its own directory while sharing one Git database:
- First run clones the repository once as a bare repository (
.bare/: Git data, no working files). - Each sync fetches, then:
- creates a directory for every remote branch that passes your filters (
main,develop,feature/*, …); - fast-forwards existing directories that are clean and fully pushed;
- moves the directories of branches deleted upstream (or filtered out) to
.trash/, if they are clean.
- creates a directory for every remote branch that passes your filters (
The bare repository is the single Git database every worktree attaches to natively, so branches share history for free
(no --reference, no alternates). It exists so that no branch is a privileged "main" checkout: every branch, the
default included, is a peer directory.
Clone mode (mode: "clone") is a per-repository alternative: a plain git clone of one branch into worktreeDir,
no bare repo, no per-branch subfolders. Use it when a repo must live at a fixed path, such as a dependency sibling or a
single-branch dev clone. See Clone mode.
What it will never do
- Merge, rebase or reset a checkout you are working in: only clean, fully pushed worktrees are fast-forwarded.
- Remove a worktree that is not clean: a gone branch's folder moves to a restorable
.trash/, and only if clean. - Silently overwrite diverged commits: they are pinned in
.trash/before a fresh checkout takes the folder (Diverged branches). - Touch directories outside the paths it manages: only registered worktrees and each branch's own folder path.
- Run your hooks unattended: hooks run only for branches you create in the TUI (Hooks and file copying).
Every removal path, its gate, where it goes and how to undo it, including the exceptions: Trash and recovery.
What it costs
- Disk: a full checkout per branch plus 30 days of
.trash/(200 branches × 300 MB is 60 GB); bound it with filters and sparse checkout. - Network: one
git fetchper repository per tick. - Processes: up to about 40 concurrent git processes by default (Parallelism).
Install and quick start
Requirements:
- Node.js 24 or newer. npm only warns when installing on an older Node; both commands then print a warning at start-up
and carry on, untested. On Node 22,
sync-worktrees@5is the supported line - Git;
git-lfson any machine syncing a repository that uses LFS (or setskipLfs: true) - macOS or Linux. Windows is not supported:
package.jsondeclaresos: ["darwin", "linux"], so npm refuses the install there tmux, only for the TUI's terminal-open wizard- An MCP-capable client, only for the optional
sync-worktrees-mcpserver
The three commands under What you get are the whole setup: sync-worktrees init walks you through one
repository and writes sync-worktrees.config.js in the current directory (.mjs, .cjs and .ts are also accepted);
to add repositories, edit that file and add entries under repositories. See Configuration.
sync-worktrees doctor then checks the whole setup (git, the config, each remote's credentials, the directories)
before the first sync.
sync-worktrees with no arguments opens the interactive TUI, syncs once straight away, then keeps
syncing on the schedule from your config, hourly by default (defaults.cronSchedule). Press q to quit. To start a
branch, press c in the TUI; it creates the folder and pushes. Don't git checkout -b inside a managed folder. See
Team workspace, step 4.
For a one-shot run (CI, scripts, ad-hoc), add --run-once: it syncs every repository once and exits 0 on success, 1 if
any repository failed (see Exit codes). sync-worktrees sets GIT_TERMINAL_PROMPT=0 (unless you exported
it yourself), so credentials must come from a credential helper or ssh-agent. See
Authentication.
Without --config, sync-worktrees, list and trash use $SYNC_WORKTREES_CONFIG when it is set, and otherwise
the nearest sync-worktrees.config.{js,mjs,cjs,ts} in the current directory or a parent, the way git finds .git.
The walk stops at your home directory when it starts inside it. sync-worktrees and list print the file they used.
If the config lives elsewhere, pass it explicitly:
sync-worktrees --config /path/to/sync-worktrees.config.js
sync-worktrees --config /path/to/sync-worktrees.config.js --run-once
sync-worktrees --run-once --filter backend # just the repositories the filter matches
sync-worktrees --dry-run --filter backend # what that sync would do, without doing it
sync-worktrees list --config ./config.js --filter "frontend-*"
--dry-run prints, per repository, the worktrees a sync would create, fast-forward, prune (and why it is safe to),
replace after a divergence, and skip (and why), then exits without changing anything. It does fetch, so the plan
matches origin now: remote-tracking refs move as they would with git fetch. --json prints the plans as JSON. See
Previewing a sync.
Running it unattended
- Laptop. Leave the TUI running in a
tmuxorscreenwindow. A tick the machine slept through is not replayed; the next tick, ors, runs the cycle, andsyncOnStartcovers restarts. - Build box, no terminal. There is no headless daemon: without
--run-oncethe TUI is what runs, and when stdin or stdout is not a terminal (piping throughteeincluded) it refuses to start and exits 1. Putsync-worktrees --run-once --quieton a cron line or a systemd/launchd timer instead;--quietcuts a clean run to its one summary line instead of a few dozen. Cron still mails that line; warnings and errors go to stderr, sosync-worktrees --run-once --quiet >/dev/nullmails only when something needs attention. Two runs that overlap on one checkout do not collide: the second skips that repository and exits 0. - Concurrency. One cross-process lock per checkout; the TUI's ticks,
--run-onceand the MCP server all contend for it, and the loser skips and says so. See Locking.
Configuration
The config is a JavaScript module (init picks ES module or CommonJS for you; .ts works too). Relative paths resolve
from the file's location, process.env is available, and the @satisfies annotation type-checks it in your editor. A
realistic multi-repo file:
// sync-worktrees.config.js
// @ts-check
/** @satisfies {import("sync-worktrees").SyncWorktreesConfig} */
const config = {
defaults: {
cronSchedule: "0 * * * *", // hourly (the default)
branchMaxAge: "30d", // skip branches with no commit in 30 days
branchExclude: ["wip-*", "tmp-*"],
},
repositories: [
{
name: "frontend",
repoUrl: "https://github.com/company/frontend.git",
worktreeDir: "./worktrees/frontend",
cronSchedule: "*/30 * * * *", // per-repo override
},
{
name: "backend",
repoUrl: process.env.BACKEND_REPO_URL || "https://github.com/company/backend.git",
worktreeDir: "./worktrees/backend",
branchMaxAge: "6m",
branchInclude: ["feature/*", "release-*", "main"],
},
],
};
export default config;
Repository settings override defaults; the default branch is always kept regardless of filters. Every setting is
annotated in sync-worktrees.config.example.js and explained in the
documentation.
Team workspace
Onboarding, step by step:
- Keep
sync-worktrees.config.jsin a small workspace repository of its own. Use relativeworktreeDirs (they resolve from the config file) orpath.join(os.homedir(), …), so the file is portable across home directories. Add a.gitignorethat keeps only the config. Three lines do it:*,!.gitignore,!sync-worktrees.config.js. Everything the tool writes lands beside that file:.bare/, the worktree folders (each with its own.trash/),.sync-worktrees-state/and.sync-worktrees-locks/. - Keep credentials out of it: HTTPS through a credential helper, SSH through
ssh-agent(Authentication); a private URL can come fromprocess.env. URLs that do carry a token are redacted in logs and insync-worktrees listoutput. - A new hire runs
git clone <workspace> && cd <workspace> && sync-worktrees --run-once(orsync-worktreesfor the TUI), andsync-worktrees listto confirm what the config resolved to. - Start a branch with
cin the TUI (it creates the folder and pushes), or withgit worktree addplus a push before the next sync. A freshly cut branch that exists only locally has nothing unpushed, so the next tick prunes it to.trash/; once it carries commits it is kept, but warned about on every tick. Push either way. Don'tgit checkout -binside a managed folder: sync tracks one folder per branch by the branch git reports there, so amain/switched to another branch stops beingmain, and nothing recreates it. - What stays manual:
hooks.onBranchCreatedandfilesToCopyOnBranchCreaterun only for branches created from the TUI wizard, so.envfiles andnpm installfor the worktrees the sync creates are still per-worktree steps.
Interactive TUI
sync-worktrees with no arguments opens an Ink-based terminal UI. Its home screen is a table with one row per
repository (state, last result, how long ago it synced, worktrees, dirty/unpushed counts, next run) above the live log,
which l folds to one line and + / - resize. It also has a manual sync trigger, wizards for the common operations,
and a status view across every repository.
| Key | Action |
|---|---|
/ | Jump to any worktree in any repository (fuzzy switcher); Enter opens it, Tab for more actions |
s | Sync all repositories now |
c | Create a branch (wizard: repo, base branch, name) |
o | Open a worktree in a terminal (tmux) or a GUI editor |
w | Worktree status across repos, with flags per worktree |
x | Force clean: purge trash and recovery refs, git gc |
r | Reload the config and re-sync |
? / h | Help |
q | Quit; asks first while a sync or hook is running (Esc only backs out of what is open) |
Every key, the wizards, the status flags, and the terminal/editor launch variables: Interactive TUI.
Optional MCP server
A second binary, sync-worktrees-mcp, speaks the Model Context Protocol over stdio
for clients that want it (Claude Code, Cursor, Claude Desktop, VS Code, Codex, …). The folders are ordinary directories
either way. The standard config, which works in most clients:
{
"mcpServers": {
"sync-worktrees": {
"command": "npx",
"args": ["-y", "-p", "sync-worktrees", "sync-worktrees-mcp"]
}
}
}
Claude Code: claude mcp add --scope user sync-worktrees -- npx -y -p sync-worktrees sync-worktrees-mcp (--scope user
makes it available in every worktree; the default scope is per directory). No config path is needed: the server walks up
from the client's working directory to find your config and the worktree it was launched in. Nine tools:
detect_context, list_worktrees, get_worktree_status, create_worktree, update_worktree, sync, initialize,
load_config, set_current_repository.
What an agent cannot do through it:
- Delete, trash, restore or purge anything directly. The only removal paths are
sync's own: the same prune, stale-directory sweep and diverged replace the CLI runs, with the same gates and destinations (.trash/by default; withtrash.enabled: falsea prune is permanent and a diverged worktree goes to.diverged/instead).syncis 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 prune again, unless it passes
force: true, which the response then warns about.
Setup for every client, what the server sees from where it is launched, the full tool table, safety in detail, and a recipe for parallel agents on parallel branches: MCP server.
CLI reference
| Option | Alias | Description | Default |
|---|---|---|---|
--config | -c | Config file path. When omitted: $SYNC_WORKTREES_CONFIG, else the nearest config in this directory or a parent (up to ~) | - |
--run-once | - | Run a sync once and exit, overriding the config's runOnce for this invocation | false |
--debug | - | Log debug output and full error details (stack, git's whole output), overriding the config's debug | false |
--filter | -f | Only sync repositories whose name matches (wildcards, comma-separated; same matching as list). Exits 1 if nothing matches | - |
--quiet | -q | One-shot runs: print only warnings, errors and the final summary line (the TUI ignores it) | false |
--dry-run | - | Print what a sync would do and exit without changing anything except remote-tracking refs; see Previewing a sync | false |
--json | - | With --dry-run: print the plans as a JSON array | false |
--help | -h | Show help | - |
--version | -V | Print version | - |
Flags are kebab-case; the camelCase spellings from earlier releases (--runOnce, --dropKeepRef, --dropAllKeepRefs)
keep working. sync-worktrees sync is an explicit name for the default command, and a mistyped command or flag gets a
"did you mean" hint.
A failed git command is reported as its one fatal: line; --debug (or debug: true) prints everything. Colour
follows NO_COLOR and FORCE_COLOR, and is stripped from log lines when stdout is not a
terminal.
Subcommands:
-
sync-worktrees init [--config <path>] [--force]runs an interactive wizard that writes a minimal config file (./sync-worktrees.config.jsby default). It refuses to overwrite an existing target unless you pass--force, and it loads the generated file back before reporting success, so a config that would not load fails the command instead of surfacing on the next run. -
sync-worktrees list [--config <path>] [--filter|-f <pattern>] [--json]prints the resolved repositories, with what is on disk for each (registered worktrees and trash entries; for a clone, whether it exists yet), and exits. It only reads (git worktree listand the.trashdirectory) and takes no lock, so it is safe next to a running sync.--jsonprints an array instead, one object per repository:{ "name": "app", "mode": "worktree", "repoUrl": "https://***@github.com/org/app.git", "worktreeDir": "/home/me/code/app", "bareRepoDir": "/home/me/code/.bare/app", "branch": null, "schedule": "0 * * * *", "runOnce": false, "skipLfs": false, "filters": { "branchInclude": null, "branchExclude": ["dependabot/*"], "branchMaxAge": "14d" }, "sparseCheckout": null, "counts": { "worktrees": 4, "trashEntries": 1, "error": null } }Every key is always present.
repoUrlhas credentials removed.bareRepoDirisnullin clone mode andbranchoutside it.sparseCheckoutis{ include, exclude, mode, skipUpdateWhenOutsideSparse }with defaults filled in.counts.worktreescounts registered worktrees whose directory exists (in clone mode, 1 once the clone exists).counts.trashEntriesisnullin clone mode, which has no trash. Both arenullwhen they could not be read, andcounts.errorthen says why. -
sync-worktrees doctor [--config <path>] [--filter|-f <pattern>] [--json] [--quiet]checks the setup without changing anything: Node and git versions, git-lfs, the config file, and for each repository whetherrepoUrlanswers a non-interactivegit ls-remote, whether its directories and lock/state directories are writable, and free disk space. OnePASS/WARN/FAILline per check with a fix hint; see Checking your setup. -
sync-worktrees trashinspects and recovers reversible removals for exactly one worktree-mode repository:sync-worktrees trash [list] [--json] # the default sync-worktrees trash restore <id> [--wait] sync-worktrees trash purge <id> [--wait] # or: purge --all sync-worktrees trash drop-keep-ref <name> sync-worktrees trash drop-all-keep-refsEach takes
--config <path>and--filter|-f <pattern>. The older flag forms (trash --restore <id>,--purge,--drop-keep-ref,--drop-all-keep-refs) still work and print a one-line hint to the subcommand. Listing columns, confirmations and the--jsonshape: Thetrashsubcommand. -
sync-worktrees completionprints a bash/zsh completion script for commands and flags:sync-worktrees completion >> ~/.bashrc # or ~/.zshrc
Exit codes
sync-worktrees --run-onceexits 0 when every repository synced, or was skipped because another process held its lock (whoever holds it is syncing it) or a clone-mode skip applied. It exits 1 when any repository failed to initialize, failed its sync after the retries, recorded a failed action (a rejected sparse pattern, for instance), or could not create its lock file at all. Ctrl-C exits 130 after cleanup.sync-worktreeswithout--run-once, andsync-worktrees init, exit 1 with a one-line explanation when stdin or stdout is not a terminal (systemd, docker, CI,< /dev/null).sync-worktrees listandsync-worktrees --filterexit 1 when--filtermatches nothing or the config does not load;sync-worktrees trashexits 1 on an expected failure (unknown id, occupied destination, a lock another process holds, a declined confirmation) with one❌line.sync-worktrees doctorexits 1 when any check failed; warnings alone exit 0.sync-worktrees --dry-runexits 1 when any repository could not be planned; what the plan contains never changes it.
Documentation
- docs/: one reference page per topic: configuration, clone mode, sparse checkout, trash and recovery, hooks and file copying, the TUI, the MCP server.
sync-worktrees.config.example.js: every setting, annotated.- CHANGELOG.md: what changed in each release.
- sync-worktrees.com: the landing page, the same FAQ, and llms.txt / llms-full.txt for agents.
Contributing
Issues and pull requests are welcome. After pnpm install:
pnpm test:unitruns the unit tests. It needs no build.pnpm test:e2ebuilds the CLI and runs the end-to-end suites againstdist/.pnpm test:e2e:networkadds the cases that clone from GitHub;nightly.ymlruns those daily on Linux and macOS.pnpm testruns both, but does not build: runpnpm buildfirst, or the end-to-end suites fail.
pr.yml checks every PR (lint, format, typecheck, build, smoke test, tests with a coverage
summary on the run page). A PR that touches code needs a changeset (pnpm changeset); changesets cut the release and
write CHANGELOG.md.
License
MIT © Yordan Kanchelov