Moving a Git tag to a newer commit

Moving a Git tag to a newer commit takes a force push, or Git rejects it because the tag already exists. Here's how, plus the gotcha that bites teammates.

You tagged a commit, pushed the tag, and then realized the tag belongs on a newer commit instead. Maybe a fix landed right after you cut the release, or you tagged one commit too early. Moving the tag locally is the easy part. The push is what trips people up: a normal git push gets rejected because the tag already exists on the remote, and Git will not quietly overwrite it for you.

A tag is just a named pointer at a commit. Git treats tags as fixed once they are published, which is usually what you want, so it refuses to update a remote tag on an ordinary push. Moving one is a deliberate two-step operation: repoint the tag locally, then force that change to the remote.

Moving the tag locally

If you are already sitting on the commit you want, git tag -f v1.2.0 re-creates the tag right there. If the target is a different commit, name it explicitly with its SHA:

git tag -f v1.2.0 <commit-sha>

The -f (short for --force) is the part that lets an existing tag be replaced. Without it, Git tells you the tag already exists and stops. If your tag is annotated rather than lightweight, keep it annotated when you move it so you do not drop the message and tagger details:

git tag -f -a v1.2.0 <commit-sha> -m "Release v1.2.0"

Every flag here is described in the git tag documentation, and if you are unsure which kind of tag you are holding, the Pro Git chapter on tagging explains the lightweight versus annotated split.

Force-pushing the tag to the remote

Now push it, and watch the plain command fail. If you run git push origin v1.2.0 after moving the tag, you get:

 ! [rejected]        v1.2.0 -> v1.2.0 (already exists)
error: failed to push some refs to 'github.com:you/repo.git'
hint: Updates were rejected because the tag already exists in the remote.

The remote tag still points at the old commit, and Git will not move it on its own. Force the update:

git push origin v1.2.0 --force

And, done! The remote tag now points at the new commit.

Why a plain git fetch keeps the old tag

Here is the part that trips people up. Moving a tag on the remote does not update anyone else’s copy. A plain git fetch will not overwrite a local tag that already exists, so a teammate who fetched the old tag stays pinned to the old commit and might build from it without realizing anything changed. They have to force it on their end:

git fetch --tags --force

It is the same reason your push needed --force: Git guards existing tags by default on both sides. The push half is described in the git push documentation, and the fetch half in the git fetch documentation, which is explicit that --force is what permits an existing local tag to be updated. So when you move a published tag, give a quick heads-up to anyone working off it, especially if that tag drives a release or a deploy.

Redoing the GitHub release

If a GitHub release is attached to the tag, the release follows the tag once you move it, so editing the existing release and saving is usually enough. You can also delete the release and recreate it against the moved tag. One caveat matters more here than it first looks: with immutable releases enabled, the tag behind a published release is locked to its commit and can be neither moved nor deleted while that release exists, and the release itself only lets you edit its title and notes. That rules out everything above, so the route there is to create the release as a draft, attach the assets, and publish once. GitHub’s guide to managing releases walks through both the web UI and gh release edit.

Git treats tags as immutable on purpose, so moving one is always delete-and-replace with a force on the way out, and a force on the way in for everyone else. If the tag is already public and people depend on it, cutting a fresh version is usually safer than moving the old one, and semantic versioning gives you a cheap way to do that without anyone having to guess what changed. A publish job that reads its version from package.json rather than from the tag name helps too, which is how GitProxy’s npm publish workflow decides whether a build is a release candidate. Save the move for when the tag genuinely has to keep the same name.