Commit hashes work fine for Git and badly for people. A tag fixes that by putting a readable name on one commit, usually a release like v1.0.0, and leaving it there. A branch name moves when new commits arrive. A tag doesn’t.
Teams lean on tags to pin builds, deployments and hotfixes to an exact point in history.
Pro Git lists the tag object as Git’s fourth object type, after blobs, trees and commits, yet only annotated tags create one. A lightweight tag is a plain reference file under refs/tags that holds a commit hash and nothing else.
What Is git tag?

Mark a commit with a name such as v1.0.0 and Git keeps that name on the commit for good. New commits don’t shift it.
Each tag is stored as a reference under refs/tags, so v1.0.0 resolves to the same commit every time. Nobody has to paste a long commit hash into a build script or a bug report.
How Git Stores a Tag
A lightweight tag is just a file in refs/tags holding the hash of the tagged commit. That’s the whole thing.
An annotated tag is heavier. Git writes a tag object with the tagger name, email, date and message, the ref points at that object, and the object points at the commit.
Tag objects aren’t limited to commits either. Pro Git notes that Git’s own source repository tags a blob containing the maintainer’s GPG public key, and the first tag in the Linux kernel repository points at a tree.
git tag vs Branch

You see the difference when a new commit lands. A git branch pointer advances with every commit, while a tag stays on the commit it was created on.
| Behavior | Branch | Tag |
|---|---|---|
| New commit on top | Pointer moves forward | Pointer stays put |
| Lives under | refs/heads | refs/tags |
| Typical job | Ongoing work | Release points and milestones |
Tag to find a release later. Branch to keep working.
Annotated vs Lightweight Tags

Annotated tags for anything you publish, lightweight tags for private bookmarks. The git-tag manual for Git 2.55.0 (June 2026) draws the same line, calling annotated tags the ones meant for releases and lightweight tags the ones for private or temporary labels.
| Property | Lightweight | Annotated |
|---|---|---|
| Stored as | Ref only | Ref plus tag object |
| Carries | Commit hash | Tagger, date, message, optional signature |
| Created with | git tag v1.0.0 | git tag -a v1.0.0 -m "Release 1.0.0" |
| Seen by git describe | Only with –tags | Yes, by default |
| Signable | No | Yes, with -s |
Where the Tag Type Changes Tool Behavior
- git describe skips lightweight tags unless –tags or –all is passed. If one commit carries both kinds, the annotated one wins, and among tags of the same kind the newer date wins. When no tag sits on the commit itself, it walks back through history and picks the nearest one.
- Azure DevOps only creates annotated tags in the web portal, so a lightweight tag means going to the command line (Microsoft Learn documentation).
- An issue filed against jj version 0.34.0 reports that it treats every Git tag as if it were annotated, which makes the tagged commit immutable.
Release scripts that read git describe break quietly on lightweight tags. The KMA genomics project ran into this. git describe reported 1.2.29 plus 11 commits instead of release 1.4.0, because most of its releases carried lightweight tags (Bitbucket issue 60).
I’d default to annotated every time. A lightweight tag saves a few seconds and leaves a release with nothing to audit.
How to Create a git tag

Run git tag -a v1.0.0 -m "Release 1.0.0" on the commit you want to mark. That makes an annotated tag, and dropping -a and -m makes a lightweight one.
- Confirm the target with
git log --oneline -1. Tags land on HEAD unless a commit is named. - Create the tag:
git tag -a v1.0.0 -m "Release 1.0.0"for annotated, orgit tag v1.0.0-lwfor lightweight. - Check the result with
git show v1.0.0. An annotated tag prints the tagger and message before the commit. - Push the tag when it is ready to share. Tags stay local until pushed.
Tagging an Older Commit
Put the commit hash, full or abbreviated, at the end: git tag -a v1.2 9fceb02. Pro Git uses this exact pattern to tag a release that got forgotten after later work landed.
Find the hash first with git log --oneline.
What Goes Wrong

If the name already exists, Git refuses unless you pass -f, and -f replaces the old tag.
Then there’s the surprise annotation. -m or -F implies -a, so git tag -m "note" v1.0.0 still creates an annotated tag (git-tag manual, Git 2.55.0).
And if you create an annotated tag without a message, your editor opens. Annoying the first time, expected after that.
How to List, Inspect and Check Out Tags

git tag with no arguments lists every tag in alphabetical order. Add a pattern with -l to narrow the output.
Listing and Filtering Tags
Git’s own source repository holds more than 500 tags, according to Pro Git, so a pattern keeps the list readable:
git tag -l "v1.8.5*"
The pattern is a shell wildcard, and -l is mandatory whenever you supply one.
--containslists tags that include a given commit--points-atlists tags on a given object--sort=-v:refnamesorts by version, newest first-nprints annotation lines, and falls back to the commit message for lightweight tags--mergedand--no-mergedfilter by what a given commit can reach
Inspecting a Tag
git show v1.0.0 prints the tagger, date and message of an annotated tag, then the commit. A lightweight tag gets the commit alone.
git describe works the other way around. It names a commit after the nearest tag behind it, such as v1.0.4-14-g2414721, which reads as tag v1.0.4, 14 commits on top, abbreviated hash 2414721 (git-describe manual).
Tag names also show up next to commits in git log output when you run git log --decorate.
Checking Out a Tag Without Losing Work
git checkout v2.0.0 shows the files as they were at that tag, and it drops the repository into detached HEAD state. A commit made there leaves the tag unchanged and belongs to no branch, so only its exact hash can reach it.
To keep changes, branch from the tag with git checkout -b version2 v2.0.0.
The new branch moves forward with each commit. The tag stays where it was.
Why git Tags Are Not Pushed by Default

Git treats a tag as a personal label until you publish it, so a plain push skips every tag. The git-tag manual explains the thinking. Someone pulling a one-off change shouldn’t inherit another developer’s private anchor points.
Publishing takes one of a few push forms, and each selects a different set of tags.
| Command | Tags sent | Watch out for |
|---|---|---|
git push origin v1.0.0 | That one tag | Nothing else travels with it |
git push origin --tags | Every tag under refs/tags | Private lightweight tags go too |
git push --follow-tags | Annotated tags reachable from pushed commits | Lightweight tags stay behind |
Making Annotated Tags Travel With Commits
Set push.followTags to true and every git push behaves as if --follow-tags were passed. A single --no-follow-tags overrides it for one push (git-push manual, Git 2.55.0).
Lightweight tags never ride along this way. One more reason to keep releases annotated.
What the Other Side Receives
Pro Git says a clone or pull brings down all of your tags. The git-fetch manual is narrower. By default it only downloads tags that point into the history being fetched, and --tags is needed for the rest.
Both statements hold for a tag on a pushed branch. A tag on a commit outside every fetched branch only arrives with --tags.
To check what the remote repository actually holds, run git ls-remote --tags origin.
How to Delete, Rename or Move a Tag

Delete with git tag -d v1.0.0 locally and git push origin --delete v1.0.0 on the remote. Git has no rename command, and moving a published tag breaks the trust other clones place in its name.
Deleting a Tag
git tag -d v1.0.0 removes the tag from this repository only. To remove it from the remote, use git push origin --delete v1.0.0, or the older form git push origin :refs/tags/v1.0.0.
Teammates keep their copies until each person deletes them. git fetch --prune --prune-tags clears local tags that no longer exist on the remote.
That last command has a catch. Per the git-fetch manual, --prune-tags also deletes local tags you never pushed and force-updates ones that differ, while plain --prune leaves auto-followed tags alone.
Renaming a Tag
- Print the tagged commit with
git rev-list -n 1 v1.0.0-beta. - Create the new name on it:
git tag -a v1.0.0-beta.1 9fceb02 -m "Beta 1". - Push the new tag with
git push origin v1.0.0-beta.1. - Delete the old name locally and on the remote.
The new tag gets its own tagger date and message. Nothing from the old annotation carries over.
Moving a Published Tag
Different sources reach the same verdict from different directions.
On the push side, every update to a ref under refs/tags is rejected unless --force or a leading + is used (Git 2.55.0 manual). Fetch used to be looser. Before Git 2.20, tag updates were accepted without force, and since 2.20 they are rejected, same as on push.
Semantic Versioning 2.0.0 comes at it from the version side. The contents of a released version must not be modified, and any change ships as a new version.
The git-tag manual recommends the same thing: admit the mistake and publish X.1 instead of reusing X.
If you do move the tag, git tag -f plus a forced push keeps the name your docs and build scripts already use. But clones that fetched the old tag keep it, so two people end up holding different commits under one name. Each of them has to run git tag -d and fetch the tag again.
Publishing a new tag instead changes nobody’s copy, and the mistake stays visible in history. The cost is one burned version number.
If the tag was never pushed, git tag -f is safe and none of the above applies.
git tag Naming Conventions
Name release tags vMAJOR.MINOR.PATCH, such as v2.4.1, unless the project releases on a calendar. Git accepts almost any string, so the convention is the only thing keeping tag lists sortable.
Semantic Versioning Tags

Semantic versioning puts compatibility into the number itself. The major number goes up for incompatible API changes, the minor for backward compatible new functionality, and the patch for backward compatible bug fixes.
The Semantic Versioning FAQ states that v1.2.3 is not itself a semantic version. The version is 1.2.3, and v1.2.3 is the tag name, which is exactly how Git uses the prefix.
Versions below 1.0.0 mark initial development, where anything can change. A pre-release suffix such as -rc.1 labels a release candidate and ranks below the final 1.0.0.
git tag --sort=version:refname orders names as versions, and the versionsort.suffix setting adjusts where suffixes like -rc land, per the git-tag manual.
Calendar Versioning Tags
Ubuntu has used a YY.0M.MICRO scheme since release 4.10 in October 2004, according to calver.org.
Twisted uses YY.MM.MICRO and removes deprecated features only after one year and two releases, so the date in the tag also dates the breakage.
| Scheme | The number signals | Example tag |
|---|---|---|
| Semantic Versioning | Compatibility of API changes | v2.4.1 |
| Calendar Versioning | Release date | v24.10.0 |
| Hybrid | Date acts as the major version | v24.10.2.0 |
Calver.org points CalVer at large or constantly changing systems and at time-driven releases such as certificate bundles. A library with dependents belongs on SemVer. An operating system belongs on CalVer.
Characters Git Rejects
A tag name has to pass the rules in git-check-ref-format, so some strings fail before the tag exists.
- Spaces and control characters
- The characters ~ ^ : ? \* [ and the backslash
- Two consecutive dots
- The sequence @{
- A name ending in a dot, or a path component ending in .lock
Hosting platforms add limits of their own. Azure DevOps advises keeping tag names under 250 ASCII characters (Microsoft Learn documentation).
How to Sign and Verify a git tag
Create a signed tag with git tag -s v1.0.0 -m "Release 1.0.0" and check it with git tag -v v1.0.0. The signature proves which key created the tag, so anyone can confirm a release came from the maintainer.
- Confirm a signing key exists with
gpg --list-keys. Generate one withgpg --gen-keyif the list is empty. - Point Git at the key:
git config --global user.signingkey 0A46826A!(the key ID here is Pro Git’s example). - Sign the tag with
-sin place of-a. Use-uto pick a specific key. - Verify with
git tag -v v1.0.0. A good signature prints the signer and key fingerprint.
A signed tag is always an annotated one, because -s creates a tag object to hold the signature. Lightweight tags have nowhere to store it.
Two settings automate the signing. tag.gpgSign signs every tag, and tag.forceSignAnnotated signs annotated ones only. The manual warns that in scripts the first causes many passphrase prompts, so run a GPG agent.
As of the Git 2.55.0 manual, the gpg.format variable selects the signing backend. OpenPGP is the default, and X.509 and SSH are supported.
When Verification Fails
The usual cause is a missing public key. GPG reports that it cannot check the signature because the public key was not found, and Git adds that it could not verify the tag.
Import the signer’s public key into your keyring and run the check again. Every machine that verifies the tag needs that key.
git tag FAQ
What is the difference between a git tag and a GitHub release?
A tag marks a commit inside Git. A GitHub release is a page built on a tag that adds release notes, attached binaries, and zip and tarball links of the source at that tag, according to GitHub Docs.
A tag can exist without any release.
How do I edit the message of an existing annotated tag?
Re-create the tag on its own commit with git tag -f -a v1.0.0 -m "New message" v1.0.0^{commit}. The new tag object carries a fresh tagger date.
If the tag is already published, it needs a forced push and a refetch by everyone, so a new version name is often safer.
Can a lightweight tag be signed?
No. A signature needs a tag object to live in, and a lightweight tag has none. Create an annotated tag with -s instead, adding -f if the lightweight name already exists.
Does deleting a git tag delete the commit?
No. Deleting a tag removes only the name.
The commit stays as long as a branch or another reference reaches it, and Git’s garbage collection removes commits only once nothing references them.
How many tags can point at the same commit?
As many as you like. Each tag is its own reference under refs/tags, so a release candidate and the final release can name one commit.
Automating Release Tagging Without Retagging
A release pipeline should run git tag itself, after the build passes, and never rewrite a tag once it is pushed.
Annotated tags are the only kind that git describe reads by default and that --follow-tags pushes, so a pipeline creating lightweight tags fails on both counts.
The order is simple. Build and test the commit, create an annotated tag from the computed version, then push that single tag by name.
Tagging first forces a retag whenever the build fails, because a published version name then points at broken code.
The downside is that builds running before the tag exists are identified only by commit hash.
This position holds for the Git 2.56.0 manuals listed on git-scm.com in October 2026, and a change in how fetch treats tag updates would reopen it. Wiring the tag into a job is the next step, covered in how GitHub Actions workflows run.
- How to Turn On Dark Mode in Notepad++ (Built-In, No Plugin) - October 3, 2026
- PostgreSQL Cheat Sheet - October 2, 2026
- How to Repair a Corrupt SQL Server Database Without Any Data Loss - October 2, 2026



