To import a repository into a monorepo and keep its history, work in a fresh clone of the source repo, use git filter-repo to move every path under a new subdirectory, rename its tags so they do not collide, then merge that rewritten history into the monorepo. Check for secrets and large files before you merge.
What filter-repo actually does
git filter-repo rewrites a repository's history using filters you choose. Its documentation describes it as a destructive operation that writes new commits, trees, tags and blobs, then deletes the original history. It lists the use cases a monorepo migration needs: moving all files into a subdirectory in preparation for merging with another repo, and renaming tags, also often in preparation for merging with another repo.
It also has a safety check. It aborts if you run it from a repo that is not a fresh clone, to prevent losing local history that exists nowhere else. Treat that as a rule for your own process: always work on a throwaway clone, never the only copy.
The steps
- Make a fresh clone of the source repo into a scratch directory. Never run the rewrite in the original.
- Run the analysis mode first. filter-repo can generate reports that help you decide what to filter, such as unusually large blobs.
- Move the files. The to-subdirectory-filter option treats the project root as if it were under the directory you name. For a service called payments, that means every path becomes
services/payments/.... - Rename tags and branches. Two repos that both have a
v1.0.0tag will collide. A tag rename with a service prefix keeps them apart. - Add the rewritten clone as a remote of the monorepo, fetch, and merge with a merge commit that allows unrelated histories.
- Check the result: file counts, a sample of
git logfor a few paths, and a build.
A sketch of step 3, run in the scratch clone:
git filter-repo --to-subdirectory-filter services/payments \
--tag-rename '':'payments-'
The empty string before the colon means "any tag name that starts with nothing", so every tag gets the prefix. Run it on a test clone first and confirm the tag names before you rely on it.
What changes when history is rewritten
Rewritten commits get new hashes. filter-repo rewrites abbreviated hashes that appear inside commit messages so they point at the new commits, and the preserve-commit-hashes option turns that off. Anything outside the repo that references an old hash, such as a ticket or a wiki page, will not match unless you keep a mapping.
Because commits are new objects, assume that signature data attached to the originals does not carry over as a valid signature on the new commits. Check this with your own tooling on a sample commit rather than assuming, and tell reviewers what to expect. If commit signing matters for your audit evidence, keep the archived source repo readable so the original signed history still exists.
Decide what not to import
Full history is not always worth the cost. Options include importing only the last year of history, or importing nothing and linking to the archived repo. Large binaries are the usual reason to filter. filter-repo has an option to strip blobs bigger than a size you set. Secrets are the other reason, and they get their own treatment in do secrets in old repo history follow you into the monorepo.
After the import
Run a dependency inventory so you know what the merged repo actually contains. Our guide to finding every lockfile in a monorepo covers that step. Then rebuild ownership and branch rules, covered in CODEOWNERS paths after moving a repo.
Safeguard's SBOM and dependency scanning can build the inventory of the merged repo, and you can try a free scan at gold.safeguard.sh/scan.
Try it yourself
Click through it at your own pace, or open it full screen.
Click through the inventory and notice how many manifests a merged repo can contain compared with the single one you started with.
Quick check
Do I have to keep full history when I move to a monorepo?
No. You can import a limited window of history or none, and keep the old repo archived and linked. The trade is that blame and bisect across the boundary need the old repo.
Can I run filter-repo on my working clone?
It refuses to run on a repo that is not a fresh clone, by design. Use a separate clone so a mistake never costs you unpushed work.
Sources
Last reviewed: 19 October 2026.