multirepo

Changelog

2026-06-12

multirepo tells you when you’re behind

multirepo now checks for new releases on its own: once a day, in the background, against a published index at multirepo.dev. When a newer version is out, your next command prints a one-line note on stderr pointing at multirepo upgrade. The note stays on stderr, so it never mixes into a command’s output or a pipe.

Nothing updates automatically, and the check is quiet: it runs at most once a day, skips entirely when output isn’t a terminal, and you can turn it off with MULTIREPO_NO_UPDATE_CHECK=1.

multirepo upgrade

multirepo upgrade

Re-runs the installer in place to pull the latest binary and shims. It’s the same install path you’d run by hand, wrapped in a command. Restart your shell (or re-source the plugin) afterward so the shell side matches the new binary.

A nudge when your shell plugin is stale

After an upgrade, the binary is new but your current shell still has the old plugin sourced until you reload it. multirepo now notices: when the sourced plugin is older than the binary, it tells you to restart your shell or re-source the plugin. Plugins carry a version stamped at release; a source checkout’s plugin is unstamped and never triggers the note.

multirepo dev for hacking on multirepo

Working on multirepo itself used to mean eval "$(make dev)". Now, from inside a checkout:

multirepo dev            # build it and use that binary + plugin in this shell
multirepo dev --release  # build the release profile instead
multirepo dev --off      # go back to the installed version

It finds the checkout by walking up from where you are, builds it, and points this shell at the result. A dev session also silences the update and drift notes, so they don’t fire against your local build.

2026-06-09

A smarter cleanup nudge after closing a feature

multirepo feature rm keeps your branches, and used to always tell you to run multirepo prune afterward. Now it only suggests prune when there’s actually something to delete — when the feature/<name> branch’s upstream is gone (the PR merged, the remote branch deleted) in one or more of the feature’s repos.

If you haven’t set fetch.prune, it also points you at git config --global fetch.prune true, which prunes stale origin/* refs on every fetch. That’s what makes a branch read as “gone” — but it only tidies remote-tracking refs. Deleting the leftover local branches is still multirepo prune’s job.

Jump into a feature with feature select

Switching into a feature no longer means remembering its path. Run multirepo feature select <name> and it cds into the feature’s worktree directory and activates it, the same context switch the cd-hook does, now in one command:

multirepo feature select auth

With no name, you get an fzf-style picker over the feature names. Type to filter, ↑/↓ to move, enter to pick:

multirepo feature select

It runs from the parent workspace or from inside another feature, so you can hop straight between sibling features. Passing a name that isn’t a feature fails and lists the ones that are. You can still cd into .multirepo/feature/<name>/ by hand; this is just the shortcut.

It has a short alias, feature sel. To match it, multirepo select now answers to sel too, so the two switch commands read the same way.

Filter repos in the select screen

The combined multirepo select screen now filters like the other pickers: start typing and the active workspace’s repo list narrows to matching names (the query carries across workspace tabs, so you can type a name and / to see it in each). / move, space toggles, and the query box clears on Esc.

Because typing now feeds the filter, the two letter shortcuts moved: drop a repo is Ctrl-d (was d), and repo navigation is the arrow keys (or ^p/^n) rather than j/k. space to toggle and / to switch workspace are unchanged.

2026-06-08

feature rm lists each repo on one line

multirepo feature rm split every repo across two lines — the name, then its state (clean, or what’s blocking removal) indented below. They share one line now, aligned into a column like multirepo status, so a feature’s repos read at a glance:

Feature blah:
 multirepo            clean
 scaffold-cloudflare  clean
 website              clean

Colored --help

multirepo --help (and every subcommand’s help, --version, and clap’s own error messages) now print in color: green section headers, cyan command and flag names. It follows the same rules as the rest of the tool — color on a terminal, off when piped or under NO_COLOR, forced on with FORCE_COLOR/CLICOLOR_FORCE.

2026-06-07

You can now bring a repo you cloned by hand into a workspace, without editing the .code-workspace file yourself.

Adopt an already-cloned repo with select

Cloned a repo into your workspace directory and want multirepo to manage it? Run multirepo select and the untracked git repos in the workspace dir show up as (new) rows — check one and it’s adopted, reading its source from its own origin remote (the same inference workspace create uses). Adopting wires up the folders[] entry and the .claudeignore / .geminiignore files in one step, and re-running is safe.

This replaces hand-editing the workspace file (or re-scaffolding and losing your settings). There’s no standalone add command — adoption lives in select, which is already where you shape the repo set.

The inverse is dropping a repo: press d on its row (or `multirepo select —rm

`). It leaves the workspace config and the derived files, but the clone on disk stays.

A nudge when a repo isn’t tracked

When a git repo is sitting in the workspace directory but isn’t tracked, multirepo sync and multirepo status now print a one-line note pointing you at multirepo select to adopt it.

Disabling a repo now actually hides it in VSCode

Disabling a repo used to write a files.exclude glob into the .code-workspace, but in a multi-root workspace that never hid anything — each repo is its own root, and those globs are matched per-root. Now a disabled repo is simply left out of the workspace’s folders[], which is the list VSCode reads, so it drops out of the Explorer for real. Enabling it adds the folder back.

While here, the folders[] entries got tidier: the inert "enabled" field (which VSCode ignored) is gone, and a "name" is written only when it differs from the directory — so a normal repo is just { "path": "api" }, while a repo cloned into _github from a .github remote stays { "name": ".github", "path": "_github" }. Old "<dir>/**" excludes are cleaned up on the next change.

Custom folder names that stick

Folder display names now live in multirepo.repositories[].name, not just in folders[]. They’re auto-filled when a repo’s real name differs from its directory (the _github.github case), and you can set your own — edit "name" on any repo entry. Because the name is stored on the repo, it survives disabling and re-enabling, even though the folders[] entry is dropped while a repo is hidden.

Commands reorganized: workspace, select, sync --all

Managing the set of known workspaces now lives under one workspace group (short alias ws):

multirepo workspace ls        # was: multirepo ls
multirepo workspace create    # was: multirepo new
multirepo workspace rm        # was: multirepo rm
multirepo ws ls               # the short alias works too

Repo curation now lives in multirepo select (the old config/focus command folded in — see the select section below). multirepo config no longer exists.

sync gained --all: multirepo sync --all reconciles every registered workspace’s repos in place — clone missing, fetch, fast-forward/rebase — without switching you out of your current workspace.

multirepo select with no name is one live screen over every workspace: / switch workspace (the repo list refreshes), / and space toggle its enabled repos. Edits to every workspace you touch are kept; Enter saves them all and drops you into the highlighted one. multirepo select <name> still switches straight there.

Features: work on a subset of repos as git worktrees

Sometimes a piece of work spans a few of your repos but not all of them, and you don’t want it tangled up with your main checkouts. A feature handles that.

multirepo feature create auth api web      # short alias: multirepo feat …

That checks out api and web as their own git worktrees on a shared feature/auth branch under .multirepo/feature/auth/ (each based on the repo’s latest origin default branch), writes an auth.code-workspace you can open in VSCode/Cursor, and drops you in. Name the repos or pick them from a checklist.

A feature is a first-class context: once you’re in it, the everyday commands act on its worktrees, not your main checkouts. multirepo status shows the worktrees’ state and multirepo sync fetches and fast-forwards them. To change which repos are in the feature, run multirepo feature edit <name> from the parent — check one to add its worktree, uncheck one to remove it. Your main checkouts stay where they were.

When the work has landed, tear it down:

multirepo feature rm auth

This won’t throw away work: it checks every worktree first and, if anything is dirty, unpushed, or unmerged (a squash-merged branch counts as merged), lists what and stops — pass --force, or confirm, to override. The worktrees go; the branches stay, to clean up later with multirepo prune.

To see the lay of the land, multirepo workspace ls -a nests each workspace’s repos under a Primary node plus one node per feature. To step into a feature later, cd into its directory (.multirepo/feature/<name>/) — the shell hook activates it, the same way it does for a workspace.

The feature lives in the parent workspace’s .code-workspace (a new features block); the per-feature auth.code-workspace is a generated file you open, never something you hand-edit. As always, every command is safe to re-run.

focus merged into select; new feature edit

focus and select both shaped a workspace, so they’re now one command. multirepo select switches workspace and curates its repos.

The no-arg screen is where repo curation happens. Space toggles a repo’s enabled state, d drops a managed repo from the workspace (the clone on disk stays), and any untracked git repo sitting in the workspace dir shows up as a (new) row — check it to adopt it, with its source read from origin. Edits across every workspace tab are kept; Enter saves them and drops you into the highlighted one.

For scripts, select takes the old focus flags against the active workspace:

multirepo select --enable api,web    # turn repos on
multirepo select --disable infra     # turn one off
multirepo select --set api,web       # set the enabled list exactly
multirepo select --rm old-repo       # drop a repo (clone kept on disk)

Adoption is interactive-only now — check the (new) row instead of --add.

Reconfiguring a feature’s repos moved to its own command, multirepo feature edit <name>, run from the parent workspace. It lists the parent’s repos with the feature’s current members checked: check one to add its worktree, uncheck to remove it through the same safe-removal gate as feature rm. --set/--add/ --rm cover the non-interactive case. Branches are kept.

multirepo focus no longer exists.

feature list, and workspace lsworkspace list

multirepo feature list (alias ls) shows the workspace’s features as a nested list. Each feature gives its shared branch and the directory you cd into; under it, one row per repo shows the worktree ( present, missing — re-create with sync) and the branch that worktree is actually on, flagging any that have drifted off the feature branch. The current feature is marked when you run it from inside one. So at a glance you see every feature, its repos, their worktrees, and their branches.

multirepo workspace ls is now multirepo workspace list, with ls kept as an alias, so the two listing commands read the same: workspace list, feature list. (Tidy-up under the hood: only feature create is eval-wrapped by the shell shim now — feature list/edit/rm are plain commands, so their output is never mistaken for shell code to run.)

status points at your features

status already switches context — inside a feature it reports that feature’s worktrees under a Feature: header. But from your main checkouts there was no sign features even existed. Now multirepo status in a workspace that has features prints a one-line note (2 features here — see multirepo feature list), so the worktree-backed features stay discoverable from the parent. It’s a stderr note, like the untracked-repo nudge, so piped status output stays clean.

A curl | sh install is now complete: binary plus shims

The remote installer used to drop only the binary, so after curl … | sh you still had to track down the shell shim yourself before cd-activation and @repo aliases worked. The release tarball now carries the shims too, and install.sh lays them down next to the binary under ${XDG_DATA_HOME:-~/.local/share}/multirepo.

It then offers to add the source line to your shell rc (zsh, bash, or fish). Say no, or run it where there’s no terminal, and it prints the exact line to add instead. Re-running never duplicates the entry.

New flags: --no-shim for the binary only, --modify-rc / --no-modify-rc to force or skip the rc edit, and --shim-dir to choose where the shims go.

macOS downloads are signed and notarized

The macOS release binaries are now signed with an Apple Developer ID and notarized, so Gatekeeper recognizes them as from a known developer. Installing is unchanged.

2026-06-01

This release adds a way to start a workspace from repos you already have, a hands-on guide, and a clearer picture of the tool for AI assistants.

Start a workspace from repos you already have: new

new builds a .code-workspace from the git repositories already sitting in a directory. Point it at a directory (or none, for the current one) and it writes a workspace file listing every repo it finds. It works out each repo’s source from that repo’s own origin remote: a github.com remote becomes a github source when gh is set up, a gitlab.com remote a gitlab source, and anything else a plain git source with its exact URL. By default it opens a picker with everything checked, so you can drop a repo or two before writing; pass --all, or run it without a terminal, to take all of them. Non-git subdirectories, and git repos with no origin, are skipped with a note. It only writes the file, and won’t overwrite a different one already there.

A hands-on guide

A guide walks through the common tasks in order: starting a workspace from repos you already have, setting one up from a file, moving between repos, and choosing what AI tools see. It’s a single file in the repo rendered as a page on the site.

A clearer picture for AI assistants

A context7.json and an llms.txt at the repo root give AI coding tools an accurate description of multirepo — what each command does and when to reach for new or sync. Assistants that read them, through Context7 or an llms.txt, can drive the tool correctly.

2026-05-31

This release adds working across several workspaces, output options for status, cooperative locking, and safety around shared workspace files.

Work across several workspaces

multirepo keeps a registry of every workspace you’ve set up on this machine. ls lists them, with the current one marked. select opens a filterable picker: type to narrow the list, enter to jump. rm drops a workspace from the registry without touching anything on disk — pass names, or run it with no arguments to pick from a list and confirm. Point MULTIREPO_CONFIG at a file to keep the registry somewhere else, handy for scratch setups that shouldn’t touch your real list.

Two layouts for status

status prints one repo per line with a plain-English summary by default (clean, 3 modified, 2 ahead, 1 to prune). -o table switches to a wider columnar view with separate dirty, ahead, behind, and gone columns. -a/--all runs across every registered workspace. Each run starts with a header naming the workspace. status reads local git state only, so it’s fast and works offline; fetching is sync’s job.

Cooperative locking and the lock command

When sync, prune, or reset work on a repo, they first take a cooperative lock at <repo>/.git/multirepo.lock, so two multirepo runs over the same repo don’t step on each other — the second skips the busy repo and says so. Locks are scoped per worktree, so separate checkouts don’t block one another. lock shows what’s held across the workspace: which repos are free, which are locked, and for each lock whether it’s alive or stale, plus its pid, age, and the operation holding it.

Safety around shared workspace files

A .code-workspace file is meant to be shared, so multirepo reads it as untrusted input. A hostile remote URL can’t slip in as a git option, and a repo entry can’t point outside the workspace directory with .. or an absolute path. Unsafe entries are ignored, with a warning that names the problem.

Disabling a repo hides it everywhere

Toggling a repo in config updates the workspace’s folder list and the ignore files together, so the editor and AI tools agree on what’s hidden.

Context follows the directory

A workspace counts as active only while you’re inside its directory. cd out of the tree and commands stop acting on it; the environment still remembers it, so stepping back in is instant. Removing the workspace you’re in clears its @ aliases and environment on your next command, and a removed workspace stays removed — acting on one that isn’t registered prints a clear message pointing you at sync.

Install through your plugin manager

The shim files sit where Fisher, Oh My Zsh, zinit, and bash-it look, so a plugin manager can install them, or you can source the path by hand. bash has its own entry point that loads the shared shim.

2026-05-30

First release. multirepo manages a set of git repositories as one workspace, described by a single .code-workspace file, with a thin shell layer for navigation.

One binary, one shell shim

multirepo is a single Rust binary. A small shim for zsh, bash, and fish wires it into your shell and runs the shell code the binary prints — the way a tool can change your current directory and aliases from the outside.

Workspaces live in the .code-workspace file

A workspace is any directory with a *.code-workspace file that carries a multirepo block listing its repos. Each repo records where it comes from (git, github, or gitlab), where it lives on disk, and whether it’s enabled. That one file is the source of truth, so there’s no separate config to keep in step.

Set up and stay current with sync

sync sets a workspace up and keeps it current. Given a workspace file it clones every repo and drops you in. With no file it reconciles the workspace you’re in: cloning anything missing, fast-forwarding what’s behind, rebasing what has diverged, and leaving dirty or unpushed repos untouched.

Jump between repos with @ aliases

Activating a workspace defines an @<repo> alias for each repo, so you can hop into any of them by name. cd-ing into a workspace activates it, and leaving it puts your shell back.

Choose what AI tools see with config

config opens a picker to choose which repos are enabled. Enabled state drives .claudeignore, .geminiignore, and the workspace’s files.exclude, so AI tools and the editor see only the repos you pick.

The rest of the commands

status shows git state across every repo at a glance. prune clears local branches whose upstream is gone. reset brings clean repos back to their main branch. ls lists every workspace on the machine, and select switches between them.

Built for many repos at once

Clones, fetches, and status run in parallel, with a live progress row per repo; concurrency is capped automatically and tunable with --jobs. Repo names carry a stable color so the same repo always reads the same way, and color steps aside for NO_COLOR and non-terminal output.

Install and updates

A one-line installer downloads a prebuilt binary for your platform, or builds from source with --local. Tagged releases ship checksum-verified tarballs for macOS and Linux through GitHub Releases.