This error occurs when Git commands attempt to determine the current branch name but the repository is in a detached HEAD state. This commonly happens in CI/CD pipelines where Git checkouts default to specific commits rather than branch references. The fix involves explicitly checking out a branch or using Git commands that handle detached HEAD gracefully.
The "fatal: ref HEAD is not a symbolic ref" error indicates that Git cannot determine which branch you're on because the repository is in a "detached HEAD" state. In this state, HEAD points directly to a specific commit SHA rather than pointing to a branch reference. Normally, HEAD is a "symbolic reference"—it contains the path to a branch (like `refs/heads/main`), and that branch contains the commit SHA. When you run `git symbolic-ref HEAD`, Git reads this reference and tells you the branch name. But when HEAD contains a raw commit SHA instead (detached HEAD), Git cannot determine the branch name and throws this error. This situation is extremely common in CI/CD environments: - **Jenkins** checks out commits by SHA by default, not by branch name - **GitHub Actions** defaults to checkout by commit for reproducibility - **GitLab CI** uses detached HEAD for most pipeline triggers - **Release events** (tags) trigger workflows on a commit, not a branch - **Maven Release Plugin** and similar tools call `git symbolic-ref` internally and fail The detached HEAD state isn't inherently problematic—Git works fine in this state for most operations. The error only surfaces when a command (or a tool wrapping Git) specifically needs to know the current branch name and uses `git symbolic-ref` to find it.
First, confirm that your repository is actually in a detached HEAD state:
# Check HEAD status
git symbolic-ref HEAD
# If detached, you'll see: fatal: ref HEAD is not a symbolic ref
# Check what HEAD points to
cat .git/HEAD
# Detached: Shows a commit SHA (e.g., "a1b2c3d4e5f6...")
# Normal: Shows "ref: refs/heads/main" or similar
# Alternative check using rev-parse
git rev-parse --abbrev-ref HEAD
# Returns "HEAD" if detached, or branch name if on a branch
# Modern alternative (Git 2.22+)
git branch --show-current
# Returns empty string if detached, or branch name if on a branchUnderstanding your current state helps you choose the right fix.
The most direct fix is to checkout an actual branch before running commands that need branch detection:
# Checkout the main branch
git checkout main
# Or checkout a specific branch
git checkout develop
# Force checkout if there are conflicts
git checkout -f mainImportant considerations:
- Make sure the branch name matches your repository's default branch
- If the branch doesn't exist locally, fetch first: git fetch origin main:main
- In CI, the branch name is usually available as an environment variable
Quick pre-flight script for CI:
#!/bin/bash
# Ensure we're on a branch, not detached HEAD
BRANCH=${BRANCH_NAME:-main}
git checkout -f "$BRANCH" || git checkout -B "$BRANCH" origin/"$BRANCH"In GitHub Actions, update your checkout step to reference a branch explicitly:
Basic fix - specify ref:
- uses: actions/checkout@v4
with:
ref: mainFor pull requests - checkout the PR branch:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}For release events - use the release branch:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.release.target_commitish }}Dynamic branch checkout for any trigger:
- uses: actions/checkout@v4
- name: Checkout branch (not detached HEAD)
run: |
git checkout -B "${{ github.ref_name }}" || trueFor create-pull-request action (v3.2.0+):
- uses: peter-evans/create-pull-request@v5
with:
base: main # Handles commit checkouts by rebasingJenkins Git plugin defaults to detached HEAD. Here's how to fix it:
Option 1: Configure via UI
1. Go to your Jenkins job configuration
2. Under "Source Code Management" → Git → "Additional Behaviours"
3. Click "Add" and select "Check out to matching local branch"
4. Leave the branch field as ** to match any branch automatically
Option 2: Pipeline script
checkout([
$class: 'GitSCM',
branches: [[name: '*/main']],
extensions: [
[$class: 'LocalBranch', localBranch: '**'],
[$class: 'CleanCheckout']
],
userRemoteConfigs: [[url: 'https://github.com/your/repo.git']]
])Option 3: Declarative pipeline
pipeline {
agent any
stages {
stage('Checkout') {
steps {
checkout scm: [
$class: 'GitSCM',
extensions: [[$class: 'LocalBranch', localBranch: '**']]
]
}
}
}
}Option 4: Post-checkout script
sh '''
git checkout -f ${BRANCH_NAME}
'''GitLab CI also uses detached HEAD by default. Add these steps to your pipeline:
In .gitlab-ci.yml:
before_script:
- git checkout -B "$CI_COMMIT_REF_NAME" "$CI_COMMIT_SHA"
# Or simply:
- git checkout "$CI_COMMIT_REF_NAME"For specific jobs:
release:
stage: deploy
before_script:
- git fetch origin
- git checkout -f $CI_COMMIT_REF_NAME
script:
- mvn release:prepare release:performUsing Git strategy variable:
variables:
GIT_STRATEGY: fetch
GIT_CHECKOUT: "true"
before_script:
- git checkout $CI_COMMIT_REF_NAMEGitLab CI environment variables for reference:
- CI_COMMIT_REF_NAME: Branch or tag name
- CI_COMMIT_SHA: Commit SHA
- CI_COMMIT_BRANCH: Branch name (empty for tags)
- CI_COMMIT_TAG: Tag name (empty for branches)
The Maven Release Plugin uses git symbolic-ref internally to detect the branch. Fix the CI checkout first, or configure Maven:
Option 1: Fix CI checkout (recommended)
Use the Jenkins or GitLab solutions from previous steps to ensure you're on a branch before Maven runs.
Option 2: Specify branch in Maven command
mvn release:prepare -DpushChanges=false -DlocalCheckout=true \
-DdevelopmentVersion=1.0.1-SNAPSHOT \
-DreleaseVersion=1.0.0Option 3: Pre-release checkout script
#!/bin/bash
# Run before mvn release:prepare
BRANCH=${GIT_BRANCH:-main}
BRANCH=${BRANCH#origin/} # Remove origin/ prefix if present
git checkout -f "$BRANCH"Option 4: Configure in pom.xml (limited)
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>3.0.1</version>
<configuration>
<localCheckout>true</localCheckout>
<pushChanges>false</pushChanges>
</configuration>
</plugin>Note: The localCheckout option helps but doesn't fully resolve the branch detection issue.
If you're writing scripts that need the branch name, use commands that handle detached HEAD without errors:
Instead of git symbolic-ref (errors on detached HEAD):
# DON'T use this - fails on detached HEAD
git symbolic-ref --short HEADUse git branch --show-current (Git 2.22+):
# Returns empty string on detached HEAD (no error)
BRANCH=$(git branch --show-current)
if [ -z "$BRANCH" ]; then
echo "Detached HEAD - no branch"
else
echo "On branch: $BRANCH"
fiUse git rev-parse --abbrev-ref:
# Returns "HEAD" if detached, otherwise returns branch name
BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$BRANCH" = "HEAD" ]; then
echo "Detached HEAD state"
else
echo "On branch: $BRANCH"
fiUse symbolic-ref with quiet mode:
# Exit code 1 on detached HEAD, no error message
if git symbolic-ref -q HEAD > /dev/null; then
BRANCH=$(git symbolic-ref --short HEAD)
echo "On branch: $BRANCH"
else
echo "Detached HEAD state"
fiShallow clones in CI can exacerbate this issue by lacking full branch information:
GitHub Actions - full clone:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history
ref: main # Checkout branch, not detachedGitLab CI - disable shallow clone:
variables:
GIT_DEPTH: 0 # Full clone, not shallow
GIT_STRATEGY: clone
before_script:
- git checkout $CI_COMMIT_REF_NAMEJenkins - disable shallow clone:
In your Git configuration, under "Additional Behaviours":
1. Add "Advanced clone behaviours"
2. Uncheck "Shallow clone" or set depth to 0
Bamboo:
1. Go to Plan Configuration → Repositories → Git
2. Under "Advanced options", uncheck "Use shallow clones"
3. Enable "Force Clean Build" in Source Checkout task
General CI fetch command:
# Convert shallow to full clone if needed
git fetch --unshallow origin || true
git fetch origin "+refs/heads/*:refs/remotes/origin/*"
git checkout $BRANCH_NAMEUnderstanding HEAD and Symbolic References:
In Git, HEAD indicates "where you are now." There are two ways HEAD can work:
1. Symbolic reference (normal): HEAD contains ref: refs/heads/main, which points to the branch file, which contains the commit SHA
2. Detached HEAD: HEAD contains the commit SHA directly (e.g., a1b2c3d4...)
When tools run git symbolic-ref HEAD, they're asking Git to read HEAD as a symbolic reference. If HEAD contains a raw SHA, this fails with "fatal: ref HEAD is not a symbolic ref."
Why CI Systems Use Detached HEAD:
CI systems default to detached HEAD for good reasons:
- Reproducibility: Checking out by SHA ensures the exact same code runs
- Parallelism: Multiple builds can run simultaneously without branch conflicts
- Pull request handling: PRs have merge commits that don't exist on any branch
- Tag builds: Tags point to commits, not branches
The Modern Solution - git branch --show-current:
Git 2.22 (June 2019) introduced git branch --show-current, which:
- Returns the current branch name if on a branch
- Returns empty string (not an error) if in detached HEAD
- Is safer for scripting than git symbolic-ref
If you maintain build scripts, consider switching to this command.
Workaround When You Can't Modify CI Config:
If you can't change the CI configuration but need branch detection, use this fallback chain:
get_branch() {
# Try symbolic-ref first (fastest)
git symbolic-ref --short HEAD 2>/dev/null && return
# Fall back to CI environment variables
[ -n "$GITHUB_HEAD_REF" ] && echo "$GITHUB_HEAD_REF" && return
[ -n "$GITHUB_REF_NAME" ] && echo "$GITHUB_REF_NAME" && return
[ -n "$CI_COMMIT_REF_NAME" ] && echo "$CI_COMMIT_REF_NAME" && return
[ -n "$GIT_BRANCH" ] && echo "${GIT_BRANCH#origin/}" && return
[ -n "$BRANCH_NAME" ] && echo "$BRANCH_NAME" && return
# Last resort: try to find branches containing current commit
git branch -r --contains HEAD 2>/dev/null | head -1 | sed 's/.*origin\///'
}Platform-Specific Environment Variables:
| Platform | Branch Variable |
|----------|-----------------|
| GitHub Actions | GITHUB_REF_NAME, GITHUB_HEAD_REF (PRs) |
| GitLab CI | CI_COMMIT_REF_NAME, CI_COMMIT_BRANCH |
| Jenkins | BRANCH_NAME, GIT_BRANCH |
| CircleCI | CIRCLE_BRANCH |
| Travis CI | TRAVIS_BRANCH |
| Azure Pipelines | BUILD_SOURCEBRANCH |
| Bitbucket Pipelines | BITBUCKET_BRANCH |
When Detached HEAD Is Actually Correct:
Some situations legitimately require detached HEAD:
- Inspecting historical commits (git checkout abc123)
- Bisecting (git bisect)
- Running CI on exact commits for reproducibility
- Checking out tags for release builds
In these cases, instead of fighting the detached HEAD, update your tools/scripts to not require branch detection, or to fall back to the commit SHA when no branch is available.
kex_exchange_identification: Connection closed by remote host
Connection closed by remote host when connecting to Git server
fatal: unable to access: Proxy auto-configuration failed
How to fix 'Proxy auto-configuration failed' in Git
fatal: unable to access: Authentication failed (proxy requires basic auth)
How to fix 'Authentication failed (proxy requires basic auth)' in Git
fatal: unable to access: no_proxy configuration not working
How to fix 'no_proxy configuration not working' in Git
fatal: unable to read tree object in treeless clone
How to fix 'unable to read tree object in treeless clone' in Git