
npm install -g cortex-sync@latest
Cuando audité cortex la semana pasada, encontré un RCE y un path traversal — eso fue la 0.5.0. Pero en esa misma auditoría anoté algo más, menos dramático pero más común: en mi propia máquina, 7 de mis 16 sesiones de Claude Code ya pesaban más de 1 MB. La más grande, 46 MB.
Esta semana até ese cabo.
cortex sync/pull con GitHub como backend usaban la Contents API — el endpoint que uso para crear/leer/actualizar un archivo en un repo. Es la API obvia: GET /repos/{owner}/{repo}/contents/{path} te da el contenido en base64 directo en la respuesta.
El problema es un detalle que no está gritando en la documentación: para archivos de más de 1 MB, GitHub omite el campo content de la respuesta. Te sigue dando el sha, el size, todo lo demás — pero no el contenido.
cortex pull no fallaba con un error claro. Fallaba en silencio: recibía una respuesta 200 OK, intentaba leer data.content, y o explotaba con un undefined opaco o —peor— seguía adelante con contenido vacío. Para cualquier sesión de más de 1 MB, tu historial de Claude Code simplemente no volvía.
GitHub tiene otra forma de leer/escribir contenido de archivos que no pasa por ese límite: la Git Data API — los mismos primitivos que usa git por debajo (blobs, trees, commits, refs).
// Antes — Contents API, corta en 1MB \
const res = await fetch(apiBase + '/contents/' + path); \
const { content } = await res.json(); // undefined si el archivo pesa >1MB
// Ahora — Git Data API, sin ese límite \
const sha = await getSha(path); // Contents API solo para el sha (sin límite de tamaño) \
const res = await fetch(apiBase + '/git/blobs/' + sha); // el contenido real, sin cortar \
const { content } = await res.json();
has() sigue usando la Contents API — ahí solo pido metadata (sha, existe/no existe), y esa consulta no tiene el límite. Es específicamente el campo content de esa respuesta el que GitHub recorta.
Escribir contra la Git Data API significa que ya no puedo usar PUT /contents/{path} (un commit automático por archivo). Tengo que construir el flujo completo a mano: leer el commit actual → su tree → crear un blob por archivo → armar un tree nuevo → crear un commit → mover el ref de la rama.
Es más código, pero una vez que lo tengo, batchearlo es casi gratis: en vez de un blob+tree+commit+ref-update por archivo, hago uno por todo el sync. Antes, sincronizar 20 archivos cambiados eran 20 commits. Ahora es 1 (o 2, si además hay borrados).
async writeMany(files: FileWrite[]): Promise { \
const parentSha = await this.getRefSha(branch); \
const baseTree = await this.getCommitTreeSha(parentSha); \
const entries = await Promise.all( \
files.map(async (f) => ({ path: f.path, mode: '100644', type: 'blob', sha: await this.createBlob(f.content) })) \
); \
await this.commitTreeEntries(branch, parentSha, baseTree, entries, 'cortex: update ' + files.length + ' file(s)'); \
}
No lo tenía planeado como objetivo del fix — salió de implementar la Git Data API correctamente.
Un cifrado AES-GCM produce bytes de alta entropía — no se puede comprimir después de cifrar, tiene que ser antes. Las sesiones de Claude Code son JSONL muy repetitivo (las mismas claves, las mismas rutas, una y otra vez), así que gzip les entra muy bien.
Lo verifiqué con una sesión sintética de 5.5 MB, altamente repetitiva a propósito:
5.55 MB en disco → 31 KB subidos (cifrados) \
15000/15000 líneas restauradas idénticas en la otra máquina \
cwd remapeado correctamente de /projA a /projB
Una sesión real (con más variedad de texto) comprime menos que ese caso extremo, pero el orden de magnitud se mantiene: menos para subir, menos para bajar, y menos probabilidad de que cualquier archivo individual se acerque a un límite de tamaño para empezar.
El roadmap original de esta fase también mencionaba arreglar la paginación de GitHubBackend.list() — el método que lista todo el árbol del repo puede venir con truncated: true si hay demasiadas entradas.
Antes de tocarlo, busqué quién lo llama. Nadie. sync, pull y status direccionan cada archivo por su ruta exacta del manifiesto — nunca necesitan listar el árbol completo. list() solo lo usan los tests.
Construir paginación real para un método que nada en el flujo activo invoca habría sido trabajo puramente especulativo. Lo dejé como está y lo anoté en el CHANGELOG — mejor decir explícitamente "esto no lo toqué, y por qué" que pretender que fue parte del alcance.
npm install -g cortex-sync@latest # → 0.5.1
Es breaking change, otra vez sin ruta de migración: el contenido remoto de la 0.5.0 no está comprimido, esta versión espera que sí lo esté. Volvé a correr cortex sync en cada proyecto después de actualizar.
169 tests, todos en verde, más una prueba real contra una sesión de 5.5 MB de punta a punta.
GitHub: SebastiaWeb/cortex-cli