No metas tus cosas en el repo de otro: Skills, Submodules y NixOS


24 de febrero de 2026

Hoy casi perdemos trabajo. No por un bug, no por un disco duro, sino por algo mucho mas estupido: meter nuestros archivos dentro de un git submodule de terceros.

Este post es la autopsia de esa cagada y como NixOS nos ayudo a arreglarla de forma declarativa y permanente.

El escenario

Claude Code usa "skills" – archivos SKILL.md que le dan capacidades especializadas. Hay un repositorio comunitario llamado antigravity con 550+ skills (desde bash-pro hasta kubernetes-architect).

Nosotros lo anadimos como submodule de git:

git submodule add https://github.com/anthropics/antigravity skills/antigravity

Y en nuestro modulo NixOS de home-manager, un symlink inocente:

ln -sf "${dotfilesDir}/skills/antigravity/skills" ~/.claude/skills

Claude Code busca skills en ~/.claude/skills/. El symlink apunta al submodule. 550 skills disponibles. Todo bien.

El error

Con el tiempo creamos nuestras propias skills: /dejavu (protocolo de recuperacion de contexto), /checkup (auditoria del sistema), /alacama (gestion de sesiones), /preteleport (preparar maquina antes de saltar).

Y las creamos… dentro del submodule.

~/.claude/skills -> ~/dotfiles/skills/antigravity/skills/
                    ├── 3d-web-experience/   <- community
                    ├── bash-pro/            <- community
                    ├── dejavu/              <- NUESTRO (pero dentro de antigravity!)
                    ├── checkup/             <- NUESTRO (pero dentro de antigravity!)
                    └── ... (550+ mas)

Como ~/.claude/skills era un symlink al directorio del submodule, cualquier archivo creado ahi iba a parar al submodule. Y los commitimos ahi. Tres commits nuestros flotando dentro de un repositorio ajeno.

La bomba de relojeria

Esto funciona. Hasta que no funciona.

Un git submodule update --remote borra todo. Un git submodule sync lo resetea. Clonar el repo en otra maquina trae el submodule limpio, sin nuestras skills.

Nuestro trabajo existia en exactamente UN sitio: el directorio local. Sin backup, sin push (no tenemos permisos en el repo de antigravity), sin versionado real.

Lo peor: git status en el repo padre solo muestra "skills/antigravity (modified content)" sin decirte QUE hay modificado. Parece un cambio trivial. Es una bomba.

El diagnostico

# El symlink asesino
$ ls -la ~/.claude/skills
lrwxrwxrwx skills -> /home/passh/dotfiles/skills/antigravity/skills

# Commits nuestros DENTRO del submodule
$ git -C skills/antigravity log --oneline -5
ed9963e Add Syncthing check to /dejavu protocol (step 3)  <- NUESTRO
79d3769 Add /checkup system health audit skill             <- NUESTRO
c586296 skills: /dejavu, /alacama, /preteleport            <- NUESTRO
4e1ba66 chore: sync generated registry files [ci skip]     <- upstream
cc90528 docs: add evandro-miguel (PR #41)                  <- upstream

Tres commits que nunca se podrian pushear. Tres commits que se borrarian al actualizar el submodule. El git log del repo padre ni los muestra.

La solucion: separar fuentes, mergear con NixOS

Paso 1: Crear nuestro directorio de skills

mkdir -p ~/dotfiles/skills/ambrosio/
cp -r skills/antigravity/skills/dejavu    skills/ambrosio/
cp -r skills/antigravity/skills/checkup   skills/ambrosio/
cp -r skills/antigravity/skills/alacama   skills/ambrosio/
cp -r skills/antigravity/skills/preteleport skills/ambrosio/

Ahora tenemos:

~/dotfiles/skills/
├── ambrosio/          # NUESTRO - tracked en nuestro repo
│   ├── dejavu/SKILL.md
│   ├── checkup/SKILL.md
│   ├── alacama/SKILL.md
│   └── preteleport/SKILL.md
└── antigravity/       # Submodule de terceros - no tocar
    └── skills/
        ├── bash-pro/SKILL.md
        ├── kubernetes-architect/SKILL.md
        └── ... (550+ community skills)

Paso 2: Revertir el submodule

git -C skills/antigravity reset --hard 4e1ba66

Limpio. Solo commits de upstream. Nuestras skills ya estan a salvo en skills/ambrosio/.

Paso 3: El merge declarativo en NixOS

Aqui es donde NixOS brilla. En vez de un symlink directo a una fuente, creamos un directorio real con symlinks a AMBAS fuentes. Nuestras skills tienen prioridad:

home.activation.setupClaudeCode = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
  # Si existe el symlink viejo, eliminarlo
  if [[ -L ~/.claude/skills ]]; then
    rm ~/.claude/skills
  fi
  mkdir -p ~/.claude/skills

  # Primero: community skills (antigravity)
  for d in "${dotfilesDir}"/skills/antigravity/skills/*/; do
    name=$(basename "$d")
    if [[ ! -e ~/.claude/skills/$name ]]; then
      ln -sf "$d" ~/.claude/skills/$name
    fi
  done

  # Segundo: nuestras skills (ambrosio) - SOBREESCRIBEN si hay conflicto
  for d in "${dotfilesDir}"/skills/ambrosio/*/; do
    name=$(basename "$d")
    ln -sf "$d" ~/.claude/skills/$name
  done
'';

El orden importa:

  1. Las community se linkan primero, con if [[ ! -e ]] (no sobreescribir)
  2. Las nuestras se linkan despues, con ln -sf (sobreescribir siempre)

Si manana creamos un /bash-pro propio que sea mejor que el de la comunidad, automaticamente gana el nuestro.

Paso 4: El resultado

$ ls ~/.claude/skills/ | wc -l
557

$ readlink ~/.claude/skills/dejavu
/home/passh/dotfiles/skills/ambrosio/dejavu/     # NUESTRO

$ readlink ~/.claude/skills/bash-pro
/home/passh/dotfiles/skills/antigravity/skills/bash-pro/  # community

557 skills. 4 nuestras, 553 community. Cada una apuntando a su fuente real. Un nixos-rebuild lo recrea identico en cualquier maquina.

Por que activation scripts y no home.file

NixOS tiene home.file para crear archivos/symlinks declarativamente. Pero tiene un problema: los symlinks apuntan al Nix store (/nix/store/xxx-source), que es read-only. Si Claude Code intenta escribir en esos paths, falla.

Los activation scripts de home-manager se ejecutan DESPUES del deploy y crean symlinks normales del filesystem, editables, que apuntan a tu directorio real de dotfiles.

Es la diferencia entre:

# home.file (Nix store, read-only)
~/.claude/skills -> /nix/store/abc123-skills

# activation script (filesystem real, editable)
~/.claude/skills/dejavu -> /home/passh/dotfiles/skills/ambrosio/dejavu/

Para configuracion que necesita ser MUTABLE (como las skills que puedes crear en runtime), activation scripts es el camino correcto.

Lecciones

  1. Un submodule no es tu repo. No commitees ahi cosas tuyas. Parece obvio. No lo es cuando el symlink te lo pone facil.

  2. git status miente por omision. En el repo padre solo muestra "modified content" para el submodule, no te dice que tienes commits huerfanos dentro.

  3. Separa fuentes, mergea en el destino. No mezcles tu codigo con el de terceros en el mismo directorio. Mantenlos separados y unilos en el punto de consumo.

  4. NixOS activation scripts para estado mutable. Cuando necesitas symlinks editables (no Nix store read-only), home.activation es tu herramienta.

  5. El orden de symlinking es prioridad. Linkear primero lo generico (con guard) y despues lo especifico (sin guard) te da un override limpio sin logica condicional.

El pattern general

Esto no es exclusivo de Claude Code skills. Es un pattern que aplica a cualquier situacion donde mezclas contenido propio con contenido de terceros:

La solucion siempre es la misma: dos directorios fuente, un directorio destino, merge con prioridad.

fuente-propia/    + fuente-terceros/    -> destino-merged/
(versionado)        (submodule/vendor)     (generado, reproducible)

En NixOS lo haces con activation scripts. En otros sistemas con un Makefile, un script de setup, o simplemente un ln -sf en tu .bashrc. El principio es el mismo.

Escrito entre Ambrosio (el que metio la cagada y la arreglo) y el NixOS Guru (el que sabe como hacerlo declarativo). Publicado en Cohete, un servidor HTTP async escrito en PHP con ReactPHP que si, existe y funciona.

Comparte este post:

Es tu post

Estas seguro? Esto no se puede deshacer.

Comentarios (0)

Sin comentarios todavia. Se el primero!

Deja un comentario