Qué es un git worktree: cómo funciona por dentro y cuándo usarlo
Qué es un git worktree explicado a fondo: qué guarda git en disco, qué comparte con el repositorio, sus comandos, sus límites y cuándo conviene usarlo.
Un git worktree es un directorio de trabajo adicional del mismo repositorio: una segunda carpeta con sus propios archivos, su propia rama activa y su propio staging, que comparte con la carpeta original el historial, las ramas y la configuración. Con él puedes tener main abierta en una carpeta y feature/login en otra al mismo tiempo, sin clonar el repositorio dos veces y sin hacer stash cada vez que cambias de tarea. El comando existe desde git 2.5 (2015), pero es fácil pasarlo por alto porque solo tiene sentido cuando sabes cómo está hecho un repositorio por dentro: qué se comparte entre carpetas y qué no.
Resumen para perezosos
- Un git worktree es otra carpeta de trabajo del mismo repositorio: tiene su propio
HEAD, su índice y sus archivos, pero comparte los commits, las ramas, la configuración y los hooks con el worktree principal. - Un git worktree vinculado no tiene una carpeta
.gitsino un archivo.gitde una línea que apunta a.git/worktrees/<nombre>, donde git guarda lo que es propio de ese worktree. Por eso crearlo es rápido y no duplica el historial. - Un git worktree tiene dos límites que conviene conocer: una rama solo puede estar activa en un worktree a la vez, y no copia lo que git no versiona (
node_modules,.env, builds).
En este artículo:
- Fundamentos — Qué es un git worktree · Las tres piezas de un repositorio · Worktree, clon, rama o stash · Cuándo usarlo
- Cómo funciona — Qué escribe git en disco · Qué es propio y qué se comparte · Un recorrido completo · Una rama, un worktree
- Uso — Los comandos · Lo que no copia · Desde un script · Limpieza y errores comunes
Qué es un git worktree
Un git worktree es un directorio de trabajo adicional del mismo repositorio, con su propia rama activa, su HEAD y su índice; los commits, las ramas y la configuración se comparten con el worktree principal.
El problema que resuelve es concreto. Un repositorio recién clonado tiene una sola carpeta de trabajo, y en esa carpeta solo puede estar activa una rama a la vez. Si estás a mitad de feature/payments y te piden revisar un bug en main, tienes tres opciones, y ninguna es cómoda: hacer un commit temporal, guardar los cambios con git stash, o clonar el repositorio otra vez en otra carpeta. Las dos primeras te obligan a interrumpir lo que estabas haciendo; la tercera duplica el historial completo y crea un repositorio aparte que no ve tus ramas locales.
git worktree agrega una cuarta opción: añadir una segunda carpeta de trabajo al mismo repositorio. En esa carpeta abres main, arreglas el bug y haces commit, mientras feature/payments sigue intacta en la carpeta original, con sus cambios a medias. Las dos carpetas leen y escriben en la misma base de datos de git, así que el commit que hiciste en una aparece de inmediato en la otra.
Para entender por qué eso funciona, y qué se comparte exactamente, hay que separar las piezas de un repositorio.
Las tres piezas de un repositorio git
Un repositorio git tiene tres piezas: la base de objetos (el historial), las referencias (las ramas y tags que apuntan a ese historial) y el estado de trabajo (la rama activa, el índice y los archivos de la carpeta).
| Pieza | Qué contiene | Dónde vive |
|---|---|---|
| La base de objetos | Todos los commits, árboles y contenidos de archivos del historial | .git/objects |
| Las referencias | Los nombres que apuntan a commits: ramas (refs/heads/main), tags, ramas remotas | .git/refs y .git/packed-refs |
| El estado de trabajo | Qué rama tienes activa (HEAD), qué preparaste para el próximo commit (el índice) y los archivos que ves en la carpeta | .git/HEAD, .git/index y el directorio de trabajo |
Las dos primeras piezas son el repositorio en sí: el historial y los nombres que apuntan a él. La tercera es tu posición dentro de ese historial: en qué rama estás, qué archivos tienes desplegados y qué cambios preparaste. Un repositorio normal tiene una sola copia de cada pieza, y por eso solo puedes estar en un lugar a la vez.
Un worktree duplica solo la tercera pieza. Cada worktree tiene su propio HEAD, su propio índice y su propia carpeta de archivos, pero todos comparten la base de objetos y las referencias.
Los worktrees tienen nombre según su origen. La carpeta donde clonaste o iniciaste el repositorio, la que contiene la carpeta .git de verdad, es el worktree principal (main worktree). Las que añades con git worktree add son worktrees vinculados (linked worktrees). Funcionan igual para el día a día; la diferencia es que el principal no se puede borrar ni mover con git worktree, porque es donde vive el repositorio.
Worktree, clon, rama o stash: en qué se diferencian
Un worktree se diferencia de un clon, de un checkout y de un stash en qué pieza toca: crea un segundo estado de trabajo sin duplicar el historial ni las referencias. Las otras tres opciones reemplazan el estado de trabajo o duplican el repositorio entero:
Rama + checkout | stash | Clon nuevo | Worktree | |
|---|---|---|---|---|
| Qué hace con el estado de trabajo | Lo reemplaza | Lo guarda y lo limpia | Crea otro, en otro repositorio | Crea otro, en el mismo repositorio |
| Varias ramas abiertas a la vez | No | No | Sí | Sí |
| Duplica el historial | No | No | Sí | No |
Un commit se ve en el otro sin push/fetch | — | — | No | Sí |
| Ve tus ramas locales | — | — | No, solo las que subiste con push | Sí, todas |
| Costo de crearlo | Casi nulo | Casi nulo | Copia el historial (un clon local usa hardlinks, pero sigue siendo otro repositorio) | Solo escribe los archivos de la rama |
La rama y el stash trabajan sobre tu única copia del estado de trabajo: la reemplazan o la vacían. El clon crea un estado de trabajo nuevo, pero dentro de un repositorio independiente, con su propia base de objetos y sus propias referencias; para que un commit pase de uno a otro necesitas push y fetch. El worktree crea un estado de trabajo nuevo dentro del mismo repositorio, así que obtienes la independencia de carpetas del clon sin perder el historial y las ramas compartidas.
Cuándo conviene usar un worktree
Un worktree conviene cuando necesitas dos estados de trabajo vivos a la vez sobre el mismo repositorio. Los casos más comunes:
| Situación | Por qué un worktree y no otra cosa |
|---|---|
| Un hotfix urgente a mitad de una feature | No tocas tu carpeta ni tus cambios a medias; arreglas en otra y vuelves |
| Revisar un pull request | Abres la rama del PR en su carpeta, la corres y la pruebas sin perder tu contexto |
| Comparar dos versiones | Abres el tag viejo con --detach y corres las dos a la vez |
| Una suite de tests o un build largo | Lo corres en un worktree mientras sigues programando en otro |
| Varios agentes de código en paralelo | Cada agente trabaja en su carpeta y su rama sin pisar los archivos de los otros |
El último caso es el más reciente. Herramientas como Orca crean un worktree por agente para correr varios a la vez sobre la misma tarea y quedarse con el mejor resultado; en el agente dentro de un repo real muestro cómo aislar una tarea de un agente con un worktree y entregarla como PR. En los dos casos el mecanismo es el que describe este post.
Y no conviene cuando el cambio de contexto es corto y no tienes cambios a medias (un checkout alcanza), o cuando necesitas aislar el repositorio entero, no solo el estado de trabajo: por ejemplo, probar algo que reescribe ramas o la configuración. Ahí un clon separado es más seguro, porque un worktree comparte todo eso.
Qué escribe git en disco cuando creas un worktree
Cuando creas un worktree, git escribe dos cosas: una carpeta de registro en .git/worktrees/<nombre> con el estado propio del worktree, y en la carpeta nueva un archivo .git de una línea que apunta a ese registro. Partimos de un repositorio en ~/app con un commit en main:
cd ~/app
git worktree add -b feature/login ../app-login
# Preparing worktree (new branch 'feature/login')
# HEAD is now at 0e4f78d primer commit
Ese comando hace cuatro cosas:
-
Crea la rama —
refs/heads/feature/loginApunta al commit actual, porque no le pasamos otro punto de partida.
-
Crea el registro interno —
.git/worktrees/app-login/Una carpeta dentro del
.gitprincipal con el estado propio del worktree nuevo. -
Crea la carpeta de trabajo —
../app-login/Con un archivo
.gitque apunta al registro interno. -
Despliega los archivos — checkout de
feature/loginEscribe en la carpeta nueva los archivos de esa rama y llena su índice.
Lo interesante está en los pasos 2 y 3. En la carpeta nueva no hay una carpeta .git, sino un archivo .git de una sola línea:
$ cat ../app-login/.git
gitdir: /home/ramon/app/.git/worktrees/app-login
Cuando ejecutas cualquier comando de git dentro de app-login, git busca .git, encuentra ese archivo y sigue la ruta. Así sabe que esa carpeta es parte del repositorio de ~/app. Del otro lado, el registro interno contiene pocos archivos:
$ ls .git/worktrees/app-login
HEAD ORIG_HEAD commondir gitdir index logs
| Archivo | Qué guarda |
|---|---|
HEAD | La rama activa de ese worktree: ref: refs/heads/feature/login |
index | El índice (staging) propio de ese worktree |
ORIG_HEAD | El commit anterior a la última operación que movió HEAD (aquí lo deja el checkout interno de add) |
logs/ | El reflog de su HEAD: a dónde apuntó y cuándo |
commondir | La ruta al repositorio compartido, relativa: ../.. |
gitdir | La ruta de vuelta al archivo .git de la carpeta de trabajo |
Es un vínculo en los dos sentidos: la carpeta de trabajo sabe dónde está su registro (archivo .git), y el registro sabe dónde está su carpeta (archivo gitdir). Gracias al segundo, git worktree list puede mostrar todas las carpetas y git worktree prune puede detectar las que ya no existen.
Git te deja ver esa separación directamente. Desde dentro del worktree vinculado:
cd ../app-login
# Dónde está el estado propio de este worktree
git rev-parse --git-dir
# /home/ramon/app/.git/worktrees/app-login
# Dónde está el repositorio compartido
git rev-parse --git-common-dir
# /home/ramon/app/.git
Fíjate en lo que no está en el registro: no hay objects, no hay refs/heads, no hay config. Todo eso se lee de --git-common-dir. Por eso crear un worktree tarda lo que tarda escribir los archivos de la rama, y no más: el historial ya existe y no se copia.
Qué es propio de cada worktree y qué se comparte
Cada worktree tiene para sí lo que forma su estado de trabajo, y comparte con los demás todo lo que forma el repositorio:
| Propio de cada worktree | Compartido por todos |
|---|---|
HEAD: qué rama o commit está activo | Los objetos: commits, árboles, contenidos (.git/objects) |
| El índice: lo que tienes en staging | Las ramas y tags (refs/heads, refs/tags) |
El reflog de HEAD | La configuración (.git/config) |
Operaciones en curso: un rebase, un merge, un cherry-pick | Los hooks (.git/hooks) |
El estado de un git bisect (refs/bisect) | El stash (refs/stash) |
| Los archivos del directorio de trabajo | Los remotos y las ramas remotas (refs/remotes) |
De esa tabla salen comportamientos que sorprenden la primera vez:
- Un
fetchen un worktree sirve para todos. Las ramas remotas son referencias compartidas, así que después de ungit fetchen cualquier carpeta, todas venorigin/mainactualizado. - Borrar una rama la borra para todos. No hay “ramas de este worktree”: hay ramas del repositorio, y cada worktree tiene una de ellas activa.
- El
stashes uno solo. Ungit stashhecho en un worktree aparece engit stash listde los demás y se puede aplicar desde cualquiera. - Los hooks y la configuración son los mismos. Todos los worktrees ejecutan el mismo hook de
pre-commit, y un cambio en.git/configlos afecta a todos. Si necesitas configuración distinta en uno, git lo permite activandoextensions.worktreeConfigy usandogit config --worktree. - Puedes hacer un
rebaseen un worktree mientras otro está a mitad de unmerge. Cada uno tiene su propio estado de operaciones en curso.
Un worktree aísla el estado de trabajo, no el repositorio. Los archivos a medio editar, el staging y la rama activa son de cada carpeta; los commits, las ramas, la configuración y los hooks son de todas.
Un recorrido completo: dos ramas abiertas a la vez
Con lo anterior, este es el flujo de principio a fin. Estás en ~/app, en main, y necesitas trabajar en el login sin tocar tu carpeta:
# 1. Crear el worktree con su rama
git worktree add -b feature/login ../app-login
# 2. Trabajar en él como en cualquier repositorio
cd ../app-login
echo "login" > login.txt
git add login.txt
git commit -m "Agrega login"
# 3. Volver a la carpeta original: el commit ya está ahí
cd ../app
git log --oneline --all
# b31849a (feature/login) Agrega login
# 0e4f78d (HEAD -> main) primer commit
# 4. Ver los dos worktrees y la rama de cada uno
git worktree list
# /home/ramon/app 0e4f78d [main]
# /home/ramon/app-login b31849a [feature/login]
En el paso 3 no hubo push ni fetch: el commit se escribió en la base de objetos compartida y la rama feature/login se actualizó en las referencias compartidas, así que ~/app lo ve en cuanto existe. Lo que ~/app no ve son los archivos: login.txt no aparece en su carpeta, porque esa carpeta sigue en main. Para integrar el trabajo haces lo mismo que con cualquier rama: git merge feature/login desde main, o un pull request.
Cuando terminas, borras el worktree:
git worktree remove ../app-login
git branch -d feature/login # la rama sobrevive al worktree; bórrala si ya se mergeó
La regla: una rama solo puede estar en un worktree
Hay una restricción que aparece pronto: git no deja tener la misma rama activa en dos worktrees a la vez. Si feature/login ya está abierta en ../app-login e intentas abrirla en otra carpeta, o hacer checkout de ella en la principal, falla:
$ git checkout feature/login
fatal: 'feature/login' is already used by worktree at '/home/ramon/app-login'
En versiones anteriores de git el mismo error dice fatal: 'feature/login' is already checked out at '…'.
La razón está en las piezas compartidas. La rama es una referencia del repositorio, y hacer commit en una carpeta mueve esa referencia. Si otra carpeta tuviera la misma rama activa, su HEAD apuntaría de pronto a un commit nuevo mientras sus archivos y su índice siguen en el anterior: un estado inconsistente. Git lo evita de entrada. Por el mismo motivo, tampoco te deja borrar una rama que está activa en algún worktree:
$ git branch -d feature/login
error: cannot delete branch 'feature/login' used by worktree at '/home/ramon/app-login'
Se puede saltar la regla con --force, pero no hay un caso normal donde convenga. Si necesitas dos carpetas partiendo del mismo punto, crea una rama distinta para cada una desde el mismo commit, o usa --detach para abrir un commit sin rama.
Los comandos de git worktree
Estos son los comandos de git worktree que cubren el uso diario:
# Crear un worktree con una rama nueva, partiendo de main
git worktree add -b feature/login ../app-login main
# Crear un worktree sobre una rama que ya existe
git worktree add ../app-hotfix hotfix/price
# Abrir un commit o un tag sin rama (útil para probar una versión vieja)
git worktree add --detach ../app-v1 v1.0.0
# Listar los worktrees con su commit y su rama
git worktree list
# Lo mismo en un formato estable, pensado para scripts
git worktree list --porcelain
# Borrar un worktree (se niega si tiene cambios sin commitear)
git worktree remove ../app-login
# Limpiar registros de worktrees cuya carpeta ya no existe
git worktree prune
# Proteger un worktree de prune (por ejemplo, si vive en un disco externo)
git worktree lock --reason "disco externo" ../app-v1
git worktree unlock ../app-v1
# Mover un worktree de carpeta sin romper el vínculo
git worktree move ../app-hotfix ../hotfixes/price
# Reparar los vínculos si moviste carpetas a mano
git worktree repair
Tres detalles de add que conviene saber:
- Si pasas solo la ruta, sin
-bni rama, git crea una rama nueva con el nombre de la última carpeta:git worktree add ../app-logindeja una rama llamadaapp-login. En un repositorio con convenciones (feature/,fix/), pasa siempre-b. - Si pasas el nombre de una rama remota que no existe en local (
git worktree add ../app-x feature/xcuando solo existeorigin/feature/x), git crea la rama local con seguimiento de la remota. - La carpeta va mejor al lado del repositorio (
../app-login) que dentro. Si la creas dentro, el worktree principal la ve como una carpeta sin versionar y tienes que añadirla a.gitignore.
Y uno de remove: borra la carpeta y el registro interno, pero no la rama. Los commits siguen en la rama, que puedes mergear o borrar después.
Un patrón que se ve seguido es combinar worktrees con un repositorio bare (sin carpeta de trabajo propia): clonas con git clone --bare <url> app.git y abres cada rama que necesitas como un worktree, por ejemplo git -C app.git worktree add ../main main. Así ninguna rama es “la principal” por estar en la carpeta del repositorio; todas son carpetas al mismo nivel.
Lo que un worktree no copia
Un worktree nuevo contiene exactamente lo que está versionado en la rama. Todo lo que git ignora o nunca vio se queda en la carpeta original. En un proyecto típico eso incluye:
- Dependencias (
node_modules,vendor,.venv): hay que instalarlas en cada worktree. Los gestores con caché global (el store depnpm, el caché denpmo depip) reducen el tiempo, pero cada carpeta necesita su instalación. - Variables de entorno (
.env,.env.local): no están en git, así que el worktree arranca sin ellas. Hay que copiarlas. - Artefactos de build (
dist,.next, cachés de compilación): empiezan vacíos, así que el primer build en cada worktree es completo. - Configuración local del editor que no esté versionada.
Y hay recursos que el worktree no aísla porque no son parte del repositorio:
- Puertos: si dos worktrees levantan el servidor de desarrollo en el puerto 3000, el segundo falla. Cada uno necesita su puerto.
- Bases de datos y servicios locales: dos worktrees apuntando a la misma base de datos de desarrollo comparten datos, y si corren migraciones distintas se interfieren.
- Submódulos: la documentación oficial de
git worktreeadvierte que el soporte para submódulos está incompleto. Cada worktree necesita su propiogit submodule update --init, ygit worktree removese niega a borrar un worktree que tiene submódulos inicializados salvo con--force.
“Crear un worktree” casi nunca es solo git worktree add: en un proyecto real también incluye los pasos para que la app arranque en esa carpeta.
Crear y listar worktrees desde un script
Cuando creas worktrees seguido, conviene juntar esos pasos en una función: crea el worktree sobre una rama, copia el .env del principal e instala las dependencias. Una segunda función lista los worktrees leyendo git worktree list --porcelain, que tiene un formato estable para scripts: un bloque por worktree, una línea clave valor por dato y una línea en blanco entre bloques.
// worktrees.ts — crear un worktree listo para trabajar y listar los existentes
import { execFileSync } from "node:child_process";
import { copyFileSync, existsSync } from "node:fs";
import { join } from "node:path";
type Worktree = { path: string; head: string; branch: string | null };
function git(repoDir: string, ...args: string[]): string {
return execFileSync("git", ["-C", repoDir, ...args], { encoding: "utf8" });
}
export function addWorktree(repoDir: string, branch: string, base = "main"): string {
// La carpeta va al lado del repositorio, con el nombre de la rama sin barras.
const path = join(repoDir, "..", branch.replaceAll("/", "-"));
git(repoDir, "worktree", "add", "-b", branch, path, base);
// Lo que git no versiona no viene en el worktree: el .env se copia a mano.
const env = join(repoDir, ".env");
if (existsSync(env)) copyFileSync(env, join(path, ".env"));
// Cada worktree necesita su propia instalación de dependencias.
execFileSync("npm", ["install"], { cwd: path, stdio: "inherit" });
return path;
}
export function listWorktrees(repoDir: string): Worktree[] {
// --porcelain: un bloque por worktree, separados por una línea en blanco.
const blocks = git(repoDir, "worktree", "list", "--porcelain").trim().split("\n\n");
return blocks.map((block) => {
const fields = new Map(
block.split("\n").map((line) => {
const [key, ...rest] = line.split(" ");
return [key, rest.join(" ")] as const;
}),
);
return {
path: fields.get("worktree") ?? "",
head: fields.get("HEAD") ?? "",
// Un worktree en modo detached no tiene línea "branch".
branch: fields.get("branch")?.replace("refs/heads/", "") ?? null,
};
});
}# worktrees.py — crear un worktree listo para trabajar y listar los existentes
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path
@dataclass
class Worktree:
path: str
head: str
branch: str | None
def git(repo_dir: str, *args: str) -> str:
return subprocess.check_output(["git", "-C", repo_dir, *args], text=True)
def add_worktree(repo_dir: str, branch: str, base: str = "main") -> str:
# La carpeta va al lado del repositorio, con el nombre de la rama sin barras.
path = Path(repo_dir).parent / branch.replace("/", "-")
git(repo_dir, "worktree", "add", "-b", branch, str(path), base)
# Lo que git no versiona no viene en el worktree: el .env se copia a mano.
env = Path(repo_dir) / ".env"
if env.exists():
shutil.copyfile(env, path / ".env")
# Cada worktree necesita su propia instalación de dependencias.
subprocess.run(["npm", "install"], cwd=path, check=True)
return str(path)
def list_worktrees(repo_dir: str) -> list[Worktree]:
# --porcelain: un bloque por worktree, separados por una línea en blanco.
blocks = git(repo_dir, "worktree", "list", "--porcelain").strip().split("\n\n")
result = []
for block in blocks:
fields = dict((line.split(" ", 1) + [""])[:2] for line in block.splitlines())
branch = fields.get("branch")
result.append(Worktree(
path=fields.get("worktree", ""),
head=fields.get("HEAD", ""),
# Un worktree en modo detached no tiene línea "branch".
branch=branch.removeprefix("refs/heads/") if branch else None,
))
return result<?php
// worktrees.php — crear un worktree listo para trabajar y listar los existentes
function run(string $cwd, string ...$cmd): string {
$parts = implode(" ", array_map("escapeshellarg", $cmd));
exec("cd " . escapeshellarg($cwd) . " && $parts 2>&1", $output, $code);
if ($code !== 0) {
throw new RuntimeException(implode("\n", $output));
}
return implode("\n", $output);
}
function addWorktree(string $repoDir, string $branch, string $base = "main"): string {
// La carpeta va al lado del repositorio, con el nombre de la rama sin barras.
$path = dirname($repoDir) . "/" . str_replace("/", "-", $branch);
run($repoDir, "git", "worktree", "add", "-b", $branch, $path, $base);
// Lo que git no versiona no viene en el worktree: el .env se copia a mano.
if (file_exists("$repoDir/.env")) {
copy("$repoDir/.env", "$path/.env");
}
// Cada worktree necesita su propia instalación de dependencias.
run($path, "npm", "install");
return $path;
}
function listWorktrees(string $repoDir): array {
// --porcelain: un bloque por worktree, separados por una línea en blanco.
$blocks = explode("\n\n", trim(run($repoDir, "git", "worktree", "list", "--porcelain")));
return array_map(function (string $block): array {
$fields = [];
foreach (explode("\n", $block) as $line) {
[$key, $value] = array_pad(explode(" ", $line, 2), 2, "");
$fields[$key] = $value;
}
return [
"path" => $fields["worktree"] ?? "",
"head" => $fields["HEAD"] ?? "",
// Un worktree en modo detached no tiene línea "branch".
"branch" => isset($fields["branch"]) ? str_replace("refs/heads/", "", $fields["branch"]) : null,
];
}, $blocks);
}El ejemplo usa npm, pero el paso es el mismo con cualquier gestor: lo que cambia es el comando de instalación. Si tu proyecto levanta un servidor, el siguiente paso natural es que la función también le asigne un puerto distinto a cada worktree.
Limpieza y errores comunes
Los worktrees se acumulan si no los borras al terminar. Estos son los problemas que aparecen con más frecuencia:
| Síntoma | Causa | Qué hacer |
|---|---|---|
is already used by worktree at … (o is already checked out at …) | La rama ya está activa en otro worktree | Usa otra rama, o borra el worktree que la tiene |
git worktree list muestra carpetas que ya no existen | Borraste la carpeta con rm -rf en vez de git worktree remove | git worktree prune limpia esos registros |
remove se niega a borrar | El worktree tiene cambios sin commitear o archivos sin versionar | Commitea lo que sirva; si no, git worktree remove --force |
| Quedan ramas de worktrees ya borrados | remove borra la carpeta, no la rama | git branch -d <rama> (o -D si no se mergeó) |
| El worktree dejó de funcionar tras mover el repositorio | El vínculo usa rutas absolutas por defecto (desde git 2.48 se pueden usar relativas con worktree.useRelativePaths) | git worktree repair reescribe los vínculos |
Sobre prune: también corre solo, como parte de git gc, y borra los registros de worktrees cuya carpeta desapareció hace más de tres meses (el valor por defecto de gc.worktreePruneExpire). Si un worktree vive en un disco que no siempre está conectado, git worktree lock evita que lo borre.
La regla que evita casi todos estos problemas: borrar siempre con git worktree remove, nunca con rm -rf, y borrar la rama en el mismo momento si ya no la necesitas.
Preguntas frecuentes
¿Qué diferencia hay entre un git worktree y un git clone?
Un clon es un repositorio independiente, con su propia base de objetos y sus propias ramas; para pasar commits de un clon a otro necesitas push y fetch. Un worktree es otra carpeta de trabajo del mismo repositorio: comparte el historial, las ramas y la configuración, así que un commit hecho en un worktree se ve al instante en los demás. El worktree es más rápido de crear y ocupa menos espacio, porque no duplica el historial.
¿Qué es la carpeta .git/worktrees?
La carpeta .git/worktrees es donde git guarda el estado propio de cada worktree vinculado: una subcarpeta por worktree con su HEAD, su índice, su reflog y dos archivos de vínculo (commondir y gitdir). La carpeta de trabajo del worktree no tiene una carpeta .git sino un archivo .git que apunta a esa subcarpeta. No conviene editarla a mano: se administra con git worktree.
¿Puedo tener la misma rama abierta en dos worktrees?
No. Git impide que una rama esté activa en dos worktrees a la vez y falla con el mensaje “is already used by worktree at”. La regla existe porque un commit en un worktree movería la rama y dejaría al otro con archivos que no coinciden con su HEAD. Si necesitas dos carpetas desde el mismo punto, crea una rama distinta para cada una o usa --detach.
¿Cuándo usar git worktree en lugar de git stash?
Git stash conviene cuando la interrupción es corta: guardas los cambios, cambias de rama y los recuperas en la misma carpeta. Git worktree conviene cuando necesitas las dos ramas vivas a la vez, por ejemplo para correr la app o los tests de una mientras sigues editando la otra, porque cada worktree tiene sus propios archivos y su propio índice y no hay que guardar ni restaurar nada.
¿Un worktree copia node_modules y el archivo .env?
No. Un worktree nuevo contiene solo lo que está versionado en la rama. Las dependencias (node_modules, .venv, vendor), los archivos .env y los artefactos de build no se copian, así que hay que instalarlos o copiarlos en cada worktree. Los gestores con caché global, como pnpm, reducen el tiempo de instalación.
¿Cómo borro un git worktree correctamente?
Un git worktree se borra con git worktree remove <ruta>, que borra la carpeta y el registro interno en .git/worktrees. Si ya borraste la carpeta a mano, git worktree prune limpia el registro huérfano. En los dos casos la rama sigue existiendo: bórrala con git branch -d cuando ya no la necesites.
Conclusión
Para probarlo: crea un worktree con git worktree add -b para tu próxima tarea, mira el archivo .git y la carpeta .git/worktrees que dejó, haz un commit y comprueba que lo ves desde la carpeta original. Después anota qué te faltó para que la app arrancara ahí y automatiza esos pasos.