GitHub Actions detached HEAD after actions/checkout
GitHub Actions detached HEAD after actions/checkout is documented behavior rather than a bug, and it does not happen on every event. The action puts you on a real local branch when the ref it was given is a branch, and leaves HEAD on a bare commit when it is anything else, which on a pull request is always the case.

What this error means
The checkout step is green and so is every step after it, until a git command needs to know which branch you are on. The commit succeeds and prints [detached HEAD <sha>] where you expected a branch name, which is the first sign and the one most people scroll past. Then the push stops, because there is no current branch for git to map to a remote one, and git prints the advice below with the command it would rather you ran. The step exits 128 and the job goes red on a line that has nothing to do with your build.
Run git config user.name github-actions
git config user.name github-actions
git config user.email github-actions@github.com
git add .
git commit -m "Build CSS"
git push
shell: /bin/bash -e {0}
[detached HEAD 706b5f8] Build CSS
2 files changed, 2 insertions(+), 2 deletions(-)
rewrite dist/css/plane.min.css.map (83%)
fatal: You are not currently on a branch.
To push the history leading to the current (detached HEAD)
state now, use
git push origin HEAD:<name-of-remote-branch>
##[error]Process completed with exit code 128.A minimal workflow that produces it
This file is written for this page and has never been run. It is the smallest shape that reaches the error: a pull request trigger, a default checkout, and a step that commits and pushes. Nothing in it is deprecated and nothing is wrong as YAML. The checkout step does exactly what its README says it will do, and the push is simply the first command in the file that assumes a current branch exists.
name: ci
on: pull_request
jobs:
format:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v5
- run: npm ci && npm run format
- run: |
git config user.name github-actions
git config user.email github-actions@github.com
git commit -am formatted
git pushCommon causes
The event ref is not a branch ref
The common one by a wide margin, and the one the README warns about. A pull request run stands on the merge ref GitHub built for the comparison, so the action checks out a remote-tracking ref and leaves HEAD on the merge commit. The same happens on a tag push and on any run where you passed a SHA yourself.
The push names no branch
A bare push from a detached HEAD has no current branch, so git has nothing to map to a remote branch. The message is not about permissions or about the token, and it names the alternative it wants: a refspec with both ends written out.
The ref passed is the merge ref rather than the source branch
Reaching for github.ref on a pull request looks like the fix and is not, because the value is the merge ref the run already has. The event carries the source branch under a different name, and that is the one the action can turn into a local branch.
The branch was named but never fetched
A shallow default checkout fetches one commit. Ask the action for a branch it has never seen and it reports that "A branch or tag with the name '<name>' could not be found" rather than detaching, which is a different message for a related mistake.
How to fix it
Decide whether this job needs a branch at all
- If the job only builds and tests, a detached HEAD is fine: stop reading the branch name.
- If the job commits, pass the source branch to the checkout step.
- If the job pushes somewhere other than the branch it is on, leave HEAD detached and write the refspec out.
Pass the source branch on a pull request
The pull request events carry the source branch as github.head_ref, documented as "The head_ref or source branch of the pull request in a workflow run". Using github.ref_name as the fallback covers every other event with one expression, because on those it is already the short branch name.
- uses: actions/checkout@v5
with:
ref: ${{ github.head_ref || github.ref_name }}Or keep HEAD detached and name both ends of the push
This is the form git suggests in the error, and it is the better one when the job is deliberately building from a merge commit or a tag. Nothing about the checkout changes, so the job keeps testing exactly the commit GitHub evaluated.
- run: git push origin HEAD:${{ github.head_ref }}Fetch the history the operation needs
Checkout takes one commit by default: "Only a single commit is fetched by default, for the ref/SHA that triggered the workflow. Set fetch-depth: 0 to fetch all history for all branches and tags." A rebase or a changelog generator needs more. Ask for what you need, because depth is the largest lever on checkout time in a big repository.
- uses: actions/checkout@v5
with:
ref: ${{ github.head_ref }}
fetch-depth: 0The action detaches for some refs and not for others
The README documents the default for the ref input: "The branch, tag or SHA to checkout. When checking out the repository that triggered a workflow, this defaults to the reference or SHA for that event. Otherwise, uses the default branch." It is explicit about the pull request case too: "In a pull request trigger, ref is required as GitHub Actions checks out in detached HEAD mode, meaning it doesn't check out your branch by default."
getCheckoutInfo in src/ref-helper.ts sorts the ref into six cases and sets a start point for exactly two of them: a ref under refs/heads/, and a bare branch name that already exists on the remote. A refs/pull/ ref, a tag, any other fully qualified ref and a bare SHA get none. The start point is what turns the checkout into a branch creation, so those two cases are the ones that leave you on a branch, which is also why the fix below works: a short branch name takes the second path.
const args = ['checkout', '--progress', '--force']
if (startPoint) {
args.push('-B', ref, startPoint)
} else {
args.push(ref)
}Name the branch, and the same file works
The corrected file below is the one above with three lines added, and it is the form the README publishes under "Push a commit to a PR using the built-in token". The pull request event carries the source branch separately from the ref the run stands on, and that is the value to hand the action.
Read the two samples as a pair. The first fails at the push because HEAD is a commit; the second pushes because HEAD is a branch with an upstream. The permissions block was already right in both, which is why this reads as a permissions problem at first.
name: ci
on: pull_request
jobs:
format:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.head_ref }}
- run: npm ci && npm run format
- run: |
git config user.name github-actions
git config user.email github-actions@github.com
git commit -am formatted
git pushWhich ref each event hands the action
The contexts reference defines what the action receives by default. github.ref is "The fully-formed ref of the branch or tag that triggered the workflow run", and for pull requests it spells out the shape: "For pull request events except pull_request_target that were not merged, it is refs/pull/<pr_number>/merge. pull_request_target events have the ref from the base branch."
Put that beside the four shapes above and the table writes itself. It also explains the reports that start "it works on main but not on pull requests".
| Event | Default ref | Where HEAD lands | What the push needs |
|---|---|---|---|
push to a branch | refs/heads/<branch> | a local branch | nothing, it already works |
push of a tag | refs/tags/<tag> | detached | an explicit ref: |
pull_request | refs/pull/<n>/merge | detached | ref: github.head_ref |
pull_request_target | the base branch ref | a local branch, the base one | an explicit ref: |
workflow_dispatch | the dispatched branch or tag | branch, or detached for a tag | HEAD:<branch> if detached |
Why there is no recorded run on this page
The decision is made inside the action from the event payload and a git ref, and the result is the same on every runner on every attempt. There is nothing transient to retry and nothing for a runner to repair: a detached HEAD is the correct outcome of the inputs the action was given. The log at the top is quoted from actions/checkout#317, where it was reported with the workflow that produced it, and both workflows above are illustrative.
How to prevent it
- Treat a checkout without an explicit
refas a checkout of whatever the event carried, which is not always a branch. - Write pushes as
HEAD:<branch>in any job that might run on a tag or a merge ref. - Read the branch from the event rather than from
git symbolic-ref, which has nothing to report on a detached HEAD. - Set
fetch-depthdeliberately whenever a step does more than read the tip commit.
Frequently asked questions
Why does actions/checkout leave a detached HEAD?
refs/heads/. A pull request merge ref, a tag and a bare SHA all take the other path and are checked out directly, which is a detached HEAD by definition.How do I fix "fatal: You are not currently on a branch" in GitHub Actions?
ref: on the checkout step, or leave the checkout alone and push to an explicit refspec. Git names the second option in the error itself, quoted at the top of this page.Why does my workflow push fine on main but not on a pull request?
refs/heads/<branch>, which the action turns into a local branch, so a bare push works. A pull request arrives as a merge ref, which it cannot, so the same steps hit a detached HEAD.Should I use github.ref or github.head_ref for checkout?
github.head_ref on pull requests, because github.ref is the merge ref there, and checking that out is what left HEAD detached in the first place. On every other event github.ref_name is the short name you want, and the pair covers both.Related guides
References
- actions/checkout README: the ref input, fetch-depth and the pull request push example
- actions/checkout#317: fatal: You are not currently on a branch
- GitHub Actions: contexts reference, github context (ref, ref_name, head_ref)
- GitHub Actions: events that trigger workflows, pull_request
- GitHub Actions documentation