This error occurs when Git cannot acquire a lock on a reference file during a prune operation, commonly in CI/CD pipelines. It usually happens when stale references conflict with prune operations, or when concurrent jobs access the same repository. The fix involves cleaning up stale refs, using proper fetch flags, or adjusting CI configuration.
The "cannot lock ref" error during `git fetch --prune` indicates that Git failed to update or delete a reference because it couldn't acquire an exclusive lock on the ref file. In CI/CD environments, this error often surfaces because of race conditions, stale repository state, or conflicting reference updates. When you run `git fetch --prune`, Git: 1. Fetches new refs from the remote 2. Attempts to delete local refs that no longer exist on the remote 3. Needs to lock each ref file during modification The error message typically looks like: ``` error: cannot lock ref 'refs/remotes/origin/feature-branch': is at abc1234 but expected def5678 ! [new branch] feature-branch -> origin/feature-branch (unable to update local ref) ``` Or in more severe cases: ``` error: cannot lock ref 'refs/remotes/origin/some-branch': unable to resolve reference ``` This happens most often in CI environments because: - **Cached workspaces** retain stale refs from previous builds - **Shallow clones** have incomplete ref histories that conflict with full prune operations - **Concurrent jobs** may be accessing or modifying the same refs - **Branch force-pushes** cause ref SHA mismatches between cached state and remote
The most reliable fix is to force Git to update refs even when they've diverged:
# Force update all refs during fetch
git fetch --prune --force origin
# Or fetch a specific branch with force
git fetch --force origin main:refs/remotes/origin/mainThe --force flag tells Git to update refs even when the update is not a fast-forward, which handles force-pushed branches.
In your CI configuration:
# GitHub Actions
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: git fetch --prune --force origin
# GitLab CI
script:
- git fetch --prune --force origin
- git checkout $CI_COMMIT_REF_NAME
# Jenkins
sh 'git fetch --prune --force origin'Before fetching, remove stale remote tracking references that may cause conflicts:
# Remove all remote tracking branches and re-fetch
git remote prune origin
# Or manually remove problematic refs
git update-ref -d refs/remotes/origin/stale-branch
# Nuclear option - remove all remote refs and re-fetch
rm -rf .git/refs/remotes/origin/*
rm -f .git/packed-refs # Also clear packed refs
git fetch --prune originAs a CI script step:
# GitHub Actions
- name: Clean refs and fetch
run: |
rm -rf .git/refs/remotes/origin/* 2>/dev/null || true
git fetch --prune --force origin
# GitLab CI
before_script:
- rm -rf .git/refs/remotes/origin/* 2>/dev/null || true
- git fetch --prune --force originIf your CI caches the git repository between builds, stale refs accumulate. Consider using a fresh clone:
GitHub Actions:
- uses: actions/checkout@v4
with:
# Don't use cached repository
clean: true
# Full clone to avoid shallow clone issues
fetch-depth: 0GitLab CI:
variables:
# Fresh clone every time
GIT_STRATEGY: clone
# Or use fetch but clean first
GIT_CLEAN_FLAGS: "-ffdx"
# Alternative: disable cache for git
cache:
paths:
- node_modules/
# Don't cache .gitJenkins:
checkout([
$class: 'GitSCM',
branches: [[name: 'main']],
extensions: [
[$class: 'CleanBeforeCheckout'],
[$class: 'WipeWorkspace']
],
userRemoteConfigs: [[url: 'https://github.com/org/repo.git']]
])CircleCI:
- checkout:
# Clean checkout
path: ~/projectShallow clones (--depth=1) can cause ref resolution issues during prune. Either deepen the clone or avoid prune with shallow clones:
# Unshallow the repository first
git fetch --unshallow origin
# Then prune safely
git fetch --prune originOr increase clone depth in CI:
# GitHub Actions
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history, no shallow clone
# GitLab CI
variables:
GIT_DEPTH: 0 # Full clone
# Azure DevOps
- checkout: self
fetchDepth: 0If you must use shallow clones, skip prune:
# Fetch without pruning on shallow repos
git fetch origin
# Only prune after unshallowingIn concurrent environments, lock files may get stuck. Clean them up before fetch:
# Find and remove any stale lock files
find .git -name "*.lock" -type f -mmin +5 -delete
# Or specifically for refs
rm -f .git/refs/remotes/origin/*.lock
rm -f .git/refs/heads/*.lock
# Then fetch
git fetch --prune originCI script with lock cleanup:
# GitHub Actions
- name: Clean locks and fetch
run: |
find .git -name "*.lock" -type f -delete 2>/dev/null || true
git fetch --prune --force originIf the error mentions "cannot lock ref" with permissions issues:
# Check ref directory permissions
ls -la .git/refs/remotes/origin/
# Fix ownership if needed (CI runners sometimes have permission issues)
chown -R $(whoami) .git/Git stores refs in both loose files (.git/refs/) and a packed file (.git/packed-refs). Corruption or inconsistencies between them cause lock errors:
# Rebuild packed-refs from loose refs
git pack-refs --all
# Or clear packed-refs and let Git rebuild
rm -f .git/packed-refs
git fetch --prune originVerify ref consistency:
# Check for ref problems
git for-each-ref --format='%(refname) %(objectname)' refs/remotes/origin/
# Verify objects exist for all refs
git fsck --fullIf specific refs are corrupted:
# Identify the problematic ref from error message
# e.g., "cannot lock ref 'refs/remotes/origin/feature'"
# Remove it from packed-refs (if present)
grep -v "refs/remotes/origin/feature" .git/packed-refs > .git/packed-refs.tmp
mv .git/packed-refs.tmp .git/packed-refs
# Remove loose ref file
rm -f .git/refs/remotes/origin/feature
# Re-fetch
git fetch origin feature:refs/remotes/origin/featureIf parallel jobs access the same workspace, coordinate access or use separate workspaces:
GitHub Actions - use unique workspaces:
jobs:
build:
runs-on: ubuntu-latest
# Each job gets a fresh runner workspace by default
steps:
- uses: actions/checkout@v4
test:
runs-on: ubuntu-latest
# This is a separate workspace - no conflict
steps:
- uses: actions/checkout@v4GitLab CI - use separate clone directories:
variables:
GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_JOB_ID
job1:
script:
- git fetch --prune origin
job2:
script:
- git fetch --prune originJenkins - use workspace cleanup:
pipeline {
options {
// Clean workspace before each build
skipDefaultCheckout()
}
stages {
stage('Checkout') {
steps {
cleanWs()
checkout scm
}
}
}
}For self-hosted runners with shared filesystems:
Consider using local git clone per job rather than caching the workspace across builds.
### Understanding Git Ref Locking
Git's ref locking mechanism prevents data corruption from concurrent updates:
1. Lock acquisition: Before modifying a ref, Git creates <ref>.lock file
2. Atomic write: New ref value is written to the lock file
3. Atomic rename: Lock file is renamed to the ref file
4. Lock release: If anything fails, lock file remains for debugging
The "cannot lock ref" error occurs at step 1 when:
- Another process holds the lock
- The ref's current SHA doesn't match expected SHA (ref changed externally)
- Filesystem permissions prevent lock creation
- Packed-refs and loose refs are inconsistent
### Why This Hits CI Harder
CI environments are particularly susceptible because:
1. Cached workspaces - Unlike local dev, CI may reuse .git directories across builds
2. Aggressive pruning - CI often runs --prune to clean up, which touches many refs
3. Force pushes - Feature branches get rebased/force-pushed frequently
4. Parallel jobs - Matrix builds may have race conditions
5. Shallow clones - Speed optimization breaks ref resolution
### The --force Flag Explained
git fetch --force updates refs even on non-fast-forward changes:
# Without --force: fails if local ref diverged from remote
git fetch origin main:refs/remotes/origin/main
# error: cannot lock ref: is at abc1234 but expected def5678
# With --force: overwrites local ref with remote
git fetch --force origin main:refs/remotes/origin/main
# success - local ref updated regardless of history### Configuring Refspec for CI
Set up fetch refspec to always force-update:
# Check current refspec
git config --get-all remote.origin.fetch
# Add force-update refspec
git config --add remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'
# The '+' prefix means force-update### Monitoring Ref State
Debug ref issues with these commands:
# Show all refs with their SHAs
git show-ref
# Show remote refs only
git show-ref --heads --tags | grep origin
# Compare local tracking refs with remote
git remote show origin
# List refs that would be pruned
git remote prune --dry-run origin### CI Platform-Specific Gotchas
GitHub Actions:
- actions/checkout@v4 creates a detached HEAD by default
- fetch-depth: 1 (default) causes shallow clone issues
- Workflow reruns may hit cached refs
GitLab CI:
- Default GIT_STRATEGY: fetch reuses repository
- GIT_STRATEGY: clone is slower but cleaner
- Shared runners may have filesystem latency
Jenkins:
- Git plugin caches repository in workspace
- "Prune stale remote-tracking branches" option helps
- Consider "Clean before checkout" extension
Azure DevOps:
- Default shallow fetch can cause issues
- Use fetchDepth: 0 for full history
- Agent pools may share workspaces
### Safe CI Git Configuration
Add this to your CI setup for robust Git operations:
# Full setup script for CI Git reliability
#!/bin/bash
set -e
# Remove any stale locks
find .git -name "*.lock" -type f -delete 2>/dev/null || true
# Ensure we have a full clone
if git rev-parse --is-shallow-repository | grep -q true; then
git fetch --unshallow origin || true
fi
# Force-update all refs
git fetch --prune --force origin
# Clean up any orphaned refs
git remote prune origin
# Verify repository health
git fsck --no-dangling || true
echo "Git repository is ready"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