Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for GitLab CI/CD By James Joyner IV · · 9 min read

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

  1. A script calls git symbolic-ref HEAD / git branch --show-current to discover the branch — but the runner checked out a detached SHA.
  2. Tooling that assumes an attached branch (semantic-release, some release plugins, git flow) runs unmodified inside CI.
  3. Shallow clone (GIT_DEPTH) removes the branch ref locally, so even git rev-parse --abbrev-ref cannot recover the name.
  4. Detached pipelines for merge requests, where the checked-out ref is a merged result commit with no branch at all.
  5. 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_BRANCH is set on branch pipelines (empty for tags and MR pipelines).
  • CI_COMMIT_REF_NAME is the branch or tag name — the safest single variable.
  • CI_COMMIT_TAG is 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 HEAD or git branch --show-current in pipeline scripts.
  • Standardise on CI_COMMIT_REF_NAME for “the thing being built” and branch on CI_COMMIT_TAG when you need tag-specific logic.
  • For release tooling, add a single git checkout -B "$CI_COMMIT_REF_NAME" step in before_script rather than patching every job.
  • Set GIT_DEPTH deliberately: shallow for speed, 0 when a tool needs history.
  • A quick way to spot these branch-detection bugs across a .gitlab-ci.yml is to paste a failing log into the free incident assistant, which flags detached-HEAD assumptions. More patterns are in the GitLab CI/CD guides.
  • fatal: reference is not a tree — a shallow clone is missing the commit a tool asks for; raise GIT_DEPTH or set it to 0.
  • 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 with git 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.

Free download · 368-page PDF

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.