GitLab CI Error Guide: 'fatal: ref HEAD is not a symbolic ref' Detached Checkout
Fix GitLab CI's 'fatal: ref HEAD is not a symbolic ref' caused by detached-HEAD checkouts and shallow clones: read CI_COMMIT_* vars instead of git symbolic-ref.
- #gitlab-cicd
- #troubleshooting
- #errors
- #git
Exact Error Message
A job that tries to read the current branch name from Git fails:
$ BRANCH=$(git symbolic-ref --short HEAD)
fatal: ref HEAD is not a symbolic ref
Cleaning up project directory and file based variables
ERROR: Job failed: exit code 1
You may also see the closely related form when a script runs git branch --show-current and gets an empty string, or git rev-parse --abbrev-ref HEAD returning the literal HEAD:
$ git rev-parse --abbrev-ref HEAD
HEAD
All three symptoms have the same root cause: the working tree is in a detached HEAD state, which is exactly how GitLab Runner checks out your code.
What the Error Means
GitLab Runner does not check out a branch. It checks out the specific commit SHA for the pipeline (CI_COMMIT_SHA) and leaves the repository in a detached HEAD state. In that state HEAD points directly at a commit object, not at a refs/heads/<branch> ref, so git symbolic-ref HEAD has nothing symbolic to resolve and exits with fatal: ref HEAD is not a symbolic ref.
This is correct, intentional runner behaviour — a pipeline is tied to a commit, not a moving branch. The fix is never to “re-attach” HEAD; it is to stop asking Git for the branch and read GitLab’s predefined variables instead.
Common Causes
- A script calls
git symbolic-ref HEAD/git branch --show-currentto discover the branch — but the runner checked out a detached SHA. - Tooling that assumes an attached branch (semantic-release, some release plugins,
git flow) runs unmodified inside CI. - Shallow clone (
GIT_DEPTH) removes the branch ref locally, so evengit rev-parse --abbrev-refcannot recover the name. - Detached pipelines for merge requests, where the checked-out ref is a merged result commit with no branch at all.
- Tag pipelines, where there is genuinely no branch — only
CI_COMMIT_TAG.
How to Reproduce the Error
Any job that derives the branch from Git triggers it:
print-branch:
image: alpine/git
script:
- git symbolic-ref --short HEAD
$ git symbolic-ref --short HEAD
fatal: ref HEAD is not a symbolic ref
ERROR: Job failed: exit code 1
Confirm the detached state directly:
debug-head:
image: alpine/git
script:
- git status | head -1
- git rev-parse --abbrev-ref HEAD
HEAD detached at 3f9c2a1
HEAD
Diagnostic Commands
These are read-only and safe to drop into a temporary debug job:
# Is the working tree detached? (expected in CI)
git status | head -1
# What does GitLab actually checked out?
git rev-parse HEAD
# Show why symbolic-ref fails (note the exit code)
git symbolic-ref HEAD; echo "exit=$?"
# Inspect the refs the shallow clone kept locally
git for-each-ref --format='%(refname)' refs/heads refs/remotes | head
HEAD detached at 3f9c2a1
3f9c2a1d8e...
fatal: ref HEAD is not a symbolic ref
exit=128
The HEAD detached line and exit=128 together confirm the cause: there is no branch ref for Git to resolve.
Step-by-Step Resolution
1. Read the branch from GitLab variables, not Git
GitLab already knows the branch. Use its predefined CI variables:
print-branch:
image: alpine/git
script:
- echo "Branch is $CI_COMMIT_BRANCH"
- echo "Ref name is $CI_COMMIT_REF_NAME"
CI_COMMIT_BRANCHis set on branch pipelines (empty for tags and MR pipelines).CI_COMMIT_REF_NAMEis the branch or tag name — the safest single variable.CI_COMMIT_TAGis set only on tag pipelines.
2. Handle tag and MR pipelines explicitly
script:
- |
if [ -n "$CI_COMMIT_TAG" ]; then
REF="$CI_COMMIT_TAG"
else
REF="$CI_COMMIT_REF_NAME"
fi
- echo "Building ref $REF"
3. If a third-party tool insists on an attached branch
Some tools genuinely require HEAD to point at a branch. Re-attach it just-in-time from the CI variable:
script:
- git checkout -B "$CI_COMMIT_REF_NAME"
checkout -B creates or resets a local branch at the current commit and attaches HEAD to it, satisfying tools like semantic-release without changing what you build.
4. Disable shallow clone if you need full history
Tools that walk history (changelog generators) need more than the default depth:
variables:
GIT_DEPTH: 0 # full clone
GIT_DEPTH: 0 fetches the complete history; a small positive value keeps clones fast while still providing recent commits.
5. Re-run and confirm
After switching to CI variables, the job prints the branch with no fatal: line and exits 0.
Prevention and Best Practices
- Treat detached HEAD as normal in CI and never call
git symbolic-ref HEADorgit branch --show-currentin pipeline scripts. - Standardise on
CI_COMMIT_REF_NAMEfor “the thing being built” and branch onCI_COMMIT_TAGwhen you need tag-specific logic. - For release tooling, add a single
git checkout -B "$CI_COMMIT_REF_NAME"step inbefore_scriptrather than patching every job. - Set
GIT_DEPTHdeliberately: shallow for speed,0when a tool needs history. - A quick way to spot these branch-detection bugs across a
.gitlab-ci.ymlis to paste a failing log into the free incident assistant, which flags detached-HEAD assumptions. More patterns are in the GitLab CI/CD guides.
Related Errors
fatal: reference is not a tree— a shallow clone is missing the commit a tool asks for; raiseGIT_DEPTHor set it to0.You are not currently on a branch— the same detached-HEAD condition surfacing from a different Git subcommand.fatal: HEAD does not point to a branch— emitted by tools like git-flow for identical reasons; fix withgit checkout -B.
Frequently Asked Questions
Why is HEAD detached in every CI job?
GitLab Runner checks out the exact commit SHA the pipeline was created for (CI_COMMIT_SHA), not a branch. Pinning to a commit guarantees reproducibility, and a detached HEAD is the natural result. It is expected, not a bug.
Which variable should I use to get the branch name?
CI_COMMIT_REF_NAME works for both branches and tags. If you specifically need a branch and want it empty on tag pipelines, use CI_COMMIT_BRANCH. Avoid deriving the name from Git inside the job.
My release tool calls git symbolic-ref internally and I can’t change it. What now?
Add git checkout -B "$CI_COMMIT_REF_NAME" before invoking the tool. That attaches HEAD to a local branch at the current commit, so the tool’s internal symbolic-ref call succeeds without altering what you build or deploy.
Could a shallow clone be making this worse?
Yes. A shallow clone keeps only the checked-out commit and minimal refs, so even git rev-parse --abbrev-ref HEAD returns HEAD. Set GIT_DEPTH: 0 when a tool needs to walk history or recover branch refs.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.