🌱 Empieza aquí · Git sin pisarse entre terminales
Cuando abres dos o más terminales (o sesiones de tu asistente) sobre el mismo proyecto, una puede llevarse sin querer el trabajo de la otra. Esta semilla le enseña a tu asistente tres cuidados con git (la herramienta que guarda el historial de tu proyecto):
- Guardar solo lo suyo. Un "agregar al commit" selectivo que nunca guarda un archivo borrado sin tu aprobación y que deja fuera lo que ya estaba a medias cuando arrancó.
- No esconder lo ajeno. Un candado que le niega
git stashy--autostash(dos comandos que "apartan" los cambios sin guardar) si con eso se llevaría cambios de otra sesión. - Avisar si deshizo algo. Un vigía que, justo después de un commit, avisa si ese commit regresó un archivo a como estaba antes del commit de otra sesión de los últimos 15 minutos.
Para instalarla
Abre esta carpeta con Claude Code y dile a tu asistente:
"Siembra esta semilla 🌱"
Con eso basta de tu parte. Tu asistente hace todo lo técnico y al final te dice cómo comprobar que quedó funcionando. Vas a ver algunas palabras en inglés en la pantalla; ni caso, él te guía.
¿Otras formas de pedírselo? "instala esta semilla", "pon a funcionar el candado de git", "haz el setup de esto". Te entiende igual.
Para el asistente que siembra 🤖
Esta semilla es un hook (gancho de automatización: un script que Claude Code corre solo
antes o después de cada herramienta). Trae tres piezas en kernel/:
| Archivo | Qué es | Cómo se activa |
|---|---|---|
| guardia-autostash.py | Candado PreToolUse (antes de ejecutar) del Bash. Niega con exit 2 un git stash o --autostash que apartaría cambios ajenos. | Se registra en settings.json. |
| commit-deshace-ajeno-aviso.py | Vigía PostToolUse (después de ejecutar) del Bash. Tras un git commit, avisa si deshizo un commit de otra sesión. Nunca bloquea. | Se registra en settings.json. |
| git-safe-add.sh | Biblioteca de bash con git_safe_add y git_write_baseline. Reemplaza a git add -A en los scripts que guardan solos. | No se registra: se carga con source desde el script que commitea. |
Además: settings.ejemplo.json (el registro de los dos hooks), turnos.ejemplo.json (el
formato opcional del registro de turnos) y pruebas/ (tres bancos de prueba con repos de
juguete).
Requisitos: bash, git (2.18 o más nuevo) y python3 (3.8 o más nuevo). No instales
nada que falte sin preguntarle a la persona.
1. Copia el kernel a la carpeta de hooks
Corre esto desde la carpeta de esta semilla (macOS, Linux, o Windows con WSL / Git Bash):
SEMILLA="$(pwd)"
DESTINO="$HOME/.claude/hooks/git-sin-pisarse"
mkdir -p "$DESTINO"
cp "$SEMILLA/kernel/git-safe-add.sh" \
"$SEMILLA/kernel/guardia-autostash.py" \
"$SEMILLA/kernel/commit-deshace-ajeno-aviso.py" "$DESTINO/"
mkdir -p "$DESTINO/pruebas"
cp "$SEMILLA/kernel/pruebas/"*.sh "$DESTINO/pruebas/"
chmod +x "$DESTINO/"*.py "$DESTINO/"*.sh "$DESTINO/pruebas/"*.sh
Windows sin WSL ni Git Bash: no está soportado. git-safe-add.sh necesita bash, y los
dos scripts de Python comparan rutas con /. Dile a la persona que use WSL.
2. Registra los dos hooks en settings.json
Abre $HOME/.claude/settings.json (créalo si no existe). Suma estas dos entradas a lo
que ya haya en hooks.PreToolUse y hooks.PostToolUse; no reemplaces los hooks que la
persona ya tenga. El contenido exacto está en kernel/settings.ejemplo.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command",
"command": "python3 \"$HOME/.claude/hooks/git-sin-pisarse/guardia-autostash.py\"",
"timeout": 20 }
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command",
"command": "python3 \"$HOME/.claude/hooks/git-sin-pisarse/commit-deshace-ajeno-aviso.py\"",
"timeout": 20 }
]
}
]
}
}
Muéstrale a la persona el cambio antes de guardarlo. Los hooks se cargan al abrir una sesión nueva de Claude Code.
3. (Opcional) Usa git_safe_add en los scripts que guardan solos
Si la persona tiene un script que commitea por su cuenta (al cerrar sesión, un respaldo
automático, una sincronización), cambia su git add -A por esto. Dos reglas del helper:
corre bajo bash (no zsh) y no se captura con $(...), porque los resultados salen
en variables y una subshell los perdería. Guarda el baseline fuera del repo.
Son dos momentos distintos. Si el baseline se escribiera al guardar, todo lo sucio quedaría como "de antes" y no se guardaría nada.
Al ARRANCAR la sesión (por ejemplo, en un hook SessionStart o al inicio del script):
anota lo que ya estaba sucio, que no es de esta sesión.
source "$HOME/.claude/hooks/git-sin-pisarse/git-safe-add.sh"
REPO="<TU-RUTA>/mi-repo"
BASELINE="${TMPDIR:-/tmp}/baseline-$(basename "$REPO")-${CLAUDE_CODE_SESSION_ID:-manual}.txt"
cd "$REPO" || exit 1
git_write_baseline "$BASELINE"
Al GUARDAR (en lugar del git add -A): agrega solo modificados y nuevos que no estaban
en el baseline.
source "$HOME/.claude/hooks/git-sin-pisarse/git-safe-add.sh"
REPO="<TU-RUTA>/mi-repo"
BASELINE="${TMPDIR:-/tmp}/baseline-$(basename "$REPO")-${CLAUDE_CODE_SESSION_ID:-manual}.txt"
cd "$REPO" || exit 1
git_safe_add "$BASELINE"
echo "agregados=$GIT_SAFE_ADDED borrados-frenados=$GIT_SAFE_SKIPPED_DELETES movidos=$GIT_SAFE_MOVED fuera-por-baseline=$GIT_SAFE_SKIPPED_BASELINE"
if [ -n "$GIT_SAFE_ADD_ERROR" ]; then echo "el add falló: $GIT_SAFE_ADD_ERROR"; fi
git diff --cached --name-status
if [ "$GIT_SAFE_ADDED" -gt 0 ]; then git commit -m "Guarda el trabajo de esta sesión"; fi
Ojo con lo que ya estaba en stage. git_safe_add decide lo que él agrega, pero no
vacía el índice (el "stage", la lista de lo que entra al siguiente commit). Si alguien ya
había puesto algo ahí (un git rm, o un git add de otra terminal), el git commit también
se lo lleva, borrados incluidos, y GIT_SAFE_ADDED lo cuenta. Por eso el bloque imprime
git diff --cached --name-status justo antes del commit: queda en la salida como registro
de qué entró (una D es un borrado). Si la persona prefiere revisar antes de guardar, quita
la última línea del bloque y que el commit se haga a mano después de leer esa lista.
Si el archivo de baseline no existe, git_safe_add no excluye nada por baseline (los
borrados siguen frenados igual). Reemplaza <TU-RUTA>/mi-repo por la carpeta real del
proyecto. Los borrados frenados quedan anotados (una línea por ruta y por día) en
$HOME/.claude/git-sin-pisarse/deletes-detectados.log, y los add fallidos en
git-add-fallidos.log de la misma carpeta. Para mover esos logs, exporta
GIT_SAFE_LOG_DIR=<TU-RUTA> antes de llamar a git_safe_add.
4. (Opcional) Registro de turnos: decir qué rutas son de esta sesión
Sin registro, el candado trata todo cambio sin guardar como ajeno y niega el stash si
el árbol tiene cualquier cambio. Es el lado seguro. Si la persona quiere que una sesión
pueda hacer stash de sus archivos, el candado lee (solo lectura) un registro de turnos
en $HOME/.claude/git-sin-pisarse/turnos.json (o en la ruta de la variable
GIT_SIN_PISARSE_TURNOS). Formato, como en kernel/turnos.ejemplo.json:
session: el id de sesión que Claude Code manda al hook.paths: archivos o carpetas de ese turno.expires_at: hasta cuándo vale, en segundos desde 1970 (epoch). Un turno vencido no protege.
Esta semilla no trae una herramienta para escribir ese registro: se escribe a mano o con otra herramienta de la persona.
5. (Opcional) El renglón Claude-Session en los commits
El vigía distingue sesiones por un renglón Claude-Session: <id> en el mensaje del commit.
Si dos commits lo traen con el mismo valor, son de la misma sesión y no avisa. Si alguno no
lo trae, cuenta como ajeno: sin ese renglón, también va a avisar cuando una sesión revierte
su propio commit de hace menos de 15 minutos.
6. Verificación post-siembra (obligatoria)
a) Los archivos quedaron en su lugar:
DESTINO="$HOME/.claude/hooks/git-sin-pisarse"
ls -la "$DESTINO/git-safe-add.sh" "$DESTINO/guardia-autostash.py" "$DESTINO/commit-deshace-ajeno-aviso.py"; echo "exit=$?"
grep -c 'git-sin-pisarse' "$HOME/.claude/settings.json"
Salida esperada: tres líneas con los archivos, exit=0, y un 2 (las dos entradas
registradas). Si sale No such file or directory, repite el paso 1; si sale 0, el paso 2.
b) El candado niega de verdad (repo de juguete, evento simulado, nada real se toca):
GUARDIA="$HOME/.claude/hooks/git-sin-pisarse/guardia-autostash.py"
R="$(mktemp -d)"
git -C "$R" init -q
printf 'uno\n' > "$R/a.txt"
git -C "$R" add a.txt
git -C "$R" -c user.name=prueba -c user.email=prueba@local commit -qm base
printf 'cambio de otra terminal\n' >> "$R/a.txt"
printf '{"tool_name":"Bash","session_id":"prueba","cwd":"%s","tool_input":{"command":"git stash"}}' "$R" \
| GIT_SIN_PISARSE_TURNOS=/nonexistent python3 "$GUARDIA"; echo "EXIT=$?"
printf '{"tool_name":"Bash","session_id":"prueba","cwd":"%s","tool_input":{"command":"git stash list"}}' "$R" \
| GIT_SIN_PISARSE_TURNOS=/nonexistent python3 "$GUARDIA"; echo "EXIT=$?"
rm -rf "$R"
Salida esperada: primero un mensaje [guardia-autostash] NEGADO: ... a.txt ... con
EXIT=2; después, sin mensaje, EXIT=0 (git stash list no aparta nada).
c) Los tres bancos de prueba en verde (cada uno arma sus repos en una carpeta temporal):
P="$HOME/.claude/hooks/git-sin-pisarse/pruebas"
bash "$P/banco-git-safe-add.sh" | tail -1
bash "$P/banco-autostash.sh" | tail -1
bash "$P/banco-commit-deshace-ajeno.sh" | tail -1
Salida esperada: BANCO VERDE (12/12), TODOS EN VERDE (43 casos, 0 fallos) y
bien=12 mal=0.
Cuando las tres partes den lo esperado, avísale a la persona que ya quedó y explícale en una línea qué gana con esto.
✅ ¿Cómo sé que funcionó?
Si tu asistente intenta git stash (o un pull con --autostash) mientras otra terminal
tiene cambios sin guardar en el mismo proyecto, el candado lo frena y le dice la salida
segura: guardar solo sus archivos con git add -- <archivos> y git commit -- <archivos>,
o traer lo del servidor con git fetch y git merge origin/<rama> --no-edit.
Si un commit regresa un archivo a como estaba antes del commit de otra sesión de hace menos
de 15 minutos, vas a ver un aviso ⚠️ COMMIT QUE DESHACE OTRO con el archivo y el commit
afectado. Solo avisa: tú decides si fue a propósito.
Y si conectaste git_safe_add a tus scripts de guardado, el script ya no mete por su cuenta
ningún borrado (salvo un movimiento cuyo contenido exacto ya está en otra ruta) ni lo que
estaba a medias al arrancar, y cada borrado frenado queda anotado en el log. Lo que alguien
ya había puesto en stage a mano sí entra al commit: revísalo en la lista que el bloque
muestra antes de guardar. Eso es todo. 🌱
