50.406 estrellas, 16.937 forks y 187 vigilantes: auditoría del framework de búsqueda de empleo en el que dos usuarios reales ya denunciaron fugas de datos personales

La API de GitHub devuelve 50.406 estrellas, 16.937 forks y 187 vigilantes para MadsLorentzen/ai-job-search. Muestreé 120 forks públicos y 113 seguían con los tokens de plantilla intactos. También leí los dos issues de privacidad que abrieron usuarios reales: un fork más /setup que publica tu CV en un repo público, y un default de gh que archiva tu pipeline de candidaturas en el tracker de un desconocido. La suite del propio repo supera 598 tests y ninguno de ellos puede frenar ninguna de las dos fugas.

Un geofísico pierde su empleo a finales de 2025, monta un flujo de trabajo con Claude Code para conducir su propia búsqueda, envía 69 candidaturas, consigue 20 primeras entrevistas, firma un contrato y publica la maquinaria en marzo de 2026. Siete meses después, el repositorio tiene 50.406 estrellas. Esa historia está en el README, en la primera pantalla, y es la razón por la que la gente hace fork.

La historia que no está en el README está en el tracker de issues. Entre el 19 y el 28 de agosto, dos usuarios distintos denunciaron que este flujo de trabajo ya había publicado datos personales en GitHub: una vez por el fork que el propio quick start te obliga a crear, y otra por un comportamiento del repositorio por defecto de gh que archiva tus notas de candidaturas en el tracker público de otra persona. Cloné el repo, ejecuté sus propios chequeos, muestreé 120 de sus forks y leí los dos issues. Esto es lo que sostienen los números y el código.

Los números, antes que nada

Todo lo que sigue viene de la API REST de GitHub el 2026-10-18, salvo donde indique otra cosa.

MétricaValor
Estrellas50.406
Forks16.937 (33,6% de las estrellas)
Vigilantes (subscribers_count)187 (0,37% de las estrellas)
Commits336
Contribuidores78
Releases10 etiquetas, v1.0.0 (2026-07-22) → v1.7.2 (2026-09-29)
Commits en master después de v1.7.238
Creado2026-03-18

Dos de esas proporciones son lo bastante raras como para merecer su propia tabla. Saqué los mismos cinco campos de cinco repositorios comparables el mismo día:

RepositorioEstrellasForksForks/estrellasVigilantesVigilantes/estrellas
MadsLorentzen/ai-job-search50.40616.93733,6%1870,37%
affaan-m/ECC282.51041.98714,9%1.4080,50%
anthropics/skills181.79621.53111,8%1.1170,61%
obra/superpowers301.22226.8968,9%1.1100,37%
akitaonrails/ai-memory9.9207167,2%550,55%
android/skills8.4265556,6%921,09%

Un 33,6% de forks no es sospechoso por sí mismo, y sería perezoso presentarlo así. Es una plantilla que te pide hacer fork («Fork it and own it»): un ratio alto es el comportamiento previsto. Lo que sí dice el ratio es que «usó este repositorio» y «le dio una estrella» miden poblaciones distintas.

Qué contienen realmente 120 forks públicos

El repositorio tiene exactamente dos archivos versionados donde /setup escribe la identidad del candidato: CLAUDE.md y .claude/skills/job-application-assistant/01-candidate-profile.md. Ambos se publican con tokens literales de plantilla: [YOUR_NAME], [YOUR_EMAIL], [YOUR_PHONE].

Así que muestreé forks y descargué esos dos archivos vía raw.githubusercontent.com: los primeros 40 con sort=newest, los primeros 40 con sort=oldest y los primeros 40 con sort=stargazers. En total, 120 filas de fork, con posible solapamiento entre los órdenes. Cada archivo se clasificó como: conserva los tokens, tokens sustituidos (personalizado), inexistente (404) o vacío.

ArchivoPlaceholder intactoPersonalizadoInexistente
CLAUDE.md11333 (+1 vacío)
01-candidate-profile.md11343

4 de 120 forks (3,3%) habían sustituido los tokens de plantilla en al menos uno de los dos archivos. El otro 96,7% son copias idénticas de la plantilla en esas rutas.

Quiero ser preciso con lo que esto demuestra, porque la lectura obvia es falsa. SETUP.md ofrece explícitamente «hacer commit en local sin hacer push» como una forma legítima de usar el framework, así que un fork intacto no es un fork sin usar. Lo que sí establece la muestra es más estrecho y aun así útil: el número de forks no se puede leer como número de instalaciones, y si te preocupaba la filtración de perfiles a escala, la tasa empírica es pequeña — unas 560 cuentas si el 3,3% se sostuviera sobre los 16.937 forks, lo cual es una proyección, no una medición.

Los 187 vigilantes importan por la misma razón. Lancé tres consultas a Hacker News vía la API de Algolia —«MadsLorentzen», «ai-job-search» y «AI job search Claude Code»— y los únicos envíos de este proyecto puntuaron 1, 2 y 1 puntos con cero comentarios. Un repositorio con 50.000 estrellas y ninguna conversación en Hacker News no llegó ahí a través de Hacker News. Lo hicieron GitHub Trending, una insignia de Trendshift y LinkedIn. Nada de eso es una falta; es lo que significa el número.

Fuga uno: el quick start publica el perfil

El paso 1 del README es:

gh repo fork MadsLorentzen/ai-job-search --clone
cd ai-job-search

El paso 3 es /setup, que escribe tu nombre, dirección, teléfono, correo, LinkedIn, historial laboral con fechas, salario mínimo aceptable, líneas rojas y autoevaluación conductual en CLAUDE.md, .claude/skills/job-application-assistant/01-…07-*.md, cv/main_example.tex y .claude/skills/job-scraper/search-queries.md. Todos están versionados, y tienen que estarlo: así es como las actualizaciones de upstream se fusionan en tres vías alrededor de tu personalización en lugar de chocar con ella. Lo verdaderamente sensible (documents/, salary_data.json, CV y cartas generados, el archivo de candidaturas) está en .gitignore; el perfil no.

GitHub no permite forks privados de un repositorio público. Así que el paso 1 garantiza un remoto público, el paso 3 garantiza datos personales versionados, y el paso natural después es git push.

El issue #345, abierto el 2026-08-19, es exactamente esa secuencia ocurriéndole a una persona real. En palabras del propio reportero:

Me pasó a mí: copié y pegué el comando del README, ejecuté /setup y solo descubrí la exposición después, cuando fui a mirar cómo se suponía que funcionaba el control de versiones en este repo.

El análisis del issue sobre por qué fallaron las salvaguardas existentes es más agudo que el resumen:

El aviso existía, pero aguas abajo de la decisión. SETUP.md, línea 293, lo tenía, correcto y completo, pero era el paso 1 de la §8, seis secciones después de que la §2 «Fork and clone» te dijera que hicieras fork.

Y sobre el propio aviso de privacidad de /setup: se imprimía en el «Paso 4: Confirmar y siguientes pasos», después de haber escrito todos los archivos. En ese punto, el consejo «no hagas push de estos commits» exigía abandonar el remoto que el paso 1 acababa de decirte que crearas.

Es un bug de documentación con forma de pérdida de datos, y el mantenedor lo trató como tal. El README actual lleva un bloque [!IMPORTANT] justo debajo del comando de fork, y /setup ejecuta ahora una comprobación previa antes de escribir nada:

Ejecuta git remote get-url origin; si el comando falla (no hay remoto, o no es un checkout de git), omite esta comprobación en silencio. Si hay un origin de GitHub, compruébalo con gh repo view <owner/repo> --json visibility,isFork cuando gh esté disponible. Si el origen es un fork público de la plantilla —o su visibilidad no puede determinarse— avisa ahora y espera…

Dos detalles de ese arreglo merecen reconocimiento. Falla hacia el aviso cuando la visibilidad no puede determinarse, y el arreglo está anclado por tests/test_onboarding_privacy.py, que verifica que el texto del aviso esté dentro de la misma sección Markdown que el comando gh repo fork, tanto en el README como en SETUP.md. Una regresión que aleje el aviso de la decisión rompe CI. Es un trato mucho más serio de un problema de documentación del que la mayoría de proyectos da a un problema de código.

Pero fíjate en lo que el arreglo no hace: añade un aviso y una pregunta. No convierte el camino seguro en el camino por defecto.

Fuga dos: gh archiva tu búsqueda en el tracker de un desconocido

El issue #389, abierto nueve días después, el 2026-08-28, es la parte que no había visto en ningún repositorio de este tipo, y es la más peligrosa de las dos.

gh repo fork --clone establece el repositorio upstream como repositorio por defecto de gh, y gh usa el valor por defecto para gh issue create. Un fork personalizado es exactamente donde alguien conecta automatización; «abre un issue de seguimiento por cada candidatura» es una petición natural para un agente. Sin --repo, sin prompt, sin nada que confirmar. El issue aterriza en MadsLorentzen/ai-job-search.

El reportero no especuló. Citó cuatro casos reales de dos usuarios independientes en una sola semana: #386/#387/#388, descritos como los planes de candidatura de un usuario —empresas objetivo, puntuaciones de encaje, restricciones personales de decisión, en japonés— y #381, el registro de incidencias de otro usuario que referenciaba la estructura de su repo privado.

Comprobé esos cuatro números el 2026-10-18. Los cuatro devuelven HTTP 410 Gone: eliminados, no cerrados. Es un hecho con doble filo. Por un lado demuestra que el mantenedor puede limpiar un desastre que los usuarios no pueden limpiar por sí mismos (solo los administradores del repositorio pueden borrar un issue; los autores no). Por otro, significa que la evidencia principal de la incidencia ya no resuelve para quien lea #389 hoy: el cuerpo del issue apunta a cuatro URLs muertas, y GitHub no deja lápida pública.

La respuesta actual de SETUP.md es una regla dura, no un aviso:

La línea de set-default no es opcional. gh repo fork --clone establece el repo upstream como repositorio por defecto de gh … Sin ella, cualquier gh issue create posterior ejecutado desde este clon —por ti o por un agente al que le hayas pedido seguir tus candidaturas— archiva en silencio en el tracker público de upstream, publicando lo que contenga el issue bajo tu identidad de GitHub, en un repo donde no puedes borrarlo (#389).

Es el arreglo correcto, y también el que más necesita una barrera de código en lugar de un párrafo, porque todo el modo de fallo consiste en un agente actuando sin que un humano lea un párrafo. Esa barrera no existe. El job de CI que podría detectarlo (el grep de integridad de placeholders) está limitado al repositorio upstream.

Donde los números y el código discrepan sobre la seguridad

Aquí está el problema estructural, y no es dejadez del mantenedor: es una tensión de diseño que el repositorio declara abiertamente en .github/workflows/ci.yml:

Fork-friendly by design: los forks personalizan CLAUDE.md, los archivos de skills y cv/main_example.tex vía /setup, así que el job de integridad de placeholders y las aserciones exactas de páginas/contenido se ejecutan solo en el repositorio plantilla de upstream.

Léelo dos veces. El job cuyo mensaje de error es expected placeholder token $2 - personal data may have been committed está deliberadamente desactivado justo donde se cometen los datos personales: tu fork. No puede activarse, porque un fork personalizado es el estado esperado. La única barrera entre un usuario primerizo y un perfil público es una cita que dice «espera».

Ejecuté la barrera que sí existe, contra el clon de upstream:

security_guards: OK (permissions allowlist, hooks allowlist, gitignore rules, package manifests)

Ese job merece entenderse antes de descartar el repositorio. Hace fallar los pull requests que amplían los permisos de .claude/settings.json (los CLIs se ejecutan preaprobados), que debilitan las reglas de .gitignore para datos personales o que añaden scripts de ciclo de vida de npm. Las acciones están fijadas a SHAs de commit y el token es de solo lectura. El encabezado del workflow incluso declara su propio límite:

un PR puede editar este mismo workflow, así que estas barreras atrapan accidentes e intentos casuales, no a un autor decidido: la protección de rama con checks obligatorios y la revisión humana de los diffs de workflow/settings siguen siendo el verdadero respaldo.

Las partes por las que nadie hace fork son las buenas

Si solo leyeras el número de estrellas esperarías un paquete de prompts. Lo que hay en el árbol es más raro y mejor.

tools/robots_check.py implementa RFC 9309 con semántica fail-closed, y documenta las formas concretas en que los parsers ingenuos fallan abiertos.

  • gana la coincidencia más larga; a igual especificidad gana Disallow
  • un Disallow para «*» o para «Claude-User» bloquea el reintento
  • las líneas en blanco dentro de un registro no lo terminan (el robotparser de Python descarta reglas en ese caso, lo que falla abierto; ver tests)
  • se salta un único byte-order mark UTF-8 inicial (si se deja, oculta una primera línea «User-agent» y descarta todo el grupo, lo que falla abierto)
  • 404 significa que no hay política publicada, lo que es permiso
  • cualquier otro fallo al leer robots.txt deja el permiso sin confirmar, y el reintento no ocurre — incluida una línea User-agent, Allow o Disallow con U+FFFD

Dos de esos son bugs que he visto en crawlers de producción. El caso soft-200 también está cubierto: un host que responde /robots.txt con una página de error HTML parsea a cero reglas, y cero reglas se leen como «permitido», así que el script trata un cuerpo no vacío sin directiva reconocida como ilegible en vez de como permiso. Hay incluso un comentario que explica que una decodificación sin fijar en una máquina Windows turca leyó un archivo como cp1254, el hilo lector del pipe murió, stdout llegó como None y un dominio real quedó como UNCONFIRMED. Eso es un informe de bug convertido en comentario de código, que es como se ve la buena ingeniería.

La suite pasa. Monté un venv con pytest y pyyaml y la ejecuté:

598 passed, 3 skipped, 328 subtests passed in 51.87s

Cincuenta y un segundos de verde en un repositorio de 50.000 estrellas no es lo habitual. Tampoco lo es fijar con un test la ubicación de un aviso dentro de Markdown.

El pipeline de candidaturas tiene una comprobación real de presentación. /apply compila los dos PDF (lualatex para el CV, xelatex para la carta) y luego mide la salida renderizada con tools/verify_layout.py: títulos de entrada huérfanos, colisiones con el pie, viñetas que caen a la fuente del cuerpo. /apply también ejecuta una pasada ATS sobre la capa de texto extraída del PDF, porque un sistema de seguimiento de candidaturas lee el texto embebido y no la página, y LaTeX puede generar en silencio un PDF cuyos datos de contacto se extraen como glifos de iconos. Cuando un CV se pasa de dos páginas no corta la sección más antigua: puntúa cada línea por relevancia frente a la oferta, unicidad en el documento y si la carta depende de ella.

La regla de no inventar está escrita, y sus límites están escritos al lado. El README afirma: «Todas las afirmaciones del CV y la carta se verifican contra tu perfil real. El sistema nunca inventa habilidades ni experiencia». La implementación es una «Auditoría de fundamentación» contra tres fuentes (01-candidate-profile.md, el CV maestro y la sección de perfil de CLAUDE.md), más una separación redactor-revisor donde un segundo agente con contexto fresco critica el borrador.

Eso es disciplina a nivel de prompt, no una comprobación determinista, y el repositorio lo dice él mismo, en la parte que habría citado incluso si hiciera quedar peor al proyecto:

Esto no es contabilidad. Un hecho que existe solo en el chat será tratado como no respaldado por una sesión posterior y eliminado de los borradores como una invención. Todo lo ausente de las fuentes no existe en lo que respecta al redactado futuro, y la pérdida es silenciosa: un logro real desaparece discretamente de todos los CV siguientes.

Una herramienta que elimina afirmaciones verdaderas porque no puede verificarlas es la versión honesta de una herramienta que dice no inventar nunca. Los dos modos de fallo vienen del mismo mecanismo.

Cuatro cosas que siguen abiertas

1. No hay ruta de remediación para datos que ya salieron. Ni los issues, ni SETUP.md, ni el README describen qué hacer después de que un perfil se ha publicado: ni git filter-repo, ni instrucciones de force-push, ni procedimiento de borrado. /reset profile limpia archivos locales. El comando /reset «muestra exactamente qué se va a borrar y exige que escribas RESET para confirmar», y lo que borra está en tu disco. GitHub no puede volver privado tu fork; el README lo dice sin rodeos. La comprobación previa reduce la probabilidad del error; nada reduce su coste.

2. Audiencia global, superficie danesa. Se envían seis CLIs de portales. Cuatro son portales de empleo daneses (Jobindex, Jobnet, Jobbank, Jobdanmark). Los dos que son agnósticos de país son linkedin-search y freehire-search, y el segundo se describe como centrado en tecnología, así que una enfermera en Lisboa y un responsable de logística en Osaka se quedan con LinkedIn o nada. /add-portal genera un CLI nuevo para un portal y prueba una consulta en vivo antes de registrarlo, lo cual es una respuesta genuinamente buena; también es trabajo que haces tú. La herramienta de salarios es peor de lo que sugiere el «funciona con cualquier dato salarial que aportes»: su emparejador de nombres de empresa fija sufijos legales daneses (a/s, aps, i/s, p/s, k/s, ivs, amba) y transliteración danesa (ø→o, æ→ae, å→aa). Con tu propio JSON funciona; espera que normalice «GmbH» o «S.p.A.» y no lo hará.

3. El canal de releases se ha parado mientras master avanza. La última etiqueta es v1.7.2, del 2026-09-29. Desde entonces han entrado 38 commits en master, incluidos 14 el 15 de octubre y 10 el 17 de octubre. La §8 de SETUP.md abre con «Prefiere releases antes que master en crudo. Las releases etiquetadas son checkpoints revisados» y luego el paso 3 de esa misma sección ordena git merge upstream/master. Las dos afirmaciones están en el documento. Para un fork que siga el paso 3, «checkpoint revisado» deja de describir lo que ejecuta.

4. El historial es inverificable y está etiquetado con honestidad. 69 candidaturas, 20 primeras entrevistas, un contrato firmado. No hay registros en el repositorio, y el README remite al LinkedIn del autor para la versión larga. Se presenta como experiencia personal, no como estudio, que es la forma correcta de presentarlo, pero no es evidencia de que el flujo te vaya a dar un 29% de primeras entrevistas, y nada en el repo finge lo contrario.

Cómo ejecutarlo sin publicar nada

El proyecto lo documenta, en la §8 de un archivo que la mayoría de usuarios nunca abre. Reunido en un solo lugar:

  1. No hagas fork. Crea un repositorio privado y luego git remote add upstream https://github.com/MadsLorentzen/ai-job-search.git. Todos los flujos de actualización de la §8 siguen funcionando: fusionar una etiqueta, ejecutar tools/check_upstream_updates.py o clasificar commits con tools/upstream_triage.py.
  2. Si ya hiciste fork, fija el default de inmediato: gh repo set-default <tu-usuario>/ai-job-search. De lo contrario, un agente al que pediste «abrir un issue de seguimiento» escribe en el tracker de upstream.
  3. Decide la cuestión del commit antes de /setup, no después. Hacer commit de la personalización en local está bien; hacer push a un fork no; hacer push a un repo privado está bien.
  4. Sabe qué cubre /reset: archivos de perfil locales y documents/ local. No tu remoto, no tu historial, no un issue que pertenece a otra persona.
  5. Lee cualquier skill de portal antes de ejecutarla. El repositorio se niega a enviar un instalador de skills de la comunidad, y da la razón: «un instalador que las descargara de repos de terceros por ti se saltaría la única comprobación que importa: que tú leas el código primero».

Y una cita que conviene guardar tal cual si usas el CLI de LinkedIn:

Pensado solo para uso personal: el acceso automatizado va contra los Términos de Servicio de LinkedIn, así que mantén el volumen bajo.

El archivo de CI es explícito en que esa es la razón por la que las pruebas en vivo de portales no están en CI: «las peticiones automatizadas desde CI lo violarían». El uso personal es la línea que traza el proyecto, y la traza en el pipeline, no solo en una nota del README.

Preguntas frecuentes

¿Es gratis? Licencia MIT. La cadena de herramientas no: Claude Code «no tiene plan gratuito» según el README, así que cuenta con una suscripción Claude Pro/Max/Team o créditos de API, además de Bun y una distribución de LaTeX.

¿/setup publica mi CV por sí solo? No. Escribe en archivos versionados de tu disco. La publicación ocurre en el git push a un remoto público, que es justo lo que crea el paso 1 del README, y por eso existe el issue #345.

¿Puedo deshacerlo si ya hice push? No con nada de este repositorio. GitHub no convierte un fork público en privado, /reset es solo local, y los issues abiertos en el tracker de upstream pertenecen a los administradores de ese repositorio. Trata el push como irreversible.

¿50.406 estrellas demuestran que funciona? Demuestran que se difundió. El 33,6% de esas estrellas corresponde a forks, el 0,37% a vigilantes, 113 de 120 forks muestreados son plantillas intactas y ningún hilo de Hacker News sobre el proyecto pasó de 2 puntos. El diseño es coherente y los tests pasan; esas son mejores señales que el número de estrellas.

¿Va a inventar experiencia en mi CV? Los prompts lo prohíben y la Auditoría de fundamentación elimina afirmaciones sin respaldo contra tres fuentes. Es disciplina a nivel de instrucción, no una comprobación determinista, y el repo documenta el compromiso: un hecho verdadero que nunca llegó al perfil se elimina como invención. Lee cada línea generada.

¿Funciona fuera de Dinamarca? El pipeline central (perfil, evaluación de encaje, redactor-revisor, verificación de PDF) es agnóstico de mercado. La capa de portales no: cuatro de seis CLIs y las reglas de normalización del buscador de salarios son danesas. Usa linkedin-search o freehire-search, o escribe tu propio portal con /add-portal.

¿Es legal hacer scraping de los portales? El proyecto condiciona su único reintento con cabeceras de navegador a una comprobación de robots.txt según RFC 9309 que falla cerrada, y etiqueta la automatización de LinkedIn como violación de ToS más allá del uso personal. Es una postura defendible, no una opinión jurídica: la decisión sigue siendo tuya.

Qué es este repositorio

No es un artefacto de hype con una carpeta de prompts. Aquí hay un parser de RFC 9309 con comentarios sobre un códec de Windows turco, 598 tests que pasan, un medidor de maquetación de PDF, una herramienta de clasificación de commits basada en patch-id para forks y un job de CI que hace fallar un pull request por añadir un script postinstall de npm.

Es también un repositorio cuyos dos pasos de onboarding más vistos publicaron cada uno los datos personales de alguien, y cuyo propio CI es arquitectónicamente incapaz de notarlo donde más ocurre. El arreglo del primero fue mover un aviso al punto de decisión. El arreglo del segundo fue una frase: correcta, bien argumentada e inaplicable por código, en un flujo donde un agente hace el trámite.

Si vas a ejecutarlo, la diferencia entre una buena experiencia y una mala no es el número de estrellas. Es si leíste la §8 antes del paso 1.