Annotated vs lightweight tags Jump to heading

Git has two kinds of tag, and they are created by almost the same command. git tag v2.4.0 creates a lightweight tag: a ref pointing directly at a commit, nothing more. git tag -a v2.4.0 -m "Release 2.4.0" creates an annotated tag: a tag object with its own author, date and message, which the ref points to. For a bookmark in your local repository the difference hardly matters. For release tags it matters a great deal: only annotated tags can be signed, git describe ignores lightweight tags by default, --follow-tags pushes only annotated ones, and the tag’s date and author record who released what and when. This page explains the differences, sets defaults that produce the right kind, and shows how to find and replace lightweight release tags, within release tagging and versioning.

When to use this approach Jump to heading

  • You create release tags and are not sure which kind your tooling produces.
  • git describe ignores some of your tags, or git push --follow-tags does not push them.
  • You want to sign release tags, as in signing and verifying release tags.
  • Release automation creates tags, and you need them to be consistent.

Step 1 β€” See what each kind stores Jump to heading

A lightweight tag is a ref whose value is a commit ID. An annotated tag is a ref whose value is the ID of a tag object, which in turn points to the commit and carries metadata.

git tag v2.4.0-light
git tag -a v2.4.0 -m "Release 2.4.0"
git cat-file -t v2.4.0-light       # commit
git cat-file -t v2.4.0             # tag
git cat-file -p v2.4.0
# object 8b3e4f2…
# type commit
# tag v2.4.0
# tagger Priya Raman <[email protected]> 1759395600 +0200
#
# Release 2.4.0
What each tag kind stores and supportsA lightweight tag is just a ref to a commit, with no author, date or message of its own, and cannot be signed. An annotated tag is a full object recording who tagged, when and why, can be signed, and is what describe and follow-tags look for by default.LightweightAnnotatedobjectnone β€” ref to committag objecttagger, date, messagenoyescan be signednoyes (-s)git describe defaultignoredusedpush --follow-tagsnot pushedpushedfor anything called a release, use annotated tags

Step 2 β€” Make annotated (and signed) the default Jump to heading

Configure Git so git tag with a message produces an annotated tag, and signing is automatic.

git config --global tag.gpgSign true              # git tag -a … is signed automatically
git config --global push.followTags true           # git push also pushes reachable annotated tags

There is no setting that turns a bare git tag name into an annotated tag. Teach the -a (or -s) habit and enforce it for release tags in CI or on the server.

Step 3 β€” Check how describe and push treat them Jump to heading

git describe uses only annotated tags unless told otherwise, so version strings derived from it skip lightweight tags. --follow-tags pushes annotated tags reachable from the pushed commits, but not lightweight ones.

git describe                 # v2.4.0-3-g1a2b3c4  (annotated only)
git describe --tags          # also considers lightweight tags
git push --follow-tags       # pushes v2.4.0 but not v2.4.0-light

Version derivation is covered in deriving versions from git describe.

Step 4 β€” Find lightweight release tags in your repository Jump to heading

List tags with their object type, and filter release-pattern tags that are lightweight.

git for-each-ref --format='%(objecttype) %(refname:short)' 'refs/tags/v*' | awk '$1=="commit" {print $2}'
Which kind of tag should this be?A release or any tag users and tooling depend on should be annotated and signed. A temporary local bookmark, such as marking a point before a risky rebase, can be lightweight and should not be pushed. Tags created by automation for releases must be annotated.What is the tag for?a releaseAnnotated + signedgit tag -sautomation-created releaseAnnotated-a with messagelocal bookmarkLightweight okdon't push itif in doubt, annotate β€” there is no downside beyond typing a message

Step 5 β€” Replace a lightweight release tag Jump to heading

If a published release tag is lightweight, replacing it with an annotated tag at the same commit changes the ref’s value from a commit ID to a tag-object ID. The commit is the same, but tools that cached the old value see a change, so treat it as moving a published tag.

commit=$(git rev-parse 'v2.3.0^{commit}')
git tag -d v2.3.0
git tag -s v2.3.0 "$commit" -m "Release 2.3.0 (re-tagged as annotated)"
git push --force origin refs/tags/v2.3.0

⚠️ SAFETY WARNING: Force-pushing a tag changes it for everyone who fetches afterwards, while those who fetched before keep the old one until they explicitly re-fetch tags. For published releases, announce the change, and prefer replacing only tags that are not consumed by package managers or deploy systems. The considerations are in moving or deleting a published tag.

Step 6 β€” Enforce annotated release tags Jump to heading

Reject lightweight release tags at push time with a server hook or a CI check on tag pushes.

# pre-receive / update: release tags must be annotated
case "$ref" in
  refs/tags/v*) [ "$(git cat-file -t "$new")" = tag ] || { echo "release tags must be annotated (git tag -a/-s)"; exit 1; } ;;
esac

Rulesets on hosted forges cannot check tag type directly, so a CI job on push: tags that fails and alerts is the practical equivalent there.

on:
  push:
    tags: ['v*']
jobs:
  tag-type:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - run: |
          t=$(git cat-file -t "refs/tags/$GITHUB_REF_NAME")
          [ "$t" = tag ] || { echo "::error::$GITHUB_REF_NAME is a lightweight tag β€” re-create it with git tag -s"; exit 1; }

The job cannot undo the push, but it fails visibly before any release job consumes the tag, and gives the release manager a clear instruction.

Guarding release tags at push timeA release manager pushes a tag. The server hook or CI job reads the object type of the tag ref. An annotated tag object passes and the release job continues. A lightweight tag, which points directly at a commit, is rejected or flagged before anything publishes it.git push tagv2.4.0Read typegit cat-file -ttag objectrelease continuescommitreject / fail CIthe check is one command β€” cheap enough to run on every tag push

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Do forges show the difference? Jump to heading

Release pages work with both, but only annotated tags show a tagger, date and verification badge. A forge β€œrelease” can add notes to a lightweight tag, but those notes live on the forge, not in Git.

Can an annotated tag point to something other than a commit? Jump to heading

Yes β€” a tree, a blob or another tag. That is rare in practice; release tags should point to commits, and git cat-file -p shows the type of the target.

Which kind does git tag -m create? Jump to heading

Passing -m implies -a, so it creates an annotated tag. Only git tag name with no message options creates a lightweight one.