Git submodules allow a Git repository to include another repository at a specific commit as a subdirectory — a shared WordPress plugin or a theme framework developed in its own repository can be included in multiple client site repositories as a submodule, ensuring every project references the same tested version while keeping the plugin codebase in a single source of truth. Adding a submodule is done with git submodule add https://github.com/org/plugin.git wp-content/plugins/my-plugin, which records the submodule URL and the current commit hash in .gitmodules and the parent repo’s index. Cloning a repository that contains submodules requires git clone --recurse-submodules or, after a plain clone, git submodule update --init --recursive to populate the submodule directories. Updating a submodule to the latest commit of its tracked branch is done with git submodule update --remote --merge wp-content/plugins/my-plugin — this fetches the latest commits from the submodule remote and updates the pointer in the parent repo, which must then be committed. A common pitfall is committing changes inside a submodule directory without first committing them in the submodule repository — the parent repo records a commit hash, so uncommitted changes in the submodule are invisible to other developers cloning the parent. Listing all submodules with their current commit hashes and status uses git submodule status; a + prefix indicates the submodule is checked out at a different commit than the one recorded in the parent repo, and a - prefix means the submodule has not been initialized. For teams that find submodule pointer management error-prone, composer packages (via wpackagist.org or a private Satis registry) are a higher-level alternative that handles versioning, dependency resolution, and updates through a familiar package manager workflow. Submodules are best suited for internal shared code developed alongside the consuming projects; third-party plugins are better managed with Composer. The Git hooks post shows how to add a pre-commit hook that verifies submodule pointers are not left in a detached HEAD state before committing.
Problem: A custom authentication plugin is used in 8 client WordPress sites — each site has its own copy of the plugin source code, so bug fixes must be manually applied to all 8 repositories, and version drift makes it impossible to know which site is running which version.
Solution: Move the plugin to its own repository, add it as a Git submodule in each client site repository, and define a per-project update workflow so all sites can pull the latest fix with git submodule update --remote and a single commit.
# ── Initial setup: add the shared plugin as a submodule ──────────────────────
cd ~/projects/client-site-1
# Add submodule at a specific path inside the project
git submodule add https://github.com/org/my-auth-plugin.git \
wp-content/plugins/my-auth-plugin
# Commit the .gitmodules entry and the submodule pointer
git add .gitmodules wp-content/plugins/my-auth-plugin
git commit -m "chore: add my-auth-plugin as submodule"
# ── Cloning a repo that has submodules ────────────────────────────────────
git clone --recurse-submodules https://github.com/org/client-site-1.git
# Or initialize submodules after a plain clone:
git submodule update --init --recursive
# ── Update submodule to latest commit on its default branch ──────────────
git submodule update --remote --merge wp-content/plugins/my-auth-plugin
# Stage and commit the updated pointer in the parent repo
git add wp-content/plugins/my-auth-plugin
git commit -m "chore(plugin): update my-auth-plugin to latest"
# ── Update all submodules at once ─────────────────────────────────────────
git submodule update --remote --merge
# ── Check submodule status ────────────────────────────────────────────────
git submodule status
# Output:
# +a3f2c1d wp-content/plugins/my-auth-plugin (v2.1.0-3-ga3f2c1d) <- ahead of recorded commit
# d8e4b09 wp-content/plugins/my-auth-plugin (v2.0.1) <- up to date
# - wp-content/plugins/another-module <- not initialized
# ── Pin a submodule to a specific tag (recommended for stability) ─────────
cd wp-content/plugins/my-auth-plugin
git checkout v2.1.0
cd ../..
git add wp-content/plugins/my-auth-plugin
git commit -m "chore(plugin): pin my-auth-plugin to v2.1.0"
# .gitmodules — generated automatically, but useful to understand the format
[submodule "wp-content/plugins/my-auth-plugin"]
path = wp-content/plugins/my-auth-plugin
url = https://github.com/org/my-auth-plugin.git
branch = main # track this branch with --remote updates
# Make git status show submodule changes (recommended)
# Add to .gitconfig or run once per clone:
git config status.submoduleSummary true
git config diff.submodule log
NOTE: Never run git push on the parent repository before pushing commits made inside a submodule — other developers will pull a parent repo that points to a submodule commit that does not exist on the remote, causing git submodule update to fail with a fatal error. Always push the submodule first (cd wp-content/plugins/my-auth-plugin && git push), then push the parent. The git push --recurse-submodules=on-demand flag automates this by pushing submodule changes first if they have unpushed commits.