skip to content

In Git, why does pushing to a non-bare repository's checked-out branch fail by default?

level: seniorimportance: nice to knowfreq 22%

answer

  1. It is about the receiving repository's working tree
  2. Files on disk would stop matching HEAD
  3. A receive-side config controls it
  4. Its default value is to refuse
  5. Shared remotes should have no working tree

basics

~20 s

Because receive.denyCurrentBranch defaults to refuse: moving the branch that a working tree has checked out would leave that tree and index disagreeing with HEAD, so the receiving repository rejects the update. Shared remotes should be bare.

solid answer

~50 s

In a non-bare repository the working tree and index are kept consistent with `HEAD`. If a push moved the branch `HEAD` points at, the checkout would silently describe a different commit than the files on disk, and the owner's next `git status` would show a mountain of phantom changes — actually the inverse of the pushed commits. Git therefore refuses, controlled by `receive.denyCurrentBranch`, whose default is `refuse`. Other values: `warn` allows the update and prints a warning, `ignore` allows it silently, and `updateInstead` accepts the push and updates the receiving working tree — but only when that tree and index are clean, refusing otherwise. Pushing to a branch that is *not* checked out there is unaffected. The standard fix is to make the shared remote bare (`git init --bare` or `git clone --bare`), which has no working tree and so no conflict.

code

console · 3 lines
console
$ git push ../teammate-checkout main
 ! [remote rejected] main -> main (branch is currently checked out)
error: failed to push some refs to '../teammate-checkout'

go deeper

for a junior

Recall that shared remotes are bare repositories and that pushing into somebody's ordinary working checkout is refused by default. The message names the checked-out branch.

for a middle

Explain the invariant: a push updates refs only, so moving the checked-out branch would leave the index and working tree describing a different commit than HEAD.

for a senior

Name receive.denyCurrentBranch and its values, choose updateInstead rather than ignore for a deliberate deploy target, and know that linked worktrees extend the protection to several branches.

for a principal

Frame it as receiver-side policy: guards like denyCurrentBranch, denyDeletes, and denyNonFastForwards belong on the receiving repository precisely because they cannot be undone by a misconfigured client.

## The invariant being protected A non-bare repository has three related things: `HEAD` naming a branch, an index, and a working tree. Normal Git operations move all three together, or refuse when that is unsafe. A push arrives from outside and updates only refs — it never touches the receiver's index or working tree. Move the checked-out branch that way and the invariant breaks: `HEAD` now names commit Y while the files and index still reflect X. The owner sees `git status` full of modifications they never made, in the *reverse* direction of the pushed change, and a careless `git checkout .` or `git reset --hard` at that point discards the pushed work or their own. ## The setting `receive.denyCurrentBranch` is read by `git receive-pack` in the receiving repository. Values: - **`refuse`** (default) — reject the update with an error explaining that the branch is checked out. - **`warn`** — perform the update but print a warning to the pusher. The working tree is left stale. - **`ignore`** — perform the update silently. The same staleness, without the notice. - **`updateInstead`** — accept the push and also update the receiving working tree and index to match, but only if they are clean; if there are local modifications or staged changes, the push is refused rather than clobbering them. `updateInstead` is the value that makes "push to deploy" onto a checked-out directory workable, since the receiving checkout stays consistent. Its clean-tree requirement is the safety property that makes it different from `ignore`. ## Bare repositories The conventional answer is that a repository serving as a shared remote should be **bare**: created with `git init --bare` or `git clone --bare`, it holds the object database and refs but no working tree, so there is no checked-out branch to conflict with and no invariant to break. Any branch can be pushed at any time. This is why hosting setups and shared servers use bare repositories, and why the error is most often seen when someone hand-rolls a remote by pointing at an ordinary clone — for example pushing between two working checkouts on the same machine. ## Worktrees widen the surface `git worktree` attaches additional working trees to one repository, each with its own `HEAD` on its own branch. Every one of those branches is "checked out" from the receiving repository's point of view, so the same protection applies to each. A repository that looks bare-ish but has linked worktrees can still refuse pushes to several branches, which is a confusing symptom until you run `git worktree list`. ## Related receiver-side guards `receive.denyCurrentBranch` sits in a family of settings the receiving repository uses to constrain what pushers may do, alongside `receive.denyDeletes` (reject branch deletions) and `receive.denyNonFastForwards` (reject history rewinds regardless of what flags the client passed). They are enforced server-side, which is the point: they do not depend on every contributor configuring their client correctly. Beyond them, `pre-receive` and `update` hooks can reject arbitrary updates with their own messages. ## Diagnosing it The error text names the situation directly — a refusal to update the checked-out branch, with a hint about `receive.denyCurrentBranch`. Two checks settle it: is the receiving repository bare (`git rev-parse --is-bare-repository` in it, or `core.bare` in its config), and which branch is its `HEAD` on. If you control the receiving end and it genuinely should be a shared remote, converting the workflow to a bare repository is a better answer than loosening the setting. If it is a deliberate deploy target, `updateInstead` is the intended mechanism rather than `ignore`. ## What a strong answer contains Name the invariant (working tree and index versus `HEAD`), name the setting and its default, describe what the alternative values trade away, and recommend a bare repository as the structural fix — mentioning `updateInstead` for the deliberate push-to-deploy case shows you know the modern option rather than only the old workaround of pushing to a detached side branch.

  • Which receive.denyCurrentBranch value makes the receiving working tree update along with the push?
    `updateInstead`. It accepts the push and updates the receiving repository's index and working tree to match the new commit, but only when that tree and index are clean — with local modifications it refuses rather than overwriting them. That clean-tree check is what distinguishes it from `ignore`, which merely lets the ref move and leaves the checkout stale.
  • Why is a bare repository the conventional shared remote?
    A bare repository has an object database and refs but no working tree and no checked-out branch, so no push can ever desynchronise files from HEAD. Any branch can be updated at any time. Create one with `git init --bare` or `git clone --bare`; the naming convention of a `.git` suffix on the directory is only a convention.
  • Can this error appear on a repository that has no obvious checkout of that branch?
    Yes, if linked worktrees are in play. `git worktree` gives one repository several working trees, each with its own HEAD on its own branch, and each of those branches is protected the same way. Run `git worktree list` in the receiving repository to see which branches are effectively checked out.

saying these in an interview costs you the question

  • Thinks non-bare repositories cannot receive pushes at all
  • Believes the push updates the receiver's files automatically
  • Suggests setting ignore as the normal fix
  • Confuses it with a permissions or transport error
  • Does not know linked worktrees are protected too

context