Skip to content
🎉 Launch sale:50% off everything over $22 — automatically applied at checkout· ends Aug 2Shop the sale →
DevOps AI ToolKit
Newsletter
All guides
AI for Automation By James Joyner IV · · 8 min read Last reviewed Jul 2026

Git Error: 'hook declined to update refs/heads/main' — Cause, Fix, and Troubleshooting Guide

Quick answer

Fix Git's 'remote: error: hook declined to update refs/heads/main' and 'pre-receive hook declined' — branch protection, push rules, file size, and policy hooks.

  • #automation
  • #troubleshooting
  • #git
Free toolkit

Fixing errors like this? Get 500 free DevOps AI prompts

500 copy-paste AI prompts for the stack you actually run — one PDF, free.

Overview

A pre-receive hook runs on the Git server the moment you push, before any ref is updated. If that hook exits non-zero, the server rejects the entire push and none of your commits land. Unlike a client-side hook you can inspect locally, this one lives on the remote — so the only clue you get is whatever the hook chooses to print, prefixed with remote::

$ git push origin main
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Writing objects: 100% (5/5), 612 bytes | 612.00 KiB/s, done.
Total 5 (delta 3), reused 0 (delta 0)
remote: error: GL-HOOK-ERR: commit a1b2c3d has no linked issue in the message
remote: error: hook declined to update refs/heads/main
To example.com:team/service.git
 ! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to 'example.com:team/service.git'

The three lines that matter, in order: the remote: error: line(s) with the actual reason, hook declined to update refs/heads/main, and ! [remote rejected] main -> main (pre-receive hook declined). The push is atomic — a single declined ref rejects everything in that push.

Symptoms

  • remote: error: hook declined to update refs/heads/<branch> on push.
  • ! [remote rejected] <branch> -> <branch> (pre-receive hook declined) in the push summary.
  • The failure is server-side: it happens after “Writing objects” completes, not during your local commit.
  • Pushing to a different branch (a feature branch instead of main) succeeds, while main/master/release/* is rejected.
  • The reason text varies by platform: GL-HOOK-ERR (GitLab), remote: error: File X is ... MB; this exceeds ... (size limits), commit-message policy messages, or a bespoke company hook.

Common Root Causes

1. Protected branch / branch protection rule

The target branch (main, master, release/*) is protected and forbids direct pushes, force-pushes, or pushes that bypass review. The platform enforces this with a pre-receive hook, so the low-level error you see is “hook declined.”

2. Server-side lint, test, or policy check failed

The hook runs a linter, secret scanner, or CI gate synchronously and rejects the push when it fails. Typical messages call out a specific file, rule, or offending commit.

3. GitLab / Gerrit push rules

Push rules can require a commit-message regex, a signed commit, a specific author email domain, or reject certain filenames. Violations surface as GL-HOOK-ERR: ... followed by the declined line.

4. File-size or LFS policy limit

A blob exceeds the server’s maximum object size (common on hosted platforms), or a large file was committed without Git LFS. The hook rejects the push and names the file and its size.

5. Commit-message or signature policy

A hook that requires a linked issue key (PROJ-123), a Signed-off-by trailer, or a GPG signature declines commits that lack it.

6. A bug in the server hook itself

Occasionally the hook script errors out (missing interpreter, bad path, a broken dependency on the server) and exits non-zero for reasons unrelated to your commits. The message is often less specific — a stack trace or command not found echoed through remote:.

How to Diagnose

Read the remote: lines first — they carry the real reason. Everything not prefixed with remote: is generic Git plumbing:

git push origin main 2>&1 | grep '^remote:'
remote: error: GL-HOOK-ERR: commit message must reference an issue (e.g. PROJ-123)
remote: error: hook declined to update refs/heads/main

Confirm whether the branch is protected. On GitHub/GitLab this is in the repo settings; from the CLI you can check with the platform tool:

# GitHub
gh api repos/team/service/branches/main/protection --jq '.required_status_checks, .required_pull_request_reviews' 2>/dev/null
# GitLab
glab api projects/:id/protected_branches 2>/dev/null
{"strict":true,"contexts":["ci/lint"]}
{"required_approving_review_count":1}

Inspect what you are actually trying to push, so you can see which commit or file the hook objects to:

git log --oneline origin/main..HEAD
git diff --stat origin/main..HEAD
a1b2c3d add bulk import
9f8e7d6 wip
 data/dump.sql | 1 +
 src/import.py |  42 +++++++

If the message mentions file size, find the large blob before you try again:

git rev-list --objects --all \
  | git cat-file --batch-check='%(objecttype) %(objectsize) %(rest)' \
  | awk '/^blob/ && $2 > 5000000 {print $2, $3}' | sort -n | tail
104857600 data/dump.sql

If you have server access (self-hosted), the hook’s own logs are authoritative:

# On the Git server
sudo tail -50 /var/log/gitlab/gitlab-shell/gitlab-shell.log
sudo tail -50 /opt/git/repositories/service.git/hooks/pre-receive.log

Fixes

Satisfy the policy rather than fighting it. If the hook demands an issue key in the message, amend the offending commit and push again:

git commit --amend -m "add bulk import (PROJ-123)"
git push origin main
remote: Resolving deltas: 100% (3/3), completed with 4 local objects.
To example.com:team/service.git
   9f8e7d6..a1b2c3d  main -> main

For a protected branch, push through the sanctioned path — a feature branch plus a pull/merge request — instead of directly to main:

git push origin HEAD:feature/bulk-import
gh pr create --base main --head feature/bulk-import --fill

For a rejected large file, remove it from history and route it through Git LFS or object storage:

git rm --cached data/dump.sql
echo 'data/dump.sql' >> .gitignore
git commit --amend --no-edit
# or track it properly with LFS
git lfs track '*.sql'
git add .gitattributes data/dump.sql && git commit --amend --no-edit

Reproduce the check locally before you push again, so you are not iterating against the server. Many teams ship the same linter as a client-side hook (see the pre-commit guide below) so violations are caught at commit time.

If the message indicates a hook bug rather than a policy you violated (a stack trace, command not found, or a check unrelated to your change), you cannot fix it from the client — capture the full remote: output and contact the repository or platform admin:

git push origin main 2>&1 | tee push-error.log

What to Watch Out For

  • The push is atomic per invocation. One declined ref rejects the whole push, so a single bad commit blocks everything you were pushing.
  • --force does not bypass a pre-receive hook; the hook runs regardless and protected branches typically reject force-pushes outright.
  • The remote: prefix is added by Git to anything the server hook writes to stderr. Do not ignore those lines as noise — they are the only diagnostic you get.
  • Amending or rebasing to satisfy a message/signature policy rewrites commit SHAs. Coordinate if others have already based work on those commits.
  • Server hook logs live on the platform, not in your clone. On hosted services you will need the admin or the platform’s audit UI; only self-hosted setups give you tail access.
Free download · 368-page PDF

Fixed it? Get 500 Automation & DevOps AI prompts — free

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.

Did this fix your issue?

Free download · 368-page PDF

Get 500 Battle-Tested DevOps AI Prompts — Free

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.