Fix: Git submodule update failed / fatal: not a git repository
Part of: Docker, DevOps & Infrastructure
Quick Answer
Resolve Git submodule update and init failures including 'fatal: not a git repository', path conflicts, URL mismatches, shallow clone issues, and CI/CD checkout problems.
The Submodule State Disagreement
Personally, I think Git submodules are the single most failure-prone subsystem in modern Git workflows. The reason is that the state lives in four separate places (.gitmodules, .git/config, the parent’s tree, and .git/modules/), and almost every “submodule update failed” report is a disagreement between two of them. I learned long ago to read all four before touching any of them. You run git submodule update, git submodule init, or clone a repository that contains submodules and get one of these errors:
fatal: not a git repository: '../.git/modules/libs/some-library'fatal: No url found for submodule path 'libs/some-library' in .gitmodulesClone of 'git@github.com:org/some-library.git' into submodule path '/project/libs/some-library' failed
Failed to clone 'libs/some-library'. Retry scheduledYou may also see variations like:
fatal: repository 'https://github.com/org/some-library.git' not found
fatal: clone of 'https://github.com/org/some-library.git' into submodule path '/project/libs/some-library' failedOr during a recursive update:
Submodule path 'libs/some-library': checked out 'abc1234def5678...'
fatal: git upload-pack: not our ref abc1234def5678
fatal: Fetched in submodule path 'libs/some-library', but it did not contain abc1234def5678. Direct fetching of that commit failed.These errors all point to a broken, misconfigured, or inaccessible Git submodule. The root cause varies, but the fixes below cover every common scenario.
Quick Reference Before You Dive In
If you arrived here from Google with a fresh failure, the five facts that resolve roughly 90 percent of cases:
- Submodule state lives in FOUR places that can disagree.
.gitmodules(committed config),.git/config(local override), the parent’s tree (pinned SHA),.git/modules/<name>(cached clone). Most failures are mismatches between two of them. Thegit submoduledocumentation and thegitsubmodulesoverview are the canonical references. - After
git pullthat changed.gitmodules, ALWAYS rungit submodule sync. The local.git/configkeeps its old URL otherwise;updatewill keep failing on the stale entry. - In CI,
actions/checkout@v4does NOT include submodules by default. Passsubmodules: recursiveand (for private repos) a PAT with the right scopes. - Shallow clones (
--depth 1) and submodules conflict. If the parent pins an old SHA, that SHA may not be in the shallow history. Either deepen the clone or use--shallow-submodules. - The nuclear reset is
git submodule deinit --all -f && rm -rf .git/modules/* && git submodule update --init --recursive. It works on every state-corruption case but wipes 10-20 minutes of re-cloning; trygit submodule syncfirst.
The rest of this article walks through each cause in detail, plus the failure modes most other guides skip.
Why the Four Sources of Truth Disagree
Git submodules are repositories nested inside a parent repository. The parent repo tracks three things for each submodule:
- The URL of the submodule’s remote repository (stored in
.gitmodules). - The path where the submodule lives in the working tree (also in
.gitmodules). - The exact commit SHA the submodule should be checked out at (stored in the parent’s tree object).
When any of these three pieces breaks, the submodule update fails. Common causes include:
- Submodules were never initialized after a fresh clone.
- The URL in
.gitmodulesis wrong (SSH vs. HTTPS mismatch, renamed repo, moved org). - The submodule is in a detached HEAD state and the expected commit no longer exists on the remote.
- Path conflicts where the submodule directory already contains files or another git repo.
- The URL was changed in
.gitmodulesbut the local.git/configstill has the old URL. - Shallow clones that don’t have enough history to check out the pinned submodule commit.
- CI/CD pipelines that don’t enable submodule checkout by default.
Each fix below targets a specific root cause. Start with Fix 1 if you just cloned the repo, or jump to the fix that matches your situation.
Diagnostic Timeline
The first instinct is to retype git submodule update --init --recursive and hope it sticks. That works perhaps a third of the time. The rest of the time you need to read three sources of truth before touching anything.
Minute 0: Read all three sources of truth at once. Run these in order without acting on the output yet:
cat .gitmodules
git config --local --get-regexp '^submodule\.'
git submodule status
ls .git/modules/ 2>/dev/null || echo "no cached modules".gitmodules is what the parent repo committed. .git/config is what your local clone uses. git submodule status shows the current SHA versus the pinned SHA. .git/modules/ is the cache. A submodule failure is almost always a disagreement between two of those four.
Minute 1: Classify the symptom. A leading - in git submodule status means the submodule was never initialized (Fix 1). A leading + means the working tree drifted off the pinned commit (Fix 3). A leading U means a merge conflict on the gitlink. No prefix but a clone failure means URL or auth (Fix 2 or Fix 8).
Minute 2: Diff the URLs explicitly. A surprising number of failures come from a URL that was edited in .gitmodules but never propagated:
diff <(git config --file .gitmodules --get-regexp '\.url$' | sort) \
<(git config --local --get-regexp '^submodule\..*\.url$' | sort)If those two lines differ, you need git submodule sync before any update will work (Fix 5).
Minute 3: Probe the cache. If .git/modules/<name> exists but the working tree is empty, Git will refuse to re-clone into the existing cache. This is the classic fatal: not a git repository: '../.git/modules/...' trigger. Skip ahead to Fix 4.
Minute 4: Check the pinned commit lives on the remote. Before you blame your network, prove the SHA exists upstream:
SUB_URL=$(git config --file .gitmodules submodule.libs/some-library.url)
SUB_SHA=$(git ls-tree HEAD libs/some-library | awk '{print $3}')
git ls-remote "$SUB_URL" | grep "$SUB_SHA" || echo "pinned SHA missing on remote"If the SHA is missing, somebody force-pushed over it or deleted the branch. No amount of --init --recursive will save you; you need to repoint the parent at a SHA that still exists.
Minute 5: Only now reach for the nuclear option. If the timeline above is inconclusive, Fix 7 (deinit + cache wipe + re-add) resolves every remaining state-corruption case. But running it first wastes 20 minutes of re-cloning when a one-line git submodule sync would have fixed it.
When to Use Which Fix
The next eight sections cover the fixes in detail. The table below maps your symptom to the recommended fix.
| Your symptom | Recommended fix | Why |
|---|---|---|
| Just cloned, submodule dirs are empty | Fix 1: git submodule update --init --recursive | Submodules not initialized |
| Clone fails with auth errors | Fix 2: switch SSH / HTTPS, configure key, use insteadOf | Wrong protocol or missing creds |
+ prefix in git submodule status | Fix 3: git submodule update --recursive to reset to pinned SHA | Working tree drifted off pin |
| Submodule directory has leftover files | Fix 4: rm -rf the dir, retry update | Path conflict prevents clone |
URL changed in .gitmodules but update still fails | Fix 5: git submodule sync --recursive first | .git/config has stale URL |
reference is not a tree errors | Fix 6: git fetch --unshallow or use --shallow-submodules | Shallow clone history too short |
| State is too broken to repair incrementally | Fix 7: deinit + cache wipe + re-add | Last resort, always works |
| Failing in GitHub Actions / GitLab CI | Fix 8: enable submodule checkout, configure CI auth | Default CI clones skip submodules |
If multiple rows apply, pick the topmost one.
Fix 1: Initialize Submodules
The most common cause is simply that submodules were never initialized after cloning. A regular git clone downloads the parent repo but leaves submodule directories empty.
Run:
git submodule update --init --recursiveThis does three things:
--initregisters each submodule listed in.gitmodulesinto your local.git/config.updateclones each submodule and checks out the commit pinned by the parent repo.--recursivehandles nested submodules (submodules that themselves contain submodules).
If you are cloning the repository for the first time, you can skip this step entirely by cloning with submodules included:
git clone --recurse-submodules https://github.com/org/project.gitTo make this the default behavior for all future clones, set a global config:
git config --global submodule.recurse trueWith this setting, git clone, git pull, and git checkout will automatically handle submodules.
If git submodule update --init itself fails, the problem is deeper. Move on to the next fixes.
Fix 2: Fix the .gitmodules URL (HTTPS vs. SSH)
Open .gitmodules in the root of your repository:
cat .gitmodulesYou will see something like:
[submodule "libs/some-library"]
path = libs/some-library
url = git@github.com:org/some-library.gitThe url field is where most problems hide. Common issues:
SSH URL but no SSH key configured. If the URL starts with git@github.com:, Git will try to authenticate via SSH. If you don’t have an SSH key set up, it fails. Switch to HTTPS:
git config submodule.libs/some-library.url https://github.com/org/some-library.gitOr edit .gitmodules directly and change the URL:
[submodule "libs/some-library"]
path = libs/some-library
url = https://github.com/org/some-library.gitThen sync the change:
git submodule sync
git submodule update --init --recursiveHTTPS URL but you need SSH. The reverse problem. If your environment uses SSH keys (common in CI/CD), change the URL to the SSH form git@github.com:org/some-library.git.
The repository was renamed or moved. If the upstream repo changed its URL (org rename, repo transfer), update .gitmodules with the new URL and run git submodule sync.
For SSH authentication issues specifically, see the detailed guide at Fix: Git Permission Denied (publickey) which covers key generation, ssh-agent setup, and deploy keys.
A pattern I rely on in mixed dev / CI environments: Git’s insteadOf rewriting. Add it to ~/.gitconfig and SSH URLs in .gitmodules quietly become HTTPS at clone time:
git config --global url."https://github.com/".insteadOf "git@github.com:"This means CI can use HTTPS tokens while devs continue to use SSH locally, with no changes to the committed .gitmodules. The same trick works for self-hosted GitLab and Bitbucket. I add it reflexively on every new machine.
Fix 3: Fix Detached HEAD in Submodules
Submodules are always checked out in a detached HEAD state by default. Git pins them to a specific commit, not a branch. This is by design, but it causes confusion and errors.
Check the submodule status:
git submodule statusOutput looks like:
+abc1234 libs/some-library (v2.1.0)The + prefix means the submodule is checked out at a different commit than what the parent expects. This often happens after someone runs commands inside the submodule directory without updating the parent reference.
To reset the submodule to the commit the parent expects:
git submodule update --recursiveIf you want the submodule to track a branch instead of a fixed commit, configure it in .gitmodules:
[submodule "libs/some-library"]
path = libs/some-library
url = https://github.com/org/some-library.git
branch = mainThen update with remote tracking:
git submodule update --remote --mergeThis fetches the latest commit on the configured branch and merges it into the submodule. You can then commit the updated submodule reference in the parent repo.
If the pinned commit no longer exists on the remote (force-pushed away or the branch was deleted), you will see:
fatal: Fetched in submodule path 'libs/some-library', but it did not contain abc1234In this case, you need to update the parent to point to a valid commit. Enter the submodule, check out a valid branch, then update the parent:
cd libs/some-library
git fetch origin
git checkout main
cd ../..
git add libs/some-library
git commit -m "Update some-library submodule to latest main"For a deeper explanation of how detached HEAD works and how to recover from it, see Fix: Git Detached HEAD.
Fix 4: Fix Submodule Path Conflicts
If the submodule directory already exists and contains files (but isn’t a proper submodule checkout), Git will refuse to overwrite it.
Check if the directory exists and what’s in it:
ls -la libs/some-libraryIf it contains leftover files, remove the directory and try again:
rm -rf libs/some-library
git submodule update --init libs/some-libraryAnother path conflict happens when the submodule path in .gitmodules doesn’t match what Git expects. Verify the path:
git config --file .gitmodules --get-regexp pathIf the path is wrong, edit .gitmodules to fix it and re-sync:
git submodule sync
git submodule update --initA subtler conflict occurs when the .git/modules/<submodule> directory exists from a previous (failed) clone but the working tree directory was removed. Git sees the cached module data and gets confused. Clear it:
rm -rf .git/modules/libs/some-library
rm -rf libs/some-library
git submodule update --init libs/some-libraryThis forces a clean re-clone of the submodule.
If you are seeing fatal: not a git repository errors pointing to a .git/modules/ path, this cached module cleanup is almost always the fix. For more background on this family of errors, see Fix: fatal: not a git repository.
Fix 5: Re-register Submodules After a URL Change (git submodule sync)
When someone updates the URL in .gitmodules and pushes it, your local .git/config still has the old URL. The git submodule update command reads from .git/config, not .gitmodules, so it tries the stale URL and fails.
The fix is git submodule sync, which copies URLs from .gitmodules into .git/config:
git submodule sync --recursive
git submodule update --init --recursiveYou can verify the URLs are correct:
git submodule foreach --recursive 'git remote get-url origin'This prints the remote URL for each submodule. Compare them against the entries in .gitmodules.
If you changed the URL yourself and want to propagate it, the full sequence is:
# 1. Edit .gitmodules with the new URL
# 2. Sync local config
git submodule sync --recursive
# 3. Update submodules
git submodule update --init --recursive
# 4. Commit the .gitmodules change
git add .gitmodules
git commit -m "Update submodule URL"A surprisingly common workflow bug: running git submodule update immediately after pulling a .gitmodules URL change, without git submodule sync in between. The update reads from .git/config, which still holds the old URL, so it fails on a stale entry that you cannot see by looking at .gitmodules. Sync first, update second; that ordering has spared me hours of “but the URL is correct in the committed file!”
Fix 6: Fix Shallow Clone Submodule Issues
Shallow clones (git clone --depth 1) save time and bandwidth, but they cause problems with submodules. A shallow clone only fetches the latest commits, so if the parent repo pins a submodule to an older commit, that commit might not exist in the shallow history.
The error typically looks like:
fatal: reference is not a tree: abc1234def5678
Unable to checkout 'abc1234def5678' in submodule path 'libs/some-library'Fix the parent repo shallow clone:
git fetch --unshallow
git submodule update --init --recursiveFix a shallow submodule clone:
If the submodule itself was cloned shallowly, deepen it:
cd libs/some-library
git fetch --unshallow
cd ../..
git submodule update --recursivePrevent the issue in future clones. If you need shallow clones for speed but also need submodules, use the --shallow-submodules flag to get shallow clones of the submodules themselves:
git clone --recurse-submodules --shallow-submodules https://github.com/org/project.gitThis clones both the parent and submodules shallowly. Each submodule gets the exact commit it needs.
For large repositories on CI servers, combine shallow cloning with specific fetch depth:
git clone --depth 50 --recurse-submodules https://github.com/org/project.gitA depth of 50 is usually enough to include the pinned submodule commits while keeping clone times reasonable.
Fix 7: Remove and Re-add Broken Submodules
When nothing else works, the nuclear option is to fully remove the submodule and re-add it from scratch. This is the most thorough fix because it wipes all local submodule state.
Step 1: Deinit the submodule:
git submodule deinit -f libs/some-libraryThis removes the submodule’s entry from .git/config and cleans its working tree.
Step 2: Remove the submodule from the Git index:
git rm -f libs/some-libraryStep 3: Remove the cached module data:
rm -rf .git/modules/libs/some-libraryStep 4: Commit the removal:
git commit -m "Remove broken submodule libs/some-library"Step 5: Re-add the submodule:
git submodule add https://github.com/org/some-library.git libs/some-library
git commit -m "Re-add libs/some-library submodule"Step 6: Verify it works:
git submodule update --init --recursive
git submodule statusThe output should show a clean status with no + or - prefixes.
If you have multiple broken submodules, you can deinit all of them at once:
git submodule deinit --all -f
rm -rf .git/modules/*Then re-initialize everything:
git submodule update --init --recursiveThis is the equivalent of “turn it off and on again” for Git submodules. It resolves stale caches, URL mismatches, and corrupted module directories in one pass.
If you encounter push errors after re-adding submodules, the parent will reject the push until the gitlink and .gitmodules are both staged together; push them in the same commit.
Fix 8: Fix CI/CD Submodule Checkout
CI/CD pipelines are the most common place submodule failures occur because pipelines don’t check out submodules by default, and authentication is handled differently than on a developer’s machine.
GitHub Actions
By default, the actions/checkout action does not include submodules. Add the submodules parameter:
- uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.GITHUB_TOKEN }}If your submodules are in private repositories, the default GITHUB_TOKEN may not have access. Use a Personal Access Token (PAT) or a GitHub App token instead:
- uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.PAT_TOKEN }}The PAT needs repo scope for private submodule repos.
Alternatively, configure URL rewriting so Git uses the token for HTTPS authentication:
- name: Configure Git for submodules
run: |
git config --global url."https://x-access-token:${{ secrets.PAT_TOKEN }}@github.com/".insteadOf "git@github.com:"
git config --global url."https://x-access-token:${{ secrets.PAT_TOKEN }}@github.com/".insteadOf "https://github.com/"
- uses: actions/checkout@v4
with:
submodules: recursiveGitLab CI
GitLab CI uses a different mechanism. Add the GIT_SUBMODULE_STRATEGY variable:
variables:
GIT_SUBMODULE_STRATEGY: recursive
GIT_SUBMODULE_DEPTH: 1
build:
script:
- echo "Submodules are checked out"For private submodules on GitLab, you need to configure the CI job to use the CI job token. Add this to your .gitmodules:
[submodule "libs/some-library"]
path = libs/some-library
url = ../../org/some-library.gitUsing relative URLs (../../org/some-library.git) instead of absolute URLs lets GitLab resolve them against the project’s own origin, which automatically applies the CI token.
If relative URLs aren’t possible, use a before_script block:
before_script:
- git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/".insteadOf "https://gitlab.com/"
- git submodule sync --recursive
- git submodule update --init --recursiveBitbucket Pipelines
clone:
depth: full
submodules: true
pipelines:
default:
- step:
script:
- echo "Submodules checked out"Common CI/CD Pitfalls
- Shallow clones: Most CI systems use shallow clones by default. If submodule commits aren’t included, increase the fetch depth or use
fetch-depth: 0in GitHub Actions. - SSH vs. HTTPS: CI environments usually prefer HTTPS with tokens over SSH. Make sure
.gitmodulesURLs match what the CI can authenticate against. - Caching: If your CI caches the
.gitdirectory, stale submodule data may persist between runs. Clear the cache or addgit submodule syncbeforegit submodule update.
For general CI/CD debugging, the Fix: GitHub Actions process completed with exit code 1 guide covers workflow failures in more detail.
Stranger Causes I Have Tracked Down
If none of the fixes above resolved your issue, try these less common solutions:
Check file permissions. On Linux and macOS, the .git/modules/ directory and its contents need to be readable by your user. Fix permissions:
chmod -R u+rwX .git/modules/Check disk space. Submodule cloning fails silently when the disk is full. Verify with df -h.
Check your Git version. Submodule behavior has changed significantly across Git versions. Some flags like --recurse-submodules don’t exist in older versions. Check yours:
git --versionGit 2.13+ is recommended for reliable submodule support. Git 2.34+ adds improvements for partial clone submodule support.
Check for nested .git files. Each submodule has a .git file (not directory) in its root that points to .git/modules/<name>. If this file is missing or has wrong content, recreate it:
echo "gitdir: ../../.git/modules/libs/some-library" > libs/some-library/.gitLook at Git LFS interactions. If your submodules use Git LFS, the LFS smudge filter can fail independently of the submodule checkout. Run GIT_LFS_SKIP_SMUDGE=1 git submodule update --init to confirm whether LFS is the actual blocker before debugging further.
Watch for case-only path renames on macOS. Rename Libs/some-library to libs/some-library on a case-insensitive filesystem and Git records the rename but the directory never moves. git submodule status shows the new path, the filesystem still has the old one, and every update fails. Do a two-step rename through a temporary name (libs_tmp) and re-init.
Check for safe.directory rejection in containers. Mounting your repo into a container under a different UID triggers fatal: detected dubious ownership in repository at '/workspace/.git/modules/...'. Allow it explicitly:
git config --global --add safe.directory '*'This is common in Docker-based CI where the runner user differs from the host user.
Inspect .git/modules/<name>/config for a wrong worktree path. When you move a working tree to a new absolute path, the stored worktree line in the cached module config still points at the old path and every update fails with fatal: not a git repository. Open the file, fix the worktree line to a relative path (../../../libs/some-library), and retry.
Check if the remote still exists. Verify that the submodule’s remote URL is reachable:
git ls-remote --exit-code $(git config --file .gitmodules submodule.libs/some-library.url)If this fails, the remote repo has been deleted, made private, or moved. Update the URL in .gitmodules accordingly.
Try a completely fresh clone. When all else fails, clone the repository from scratch with submodules:
git clone --recurse-submodules https://github.com/org/project.git fresh-projectIf the fresh clone works but your existing checkout doesn’t, the issue is corrupted local state. Copy your local changes over to the fresh clone and continue working there.
What Other Tutorials Get Wrong About Submodule Failures
Most Git tutorials list the same fixes but frame them in ways that produce subtle bugs.
They jump straight to git submodule deinit && rm -rf .git/modules. That works but discards 10-20 minutes of caching. Try git submodule sync first; it is a one-line fix for the most common URL-mismatch case.
They omit the .gitmodules vs .git/config distinction. git submodule update reads .git/config, not .gitmodules. Tutorials that show editing .gitmodules without git submodule sync leave readers chasing a fix that does not propagate.
They miss the shallow-clone trap. git clone --depth 1 is recommended widely for fast CI checkouts, but it conflicts with submodules pinned to older commits. The error reference is not a tree is a shallow-history symptom, not a corruption symptom; tutorials that treat it as the latter send readers deinitting cleanly cached repos.
They omit submodule.recurse=true as a global setting. Once set, git clone, git pull, and git checkout all handle submodules automatically. Most “I forgot to update submodules” reports vanish under this one config line. Tutorials that show manual git submodule update commands at every step train fragile habits.
They miss CI / private-repo auth. GitHub Actions’ default GITHUB_TOKEN cannot access submodules in OTHER private repos. Articles that show submodules: recursive without explaining the PAT requirement send readers chasing “auth fails” on green-looking workflows.
They treat git submodule status as decorative. The + / - / U prefix is the single most useful diagnostic in this entire problem space. Tutorials that focus on commands without explaining how to read status leave the actual diagnostic information on the floor.
Frequently Asked Questions
What is the difference between .gitmodules and .git/config?
.gitmodules is a committed file at the root of the parent repo; it is the canonical source of truth for which submodules exist, their paths, and their default URLs. .git/config is per-clone local configuration; it can override URLs (for local mirrors, auth tokens, alternate hosts) without changing what is committed. git submodule update reads .git/config. After any .gitmodules change, run git submodule sync to copy the new URL into .git/config.
When should I use git submodule sync?
After any git pull that includes a .gitmodules change. The pull updates the committed file, but .git/config keeps its old URL until you sync. If submodule updates start failing right after a pull, git submodule sync --recursive is usually the fix.
Why is my submodule always in detached HEAD?
By design. Submodules pin to a specific SHA, not a branch. Detached HEAD reflects that the working tree is at the committed-pinned SHA. To check out a branch inside the submodule, cd in and run git checkout main (or whatever branch). Commit work, return to the parent, and git add submodule_path to bump the parent’s pinned SHA.
Should I use git submodule update --remote?
Use it sparingly. --remote fetches the latest commit on the configured branch and overwrites the pinned SHA. This breaks reproducibility: the parent commit no longer specifies the exact submodule SHA, only “whatever was latest when someone ran the command.” For projects that want this behavior (vendoring docs, themes), it is fine. For libraries with reproducible-build requirements, stick to explicit SHA pins.
Why does actions/checkout@v4 not include submodules by default?
The default is to clone only the parent for speed. Add submodules: recursive to the action’s with block to enable submodule checkout. For private submodules in other repos, you also need a PAT or GitHub App token; the default GITHUB_TOKEN is scoped to the parent repo only.
Is there a way to make submodules work like normal directories?
Not really. If you want a vendored copy that lives in version control without the indirection layer, use git subtree instead of git submodule. Subtree merges the entire history of the dependency into the parent repo, which means no auth juggling, no --init step, and no detached HEAD. The trade-off is repo size and history complexity.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: DVC Not Working — Remote Push Errors, Pipeline DAG Issues, and Git Integration
How to fix DVC errors — dvc push authentication failed, dvc pull file missing, pipeline stage not reproducing, cache out of disk space, dvc add vs dvc stage, conflict with git LFS, and S3/GCS remote setup.
Fix: Git Hooks Not Running — Husky Not Working, pre-commit Skipped, or lint-staged Failing
How to fix Git hooks not executing — Husky v9 setup, hook file permissions, lint-staged configuration, pre-commit Python tool, lefthook, and bypassing hooks in CI.
Fix: Git Keeps Asking for Username and Password
How to fix Git repeatedly prompting for credentials — credential helper not configured, HTTPS vs SSH, expired tokens, macOS keychain issues, and setting up a Personal Access Token.
Fix: Undo git reset --hard and Recover Lost Commits
How to undo git reset --hard and recover lost commits using git reflog — step-by-step recovery for accidentally reset branches, lost work, and dropped stashes.