Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Git Clone a Project With Submodules: The One Flag, the Empty-Folder Symptom, and What CI Needs

Sean

Platform Writer

Sep 14, 2026
7 min read

To clone a project with submodules, add one flag: git clone --recurse-submodules <url>. Without it, Git clones the main repository and leaves every submodule directory empty, because a submodule is a pointer to a commit in another repository, not a copy of its files. If the clone already happened, git submodule update --init --recursive fills the folders in. Build servers and deployment platforms need the same flag or the same command, or they build against empty directories and fail on the first missing file.

Git Clone a Project With Submodules: The One Flag, the Empty-Folder Symptom, and What CI Needs

The top results are the accepted Stack Overflow answer, the relevant chapter of the Git book, and a handful of tutorials repeating both. They cover the command well and the consequences badly. The interesting cases are the empty folder that makes people think the clone failed, the CI job that cannot see a private submodule, and the question nobody asks until it hurts: should this have been a submodule at all.

Table of contents

The symptom: an empty folder

You clone a repository, open the vendor/lib directory the README mentions, and it is empty. git status says the tree is clean. Nothing is broken. A submodule is recorded in the parent repository as two things: an entry in .gitmodules giving the path and the URL, and a special tree entry, a gitlink, holding the exact commit the parent expects. The files themselves live in the other repository, and a plain clone does not fetch them.

$ cat .gitmodules
[submodule "vendor/lib"]
    path = vendor/lib
    url = https://github.com/example/lib.git

$ git submodule status
-a1b2c3d4e5f6... vendor/lib

The leading - in git submodule status means not initialised. That single character is the diagnosis for most submodule confusion.

The one flag

The clone that gets everything in one step:

git clone --recurse-submodules https://github.com/example/project.git

This initialises and checks out every submodule, including submodules inside submodules. Two refinements are worth knowing for large projects. --jobs 8 fetches submodules in parallel, and --shallow-submodules clones each at depth one, which is usually what a build needs and saves a great deal of time when a submodule has a long history:

git clone --recurse-submodules --shallow-submodules --jobs 8 https://github.com/example/project.git

The git shallow clone post covers what depth-one means for the parent repository; the same trade-offs apply to each submodule.

Already cloned? Init and update

If the clone happened without the flag, there is no need to start over:

git submodule update --init --recursive

--init registers the submodules from .gitmodules into your local config, update checks each out at the commit the parent records, and --recursive does the same for nested ones. Run it again any time the parent moves to a commit that points at a different submodule commit, such as after a git pull or a branch switch; Git does not do that automatically unless submodule.recurse is set to true in your config.

Updating submodules later

There are two different updates and conflating them is the second most common submodule mistake. Moving your checkout to what the parent expects is the command above. Moving the parent to a newer commit of the submodule is a change you make and commit:

git submodule update --remote vendor/lib   # fetch and check out the tracked branch's tip
git add vendor/lib
git commit -m "Bump vendor/lib to latest"

The commit records the new pointer. Anyone who pulls it and runs submodule update gets the same version. Skip the git add and the bump exists only on your machine, which is how two developers end up building different code from the same parent commit. The git pull all branches post covers keeping the parent itself current.

What CI and deploy servers need

A build server clones the repository the same way you do, and without the flag it builds against the empty folder. The failure is usually a missing file or an import error two minutes into the build, not a message about submodules. Every CI system has a setting for this: a recursive checkout option in the pipeline configuration, or an explicit git submodule update --init --recursive step before the build.

Private submodules add authentication. The build server has credentials for the parent repository; it needs them for the submodule too. Over HTTPS that means a token with read access to both, and a .gitmodules URL that matches the scheme the token works with. Over SSH it means a deploy key on both repositories, and the URL in .gitmodules written in SSH form. A mismatch between the two forms is the reason a submodule step works on a laptop and fails on the server.

On a deployment platform that builds from a connected repository, read the build log for the submodule path before assuming anything: if the directory is empty at build time, the platform did not recurse, and the reliable fix is to stop depending on it, which is the next section. On RunxBuild the build log is in the dashboard for every deploy, so the empty directory shows up in the first failed build rather than in production. The deploying from GitHub docs cover how the repository connection works.

Should this be a submodule at all

Submodules are the right tool when a project must pin an exact commit of another repository it does not own, and the pin must be visible in history. They are the wrong tool for sharing code between your own projects, which is what most of them are used for. The alternatives are better for that job.

  • A package. Publish the shared code to a registry, private if needed, and depend on a version. The build fetches it like any other dependency and no clone flag is involved.
  • A subtree. git subtree copies the other repository’s history into a directory of this one. Clones are plain clones; the cost is a slightly awkward command for pulling updates.
  • A monorepo. If the pieces always change together, put them in one repository. The monorepo on Netlify post covers the deploy-side consequences of that choice.

A submodule that every developer and every build has to remember is a small tax paid forever. Pay it for vendored third-party code with a real reason to pin, and choose one of the above for everything else.

How this fits the rest of the stack

Clone with --recurse-submodules, repair with submodule update --init --recursive, bump with --remote and commit the pointer, and give the build server the same flag and the same credentials. If the submodule is your own code, consider whether a package or a subtree would remove the flag from everyone’s life. The RunxBuild hosting calculator is where the deployed side of this gets priced, once the build is producing the files it was supposed to. Get the folder to fill, then get the build log to agree.

Useful related references:

FAQ

Why is the submodule folder empty after cloning?

Because a submodule is a pointer to a commit in another repository, and a plain clone fetches only the parent. The directory exists so the pointer has somewhere to live. Run git submodule update —init —recursive to fetch and check out the submodule contents, or clone with —recurse-submodules next time.

What is the difference between —recurse-submodules and —recursive?

For git clone they do the same thing; —recursive is the older spelling and —recurse-submodules the current one. For git submodule update, —recursive means also handle submodules nested inside submodules, and —init means register them first. The safe habit is git submodule update —init —recursive.

How do I clone a submodule at a specific branch?

Set the branch in .gitmodules with git submodule set-branch —branch main vendor/lib, then git submodule update —remote vendor/lib checks out that branch’s tip. Commit the updated pointer in the parent. Without —remote, submodule update always checks out the exact commit the parent records, regardless of branch.

How do I remove a submodule?

Run git submodule deinit -f path, then git rm -f path, then delete the leftover .git/modules/path directory, and commit. This removes the .gitmodules entry, the gitlink and the local checkout. Removing only the directory leaves a broken pointer that fails on the next update.

Do I need —shallow-submodules?

Not for correctness, but it saves time and disk when a submodule has a long history you never need, which is the usual case for vendored code. It clones each submodule at depth one. Combine it with —jobs to fetch submodules in parallel on projects with many of them.

#git clone project with submodules#git recurse submodules#git submodule update init#git submodules ci#.gitmodules