Git Tag Points to the Wrong Commit

A release tag landed on the wrong commit and you already pushed it. Move the tag safely, force-fetch it on every clone, and handle the npm/GitHub Release caches that already pinned the old code.

You ran git tag v2.4.0 on the wrong commit — the version-bump commit was not yet on your branch, or you tagged before the CI hotfix landed — and git push --tags already published it to GitHub. Now your package registry built from the wrong code, or a teammate cloned the bad tag.

Fastest fix: find the correct commit SHA, then delete and re-create the tag and force it to the remote:

git tag -d v2.4.0                        # delete locally
git tag -a v2.4.0 <CORRECT_SHA> -m "Release v2.4.0"
git push origin :refs/tags/v2.4.0        # delete remote tag
git push origin refs/tags/v2.4.0         # push corrected tag

The catch: moving a pushed tag is not self-healing. Anyone who already fetched v2.4.0 keeps the old one — git fetch --tags will not overwrite it (more on that below), and any artifact already built from the old tag (npm, a Docker image, a GitHub Release) stays wrong until you rebuild or republish it. The steps below cover all three.

Which bucket are you in?

SymptomMost likely causeJump to
git show v2.4.0 --stat is missing the version-bump diffTagged one commit too earlyCauses #1
Tag sits mid-log, not at the merge into mainTagged the branch tip, not the merge commitCause #2
git cat-file -p v2.4.0 object field is an unexpected SHAWrong SHA pasted into git tag -aCause #3
Tag points at code that later failed CICI tagged before tests finishedCause #4
git log v2.4.0..origin/main shows commits after a fetchTagged from a stale local cloneCause #5
Local and remote v2.4.0 disagreeTag moved on one side onlyCause #6

Common causes

Ordered by hit rate, highest first.

1. Tagged before the final version-bump commit landed

The release process commits a package.json (or pyproject.toml) version bump, but someone ran git tag v2.4.0 one commit too early — before that bump was merged or cherry-picked.

How to spot it: git show v2.4.0 --stat. If the version-bump file is not in the tagged commit’s diff, the tag is one commit early.

2. Tagged on a branch instead of the merge commit

The tag was created on the release/2.4.0 branch tip, not on the merge commit into main. Anyone who clones main checks out a different commit than the tag points to.

How to spot it: git log --oneline --decorate main | head -5. If v2.4.0 appears mid-log rather than at the merge commit, it is on a branch-only commit.

3. Annotated tag created with the wrong SHA

git tag -a v2.4.0 -m "release" <wrong-sha> — the SHA was miscopied from a Slack message or a CI log. The tag object stores a tagger, a timestamp, and an object pointer, and the pointer is wrong.

How to spot it: git cat-file -p v2.4.0 (annotated tags only) prints an object line. Compare that SHA with the commit you meant to ship.

4. CI tagged the commit before tests passed

The tagging step runs in parallel with the test job. Tests happened to pass this time, but had they failed, the tag would point at broken code — and a deploy keyed off the tag would ship it.

How to spot it: open the CI run timeline. If the “create tag” step finished before the “run tests” step, the ordering is unsafe regardless of this run’s result.

5. Tagged from a stale local clone

The developer’s local main was a couple of commits behind the remote. git tag v2.4.0 && git push --tags tagged the out-of-date local HEAD.

How to spot it: git fetch --all, then git log --oneline v2.4.0..origin/main. Any commits listed are ones that should have been in the release but were not.

6. Tag moved on one side only (local vs remote out of sync)

You ran git tag -f v2.4.0 <new-sha> locally but forgot to push, or the remote was updated and your git fetch did not pull the moved tag (the default refuses to clobber — see the FAQ).

How to spot it: compare git rev-parse v2.4.0 (local) with git ls-remote origin refs/tags/v2.4.0 (remote). If the SHAs differ, the two sides disagree.

Step-by-step fix

Step 1: Confirm the correct target commit

git fetch --all
git log --oneline --decorate main | head -10

Note the SHA you meant to tag — call it CORRECT_SHA. For an annotated/merge release, this is usually the merge commit into main.

So you can recover the original pointer if anyone questions the change:

git tag backup/v2.4.0-wrong "$(git rev-parse v2.4.0)"

Step 3: Delete the tag locally

git tag -d v2.4.0

Step 4: Re-create the tag at the correct commit

# Lightweight tag
git tag v2.4.0 <CORRECT_SHA>

# Annotated tag (preferred for releases — records tagger, date, message)
git tag -a v2.4.0 <CORRECT_SHA> -m "Release v2.4.0: add OAuth2 support, fix session expiry"

# Signed annotated tag, if you sign releases
git tag -s v2.4.0 <CORRECT_SHA> -m "Release v2.4.0"

Step 5: Force the corrected tag to the remote

Delete the remote tag first, then push the new one — this is the explicit, reviewable form:

git push origin :refs/tags/v2.4.0     # delete remote tag
git push origin refs/tags/v2.4.0      # push corrected tag

The one-liner equivalent (force-pushes all local tags, so use it only on a clean clone):

git push origin --force --tags

Step 6: Verify the move

git rev-parse v2.4.0~0                              # local resolves to CORRECT_SHA
git ls-remote origin refs/tags/v2.4.0              # remote SHA matches
git show v2.4.0 --stat                             # diff includes the version bump

For an annotated tag, git ls-remote prints two lines for the tag — the tag object and a ^{} line resolving to the commit. The ^{} line is the commit SHA you should compare against CORRECT_SHA.

Step 7: Tell every teammate to force-fetch

This is the step people skip. A plain git fetch --tags will not replace a tag that already exists locally — Git rejects it with ! [rejected] v2.4.0 -> v2.4.0 (would clobber existing tag). Everyone who fetched the bad tag must run:

git fetch origin --tags --force
# or just the one tag:
git fetch origin refs/tags/v2.4.0:refs/tags/v2.4.0 --force

Step 8: Fix anything built from the old tag

  • GitHub Release: a release is bound to a tag object. Editing the tag does not refresh it. Delete the release (Releases > the release > Delete), then recreate it and select the corrected v2.4.0 — or simply edit the existing release and re-select the tag.
  • npm: you cannot reuse a published version number, ever. If v2.4.0 is already on npm, publish a corrected v2.4.1 and deprecate the bad one:
    npm deprecate "your-pkg@2.4.0" "Built from the wrong commit — use 2.4.1+"
    npm version patch && npm publish
    (Within 72 hours and with no dependents you can npm unpublish your-pkg@2.4.0, but you still cannot republish 2.4.0 — the version number is burned. See the FAQ.)
  • Docker / OCI image: the image already exists from the wrong commit. Rebuild from the corrected tag and re-push; retag or delete the bad image digest per your registry’s retention policy.

Prevention

  • Tag in CI, not locally. The pipeline knows the exact SHA after every job has passed; a laptop with a stale main does not.
  • Order CI so tagging runs last — after tests and the build succeed, never in parallel with them.
  • Verify right after tagging: have the release job run git show "$TAG" --stat and fail if the version-bump file is absent.
  • Protect release tags on GitHub. Tag protection rules were sunset on August 30, 2024 and replaced by Rulesets. Go to Settings > Rules > Rulesets > New ruleset > New tag ruleset, set Target tags to a pattern like v*, and enable Restrict creations / Restrict updates / Restrict deletions so only release automation can move them.
  • Add a pre-push hook that blocks pushing a tag whose target is not contained in main’s history.
  • Automate the whole release with semantic-release or Release Please so version bump, tag, and changelog come from one deterministic step instead of a hand-typed SHA.

How to confirm it’s fixed

  1. git ls-remote origin refs/tags/v2.4.0 returns the corrected commit (the ^{} line, for annotated tags).
  2. git show v2.4.0 --stat includes the version-bump diff.
  3. A fresh git clone of the repo checks out the right code at git checkout v2.4.0.
  4. The GitHub Release page shows the corrected tag, and any registry artifact is republished or deprecated.

FAQ

Why didn’t git fetch --tags update my teammates’ tags automatically? By design. Since Git 2.2.0, fetching never clobbers an existing local tag that points elsewhere — you get (would clobber existing tag) and the old tag stays. They must run git fetch --tags --force (or --prune-tags). This is exactly why a moved tag needs an explicit heads-up to the team.

Can I move a tag without a force/delete operation? No. Tags are meant to be immutable, so there is no “update in place” over the wire. Moving one always means deleting the remote ref and pushing a new ref of the same name, which is force-equivalent.

I already published v2.4.0 to npm. Can I just republish 2.4.0 with the right code? No — a used package@version can never be reused, even after unpublishing. Within 72 hours, with no dependents, you can npm unpublish your-pkg@2.4.0; after that you also need fewer than 300 downloads in the last week and a single maintainer, and unpublishing every version locks new publishes for 24 hours. The clean path is publish 2.4.1 and npm deprecate the bad version.

My tag is GPG-signed. Do I need to re-sign after moving it? Yes. The tag object embeds the SHA it points to, so the old signature does not cover the new target. Re-create it with git tag -s v2.4.0 <CORRECT_SHA> -m "...".

A downstream system already built a Docker image from the wrong tag. What now? The image is frozen at the wrong commit. Rebuild from the corrected tag, re-push, and redeploy. Retag or delete the bad image digest in your registry depending on your retention policy — moving the Git tag alone never rebuilds an image.

The new tag points to the right commit locally, but the remote still shows the old SHA. What happened? Step 5 didn’t land. You moved the tag locally but the remote ref is unchanged — re-run git push origin :refs/tags/v2.4.0 then git push origin refs/tags/v2.4.0, and confirm with git ls-remote origin refs/tags/v2.4.0.

Tags: #git #version-control #Troubleshooting