TechEarl

Git Tags: Create, Push, List, and Delete

Create, push, list, and delete git tags from the command line. When to use an annotated tag over a lightweight one, why git push leaves your tags behind, and how to delete a tag on the remote.

Ishan Karunaratne⏱️ 6 min readUpdated
Share thisCopied
Create, push, list, and delete git tags: annotated vs lightweight tags, pushing tags to a remote, and removing a tag locally and on origin.

A git tag is a permanent, human-readable name pinned to one commit, the way you mark v1.2.0 so you can find the exact code you shipped months later. For anything you release, create an annotated tag, push it to the remote on its own, and you are done:

bash
git tag -a v1.2.0 -m "Release 1.2.0"   # create an annotated tag
git push origin v1.2.0                  # push it (git push alone will NOT)

That is the whole job for most people. The rest of this page is the detail behind it: the difference between annotated and lightweight tags, tagging a commit you made last week, listing and inspecting tags, deleting one in both places it lives, and checking one out.

Annotated vs lightweight tags

Git has two kinds of tag, and the choice matters more than it looks.

An annotated tag is a full object in git's database. It stores the tagger's name and email, the date, a message, and can be GPG-signed and verified. It is the right tool for a release, and it is what release tooling and CI expect.

bash
git tag -a v1.2.0 -m "Release 1.2.0"

A lightweight tag is just a pointer to a commit, with no metadata attached. It is fine as a private, throwaway bookmark, but it carries no record of who tagged it or when.

bash
git tag v1.2.0

The rule I follow: annotated for anything anyone else will see, lightweight only for a quick local marker. There is a real functional reason beyond tidiness. Some commands, git describe chief among them, ignore lightweight tags by default, so a release marked with a lightweight tag can quietly fall out of your version strings. If in doubt, use -a.

To sign a tag instead of just annotating it (the tagger identity is then cryptographically verifiable), use -s:

bash
git tag -s v1.2.0 -m "Release 1.2.0"   # GPG-signed
git tag -v v1.2.0                       # verify a signed tag

Tag a past commit

By default a tag points at whatever HEAD is right now. To tag an older commit (you forgot to tag the release, or you are backfilling history), pass the commit SHA as the last argument:

bash
git log --oneline          # find the commit you want
git tag -a v1.0.0 -m "Release 1.0.0" 9fceb02   # tag that specific commit

The same form works with any commit-ish: a branch name, HEAD~3, or a short SHA.

Push tags to the remote

Here is the gotcha that catches almost everyone: git push does not push tags. You can tag a commit, push your branch, and the tag stays sitting on your machine. Tags have to go up explicitly.

Push a single tag (the form I prefer, because it is deliberate):

bash
git push origin v1.2.0

Push every tag you have that the remote does not:

bash
git push origin --tags

There is a middle option worth knowing. --follow-tags pushes your branch and any annotated tags that point at commits in the range being pushed, in one command. It deliberately ignores lightweight tags, which is another reason to annotate your releases:

bash
git push --follow-tags

You can make that the default with git config --global push.followTags true, so a normal git push carries your release tags along without --tags dragging up every stray bookmark.

List and inspect tags

List all tags:

bash
git tag

Filter with a pattern (-l plus a glob), which is how you find one release line among many:

bash
git tag -l "v1.*"

To see what a tag actually points at, plus its message and (for annotated tags) the tagger and date, use git show:

bash
git show v1.2.0

For an annotated tag that prints the tag object header, then the commit it references. For a lightweight tag you just get the commit, which is itself a quick way to tell the two apart.

Delete a tag

A tag lives in two places once you have pushed it: your local repo and the remote. Deleting it locally does not touch the remote, so you usually need both commands.

Delete the local tag:

bash
git tag -d v1.2.0

Delete it on the remote:

bash
git push origin --delete v1.2.0

You may still see the old syntax git push origin :refs/tags/v1.2.0 around. It does the same thing (pushing an empty source ref deletes the destination), but --delete is the readable modern form, so reach for that.

Check out a tag

You can check out the code at a tag to inspect or build it. Because a tag points at a fixed commit rather than a moving branch, this drops you into a detached HEAD state, where new commits belong to no branch and are easy to lose:

bash
git checkout v1.2.0

If you only want to look, that is fine. If you intend to make changes from that point, branch off it first so your work has a home:

bash
git switch -c hotfix-1.2.1 v1.2.0

For more on moving between branches and the modern switch command, see my essential git branch commands reference.

FAQ

Use an annotated tag (git tag -a v1.2.0 -m "...") for anything you release or share. It stores the tagger, date, and a message, can be GPG-signed, and is what release tooling expects. A lightweight tag (git tag v1.2.0) is just a pointer with no metadata, fine only as a private bookmark. Commands like git describe ignore lightweight tags by default.

Because git push does not push tags. Pushing a branch leaves your tags on your machine. Push the tag explicitly with git push origin v1.2.0, or push all of them with git push origin --tags. To push annotated tags along with a branch automatically, use git push --follow-tags.

Run git push origin --delete v1.2.0. Deleting the tag locally with git tag -d v1.2.0 does not remove it from the remote, so for a fully removed tag you run both. The older git push origin :refs/tags/v1.2.0 syntax also works but is less readable.

Pass the commit SHA as the final argument: git tag -a v1.0.0 -m "Release 1.0.0" 9fceb02. Find the SHA with git log --oneline. Any commit-ish works, including a branch name or HEAD~3.

See also

Sources

Authoritative references this article was fact-checked against.

Tagsgitgit taggit push tagsannotated tagdelete git tagversion controlCLI

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Software Systems Architect · Senior Software Engineer · Engineering Leadership

Software systems architect and senior software engineer with more than two decades designing, building, and running production software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Now a CTO, though what I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts

How to create a pull request on GitHub and GitLab: the branch, push, open, review, and merge flow for beginners.

How to Create a Pull Request

A pull request proposes merging one branch into another so a teammate can review it first. Here is the full flow on GitHub and GitLab: branch, push, open, review, merge.

find -delete removes every matched file with no confirmation. The safe -print-first dry-run pattern, depth-first directory deletion, when to use -exec rm vs xargs rm -f, and the BSD vs GNU differences.

How to Find and Delete Files Safely with find -delete

find -delete removes every matched file with no confirmation and no undo. The safe pattern is to write the command with -print first, eyeball the list, then swap -print for -delete. Plus the directory-depth-first trap, when to use -exec rm instead, and the find -delete vs xargs rm -f tradeoff.