Guía9 min

git update-ref para coding agents: plumbing de refs, no mover HEAD

Resumen

git update-ref escribe el SHA de una ref con compare-and-swap. Dos args mueven HEAD sin checkout y dejan el working tree atrás. Tres args exigen old-oid. Cero autónomo. Cero -d, --stdin o --no-deref. Distinto de branch, symbolic-ref y reset. Git 2.50.1.

GitHub
Un candado compare-and-swap sobre una ref Git; el working tree no se mueve al escribir el SHA

Qué resuelve

Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.

git update-ref actualiza de forma segura el object name guardado en una ref. El man (git-update-ref(1); git-scm.com/docs/git-update-ref HTTP 200, last-modified 2026-08-31; pie del man local Git 2.50.1.428.g0e8243, 2025-07-22; binario Git 2.50.1 / Apple Git-155): Update the object name stored in a ref safely. SYNOPSIS: git update-ref [-m <reason>] [--no-deref] -d <ref> [<old-oid>] · git update-ref [-m <reason>] [--no-deref] [--create-reflog] <ref> <new-oid> [<old-oid>] · git update-ref [-m <reason>] [--no-deref] --stdin [-z] [--batch-updates].

Es plumbing. No lista. No hace checkout. No toca el índice ni el working tree. Solo escribe (o borra) el puntero. Por eso es más peligroso que branch: git branch feat crea una ref y te deja donde estás. git update-ref HEAD <oid> mueve la rama actual y el árbol se queda en el commit anterior. El man lo dice: Given two arguments, stores the <new-oid> in the <ref>, possibly dereferencing the symbolic refs. E.g. git update-ref HEAD <new-oid> updates the current branch head to the new object.

No es symbolic-ref. Ese comando lee o reescribe punteros simbólicos (ref: refs/heads/…). update-ref sin opciones does not support updating a symbolic ref to point to another ref. Para eso el man remite a git-symbolic-ref(1). --stdin sí trae symref-*; un agente no entra ahí.

Tampoco es reset. reset mueve HEAD y, según el modo, el index y el disco. update-ref solo mueve el puntero. El working tree queda detrás del tip. Verificado abajo: dos args sobre HEAD dejan M en el archivo y status en la misma rama.

Ni show-ref ni rev-parse: esos leen. update-ref escribe.

GitHub Docs (REST API endpoints for Git references, HTTP 200 2026-09-06): A Git reference (git ref) is a file that contains a Git commit SHA-1 hash. Get a reference pide Contents read. Create / Update / Delete a reference piden Contents write. Eso es github.com, no el clone local. Un POST a /git/refs no sustituye update-ref en el worktree del humano.

Contrato: no corras git update-ref autónomo. Crear ramas = branch. Leer HEAD = symbolic-ref --quiet o rev-parse --verify --quiet. Mover el árbol = switch. Si el humano pide un compare-and-swap en una ref que no es HEAD: tres args, old-oid real, -m corto. Cero -d. Cero --stdin. Cero --no-deref. Cero cuarenta ceros salvo que pida crear.

Dos args escriben; tres args verifican

El man parte el contrato en dos formas (más -d y --stdin):

FormaQué haceAgente
<ref> <new-oid>Escribe. Sigue refs simbólicas. HEAD = mueve la rama actualCero
<ref> <new-oid> <old-oid>Igual, solo si el valor actual es <old-oid>Solo si el humano nombra la ref y el SHA viejo
40 0 o string vacío como <old-oid>make sure that the ref you are creating does not existCero autónomo
-d <ref> [<old-oid>]Borra tras verificar <old-oid>Cero
--no-derefPisa <ref> misma, no el destino del symlink simbólicoCero
--stdin / -z / --batch-updatesTransacción o lote desde stdinCero
--create-reflogCrea reflog aunque no tocaraCero
-m <reason>Mensaje en el reflog (TAB + texto)OK si ya vas a escribir

Tres args es compare-and-swap. El man: stores the <new-oid> in the <ref> … after verifying that the current value of the <ref> matches <old-oid>. Si otro proceso ganó, fallas sin escribir. Eso es el único motivo para existir este comando frente a echo SHA > .git/refs/heads/feat.

Cuarenta ceros como <old-oid> no es “reset a empty”. Es “esta ref no debe existir”. Verificado: con refs/heads/feat ya creada, update-ref refs/heads/feat <HEAD> 0000…0000128, cannot lock ref … reference already exists. Al revés, zeros + ref inexistente → 0 (crea).

-d deletes the named <ref> after verifying that it still contains <old-oid>. Sin old-oid también borra (verificado: 0). Un agente no borra refs. Para ramas usa branch -d solo si --merged; nunca -D.

Compare-and-swap: el SHA nuevo solo entra si el actual coincide

--stdin es una transacción, no un atajo

Con --stdin el man acepta update / create / delete / verify y las variantes symref-*, más start / prepare / commit / abort. If all <ref>s can be locked with matching <old-oid>s simultaneously, all modifications are performed. Otherwise, no modifications are performed. Un lote mal armado no “aplica lo que pueda”: o todo o nada. --batch-updates allows individual updates to fail due to invalid or incorrect user input y reporta rejected; errores de I/O tiran el lote entero. -z usa NUL. start sin commit aborta y puede dejar locks. Cero stdin.

Verificado 2026-09-06 (Git 2.50.1 / Apple Git-155). Repo mínimo, dos commits:

  • git update-ref refs/heads/feat <HEAD>: 0. show-ref --heads lista feat y main.
  • Tres args con 40 ceros sobre feat existente: 128, reference already exists. Con old-oid real: 0.
  • -d refs/heads/feat (sin old-oid): 0. La ref desaparece.
  • Dos args update-ref HEAD <commit-viejo>: 0. rev-parse HEAD cambia; status -sb sigue ## main con M f. El árbol no viajó.
  • printf 'create refs/heads/tmp %s\n' $oid | git update-ref --stdin: 0. Un agente no lo usa como API.

Nombres: check-ref-format --branch antes. feat corto en update-ref no es el path; el man espera <ref> (refs/heads/feat). No adivines el prefijo.

Por qué un agente no “actualiza la ref para ayudar”

Porcelain (branch, tag, switch, commit) ya escribe refs con reflog, hooks y locks. update-ref es el primitivo de debajo. Dos args sobre HEAD es un reset --soft sin ORIG_HEAD de cortesía y sin que el humano lo pida. --no-deref sobre HEAD pisa el archivo simbólico: HEAD deja de apuntar a refs/heads/main y pasa a ser un SHA suelto (detached disfrazado). El man: If --no-deref is given, <ref> itself is overwritten, rather than the result of following the symbolic pointers.

Logging: si core.logAllRefUpdates es true (heads/remotes/notes o pseudorefs) o ya existe $GIT_DIR/logs/<ref>, appende oldsha1 SP newsha1 SP committer. Con -m, TAB + mensaje. An update will fail (without changing <ref>) si no hay ident o no puede escribir el log. Un sandbox sin ident no se “arregla” con --create-reflog.

No leas ni escribas .git/HEAD, .git/refs/* ni packed-refs a mano. El man de show-ref existe precisamente para no parsear esos archivos. Tampoco force-push: mover la ref local no publica; git push --force es otro verbo y está vetado.

HEAD simbólico vs SHA directo: --no-deref pisa el puntero, no la rama

Qué sí / no corre

PreguntaComandoProhibido
¿Existe la ref local?show-ref --verify --quiet refs/heads/feat (0/1)update-ref -d para “probar”
¿SHA de HEAD?rev-parse --verify --quiet HEADupdate-ref HEAD …
¿A qué rama apunta HEAD?symbolic-ref --quiet HEAD--no-deref sobre HEAD
¿Crear rama?git branch featupdate-ref refs/heads/feat <oid>
¿Entrar a la rama?switchdos args sobre HEAD
¿CAS que el humano pidió?git update-ref -m "…" refs/heads/feat <nuevo> <viejo>sin <old-oid>; zeros; otra ref
¿Borrar rama?git branch -d si --merged-d de update-ref; -D
¿Refs en GitHub?REST Get a reference (Contents read)REST write; --stdin local “para sincronizar”

FAQ

¿Puedo usarlo para “arreglar” un detached HEAD? No. Detached se sale con switch a una rama. update-ref HEAD <oid> en detached escribe el SHA en HEAD; no te reatacha.

¿--batch-updates hace el lote más seguro? No para un agente. Permite que parte del stdin aplique. El man reporta rejected. Eso es estado a medias. Cero.

¿Cuarenta ceros como <new-oid> borra? En el dialecto --stdin, Specify a zero <new-oid> to ensure the ref does not exist after the update. En la forma posicional, el borrado es -d. Ninguno es autónomo.

¿Es lo mismo que git branch -f? -f / -M en branch también mueven una ref existente. Están vetados en branch. update-ref es el mismo movimiento sin el porcelain. Peor, no mejor.

Checklist

  1. No invoques git update-ref salvo que el humano nombre la ref, el SHA nuevo y el viejo.
  2. Nunca HEAD como <ref>. Nunca --no-deref. Nunca -d. Nunca --stdin.
  3. Nombres con check-ref-format --branch; path completo refs/heads/….
  4. Confirma el valor actual con show-ref --verify antes del CAS.
  5. No sustituyas reset, switch ni REST de GitHub.
  6. Si el ident o el reflog fallan, para. No inventes --create-reflog.

Hoy: si tu agente “crea ramas” con update-ref, cámbialo a git branch <nombre>. Si mueve HEAD con dos args, es un reset blando no pedido: quítalo. El resto del flujo de refs está en el hub de construir agentes y, para convenciones de repo, en AGENTS.md.