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?
| Symptom | Most likely cause | Jump to |
|---|---|---|
git show v2.4.0 --stat is missing the version-bump diff | Tagged one commit too early | Causes #1 |
Tag sits mid-log, not at the merge into main | Tagged the branch tip, not the merge commit | Cause #2 |
git cat-file -p v2.4.0 object field is an unexpected SHA | Wrong SHA pasted into git tag -a | Cause #3 |
| Tag points at code that later failed CI | CI tagged before tests finished | Cause #4 |
git log v2.4.0..origin/main shows commits after a fetch | Tagged from a stale local clone | Cause #5 |
Local and remote v2.4.0 disagree | Tag moved on one side only | Cause #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.
Step 2: Back up the old tag (optional but recommended)
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.0is already on npm, publish a correctedv2.4.1and deprecate the bad one:
(Within 72 hours and with no dependents you cannpm deprecate "your-pkg@2.4.0" "Built from the wrong commit — use 2.4.1+" npm version patch && npm publishnpm unpublish your-pkg@2.4.0, but you still cannot republish2.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
maindoes 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" --statand 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
git ls-remote origin refs/tags/v2.4.0returns the corrected commit (the^{}line, for annotated tags).git show v2.4.0 --statincludes the version-bump diff.- A fresh
git cloneof the repo checks out the right code atgit checkout v2.4.0. - 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.