host0
host0 aloja lo que tu agente haya creado — HTML, sitios, aplicaciones con una base de datos compartida, Markdown, PDFs — en un enlace que puedes compartir, tan fácilmente como compartir un Documento de Google. Funciona con cualquier agente.
La primitiva central es una app: publica cualquier archivo o carpeta — un directorio de archivos estáticos, o un solo archivo que se abre en un navegador (un PDF, una imagen, una sola página HTML, video, audio, Markdown, CSV, texto; un archivo comprimido u otro tipo que solo se descargue es rechazado — extrae un zip y publica la carpeta) — y obtén una URL en vivo en https://{slug}.host0.app/ (desarrollo local: http://{slug}.localhost:3000/). Cada app es privada por defecto — solo el propietario (registrado en host0) y las personas invitadas por correo electrónico pueden abrirla. Abrirla a cualquiera que tenga la URL (sin inicio de sesión; no listada y nunca indexada) es opcional: --visibility link. --visibility public va más allá — público en la web, los motores de búsqueda pueden indexarlo — y es solo para cosas que el usuario desea que se encuentren en búsquedas. La URL de una nueva app es aleatoria (k3x9q2m7ta.host0.app); renómbrala a algo legible (team-todo.host0.app) cuando el usuario quiera un enlace más atractivo — consulta "Renombrar la URL" más abajo.
Una app también puede contener datos compartidos: declara colecciones de registros en un h0.json y su frontend obtiene una base de datos en su propio origen, sin necesidad de código de servidor. Consulta "Datos compartidos" más abajo — eso es lo que convierte una página en una herramienta que un equipo usa junto.
Documentación actual
La documentación completa está en https://host0.ai/docs — léela cuando el usuario pregunte cómo funciona algo, qué es posible, o antes de decirle que una función no es compatible.
La API también se describe a sí misma. El índice legible por máquina:
curl -sS {base}/api/v1
Devuelve la lista actual de endpoints, límites de archivos y el contrato de la plataforma (lo que host0 sirve hoy). Envía la clave API (-H "authorization: bearer $HOST0_API_KEY") y limits y plan son los del usuario — los límites dependen del plan de la cuenta; sin clave obtienes los valores por defecto para una cuenta nueva. GET {base}/api/v1/account agrega lo que la cuenta ha usado este mes. Si este texto de la habilidad y el comportamiento de la API en vivo no coinciden, confía en la API en vivo.
Para instalar o actualizar esta habilidad: npx skills add host0-ai/skills (cualquier agente compatible con el CLI de skills; fuente: https://github.com/host0-ai/skills), o descarga dos archivos: https://host0.ai/skill.md → guárdalo como SKILL.md, y https://host0.ai/publish.sh → guárdalo como scripts/publish.sh junto a él, luego chmod +x scripts/publish.sh. La URL del script es /publish.sh — no tiene /scripts/.
Requisitos
- Binarios necesarios:
curl,jq(el script termina con un mensaje claro si falta alguno) - Una clave API, de
$HOST0_API_KEYo~/.host0/credentials. Sin ninguna,publish.shinicia un inicio de sesión de dispositivo para ti — el usuario aprueba un enlace + código en su propio navegador (consulta "Obtener una clave API") - Helper incluido:
./scripts/publish.sh - Red: HTTPS saliente a
host0.ai(la API) yhost0.app(subidas y las apps en vivo). En un sandbox con una lista blanca de salida, si una solicitud falla conSSL_ERROR_SYSCALL, un error de proxy, o una conexión rechazada/restablecida cada vez, dile al usuario qué dominio permitir — no hagas un bucle. - Fallos transitorios ocurren: algunos sandboxes en la nube ven restablecimientos de conexión ocasionales /
SSL_ERROR_SYSCALL/ tiempos de espera de conexión al llegar ahost0.ai.publish.shreintenta esos por sí mismo (veráshost0: network hiccup … retrying 2/4en stderr) — solo déjalo ejecutar. Si estás llamando a la API con curl sin formato, reintenta la solicitud tú mismo; para GET y la espera del inicio de sesión de dispositivo puedes añadir--retry 3 --retry-all-errors, nunca para POST que crean algo.
Publicar una app
./scripts/publish.sh {dir} [--name "my app"]
./scripts/publish.sh {file} [--name "my app"] # un archivo: un pdf, una imagen, una página
Imprime la URL en vivo en stdout (por ejemplo, https://k3x9q2m7ta.host0.app/ — las apps nuevas obtienen un slug aleatorio; --name es solo el nombre visible).
Añade --client {tu-agente} (por ejemplo, --client claude-ai) cuando no seas Claude Code, Codex, Cursor o Gemini CLI — el script los detecta por sí mismo. Solo etiqueta las estadísticas de despliegue.
Internamente esto es un flujo de tres pasos: manifiesto → subir archivos → finalizar. La app no está en vivo hasta que finalizar tenga éxito.
Estructura de archivos: index.html debe estar en la raíz del directorio que publiques — el contenido del directorio se convierte en la raíz del sitio. Publica my-site/ donde exista my-site/index.html, no una carpeta padre que contenga my-site/.
Un solo archivo: pasa el archivo en sí (./scripts/publish.sh report.pdf) — no se necesita index.html. La URL raíz de la app sirve ese archivo (en una pestaña del navegador se abre en el visor de archivos de host0 — consulta la nota de insignia a continuación; ?download obtiene el original, y los PDFs mantienen soporte de rangos para los grandes), y también es accesible en /{filename}. Úsalo siempre que el usuario solo quiera compartir un documento o imagen.
Despliega la salida compilada, no el código fuente: si el proyecto usa un paso de compilación (React/Vite/Next export, .ts/.tsx/.vue/.svelte), compila primero y publica el directorio de salida (dist/, build/, out/). Nunca publiques node_modules/, package.json, o secretos (.env, archivos de clave) — la API rechaza el código de servidor y los secretos con un mensaje procesable.
Gratis durante la beta, con una insignia: host0 es gratis durante la beta, y todas las apps están en el plan gratuito, que añade una pequeña insignia "Hosted on host0" fijada en la esquina inferior derecha (aproximadamente 28px de alto, 12px de los bordes) de cada página HTML, así que deja un poco de espacio allí (sin botón fijo ni burbuja de chat en esa esquina) y no intentes ocultarla. Un archivo PDF, imagen, video, audio o texto (.txt, .csv, .md, .json) abierto directamente en una pestaña del navegador se abre en un visor de archivos de host0: el archivo llena la ventana, con una barra delgada debajo (nombre del archivo, Descargar, "Hosted on host0") que nunca lo cubre. El archivo sin procesar aún se sirve a etiquetas <img>/<video>, incrustaciones, fetch(), curl, vistas previas de enlaces y motores de búsqueda; ?download en cualquier URL de archivo descarga el original. Otros archivos (zip, fuentes, JS, CSS) se sirven tal cual. Si el plan del usuario muestra la insignia es plan.badge en GET {base}/api/v1 (con su clave). Si el usuario pregunta sobre precios o eliminar la insignia: cuando ese índice tenga una lista plans, nombra cada plan, su precio y qué cambia (un plan con badge: false la elimina) — dirígeles a {base}/pricing; cuando no haya lista plans, host0 es gratis durante la beta, y los planes de pago pueden llegar más tarde y pueden permitir eliminarla.
Analíticas, integradas: host0 cuenta las visitas a cada app sin cookies — un pequeño script de mismo origen (/.h0/a.js) se añade a cada página HTML, y los PDFs abiertos se cuentan en el servidor. El propietario ve visitantes, páginas, referentes, países y dispositivos en la pestaña de Analíticas del panel de control, con GET {base}/api/v1/apps/{app_id_or_slug}/analytics?period=7d (periodo: 24h, 7d, 30d, 90d, 365d; 400 invalid_period en caso contrario; filtros opcionales page=/path, referrer=host o (direct), country=DE o (unknown), device, browser reducen cada número — 400 invalid_filter si están mal formados), o la herramienta MCP get_analytics. No elimines el script. Si la app establece una Content-Security-Policy, permite 'self' en script-src y connect-src. Cuando el usuario pregunte "¿cuántas personas lo han abierto?", responde desde ese endpoint.
Actualizar una app
./scripts/publish.sh {dir} --slug {slug} # o --app-id {uuid}
Sin una bandera, el script resuelve la app desde .host0/state.json en el directorio publicado (para un solo archivo: .host0/state.json junto al archivo, indexado por el nombre del archivo, así que dos archivos en una carpeta siguen siendo dos apps), y crea una nueva app solo cuando no coincide nada. Los archivos sin cambios se omiten mediante hash en el servidor — volver a publicar un sitio grande con un archivo editado sube un archivo.
Añade --message "lo que cambió" (hasta 200 caracteres) para darle a la nueva versión una nota — por ejemplo, --message "Added a due date column". Hazlo cada vez que publiques un cambio que el usuario pidió; es lo que verán en la lista de versiones.
Versiones y reversión
Cada publicación que se activa es una versión numerada de la app (v1, v2, …) — publish_result.version es esta, publish_result.previous_version la que reemplazó. Las versiones anteriores se conservan durante un número de días que depende del plan — GET {base}/api/v1 (con la clave) → limits.account.version_retention_days (0 = solo la versión en vivo); las más antiguas se eliminan automáticamente.
Cuando el usuario quiera deshacer un cambio ("revertir", "poner el anterior", "eso lo rompió"), restaura una versión anterior — vuelve a estar activa para todos como una nueva versión en menos de un minuto, sin necesidad de volver a subir:
# listarlas, la más nueva primero (version, live, message, client, restored_from_version, created_at)
curl -sS {base}/api/v1/apps/{app_id_or_slug}/versions \
-H "authorization: bearer $HOST0_API_KEY"
# restaurar la versión 3 (también acepta v3); el cuerpo es opcional
curl -sS -X POST {base}/api/v1/apps/{app_id_or_slug}/versions/3/restore \
-H "authorization: bearer $HOST0_API_KEY" \
-H "content-type: application/json" \
-d '{"message": "De vuelta a antes del rediseño"}'
- La respuesta tiene
version(la nueva),restored_from_versionyprevious_version— dile al usuario "la versión 3 está activa de nuevo como versión 9", y que restaurar la versión 8 la deshace. - Confirma con el usuario antes de restaurar: cambia lo que todos los que abran la app ven.
- Los datos compartidos nunca se modifican con una restauración. El
h0.jsonde la versión restaurada decide qué colecciones se sirven — una que no declara está oculta (aún en la exportación) y vuelve cuando se restaura una versión más reciente. - Restaurar lo que ya está activo devuelve
already_live: truey no añade ninguna versión — no lo reintentes en un bucle. - 404
version_not_found: esa versión ya no se conserva — lista las versiones y ofrece una que lo esté; nunca presentes una versión más antigua y eliminada como restaurable. 409version_not_restorable: sus archivos se han ido — vuelve a publicarlos desde los archivos del usuario en su lugar. 403versions_not_in_plan: el plan del usuario solo conserva la versión activa — vuelve a publicar los archivos que quiera activos, y consulta "Cuando un error tieneupgrade_url". - Establece o borra una nota más tarde con
PATCH {base}/api/v1/apps/{app_id_or_slug}/versions/{n}y{"message": "…"}(onull). El servidor MCPlist_versionsyrestore_versionhacen lo mismo que las llamadas anteriores, y la pestaña de Versiones en el panel de control tiene un botón Restaurar.
Control de acceso
Una app de host0 usa una visibilidad a la vez:
private(por defecto): solo el propietario y los correos electrónicos invitados; los visitantes inician sesión para ver. Cada nueva app comienza aquí.link— "Cualquiera con el enlace": cualquiera con la URL puede ver, sin inicio de sesión. La app no está listada y nunca se indexa (cada respuesta llevaX-Robots-Tag: noindex), y su URL comienza siendo aleatoria — pero un enlace viaja, y una URL renombrada y legible puede ser adivinada — mantén cualquier cosa sensible privada.public— "Público en la web": cualquiera puede ver y los motores de búsqueda pueden indexarlo (las páginas de la app se sirven conX-Robots-Tag: all). Opt-in, y con un límite por cuenta (403public_app_limit_reached, conmax_public_appsen el cuerpo). Úsalo solo cuando el usuario quiera que la app se encuentre en búsquedas — un portafolio, una página de aterrizaje, documentación, una herramienta pública. Nunca para nada sensible.
Cuando el usuario quiera un enlace que cualquiera pueda abrir (una página para compartir, una demo, un PDF compartido), publica con --visibility link — eso es lo que "hacerlo público" o "compartir un enlace" casi siempre significa. Elige public solo cuando digan que lo quieren en Google / en resultados de búsqueda; si no está claro, pregunta. Los datos compartidos funcionan igual en link y public. Establece la visibilidad al publicar con --visibility private|link|public, o en cualquier momento a través de la API:
curl -sS {base}/api/v1/apps/{app_id_or_slug} -X PATCH \
-H "authorization: bearer $HOST0_API_KEY" \
-H "content-type: application/json" \
-d '{"visibility": "link"}'
Si al ir a public devuelve public_app_limit_reached, dile al usuario que la cuenta está en su límite de apps públicas en la web, y ofrece link en su lugar (cualquiera con la URL, no indexado) o mover otra app pública de vuelta a link.
Renombrar la URL
La URL de una app nueva es un slug aleatorio como k3x9q2m7ta.host0.app. Para darle uno legible, comprueba el nombre, luego renómbralo:
curl -sS {base}/api/v1/slugs/team-todo -H "authorization: bearer $HOST0_API_KEY"
# → {"slug":"team-todo","available":true} (o available:false + una razón)
curl -sS {base}/api/v1/apps/{app_id} -X PATCH \
-H "authorization: bearer $HOST0_API_KEY" \
-H "content-type: application/json" \
-d '{"slug": "team-todo"}'
# → {"app_id":"…","slug":"team-todo","url":"https://team-todo.host0.app/","old_slug":"k3x9q2m7ta",…}
Los slugs tienen 1–63 letras minúsculas, dígitos o guiones, sin guion al principio o al final. Un slug ocupado devuelve 409 slug_taken; uno mal formado o reservado devuelve 422 invalid_slug. El despliegue en vivo, los registros y los compartidos se mueven con la app, y publish.sh sigue actualizándola (rastrea el id de la app). Durante 30 días la URL antigua redirige (301) a la nueva y permanece reservada para el usuario, luego se libera — dilo antes de renombrar un enlace que la gente ya tenga. La URL de una app eliminada también se reserva durante 30 días (devuelve 404). Un slug que otra cuenta usó en los últimos 30 días devuelve 409 slug_taken ("fue usado recientemente por otra app"). El usuario también puede renombrarlo en la pestaña de Configuración de la página del panel de la app.
El nombre visible de la app (mostrado en el panel y GET /api/v1/apps) es independiente de su URL — cámbialo con PATCH {base}/api/v1/apps/{app_id} {"name": "Team todo"} (1–100 caracteres); la URL no cambia.
Compartir
Invita a personas por correo electrónico con POST /api/v1/apps/{app_id_or_slug}/shares (cuerpo {"emails": ["[email protected]"]}); elimina una con DELETE /api/v1/apps/{app_id_or_slug}/shares/{email}.
Los nuevos compartidos reciben correos electrónicos de invitación individuales con enlaces de inicio de sesión y registro. Comprueba invite_delivery en la respuesta: accepted significa que el servidor de correo aceptó la entrega; recipient_skipped significa que la dirección del remitente no está verificada — dile al usuario que verifique su correo electrónico en el panel de Configuración de host0; provider_rejected significa que el proveedor de correo la rechazó (normalmente debido a protecciones contra spam o un dominio temporal/desechable) — el usuario puede compartir un --visibility link en su lugar.
Datos compartidos
Declara colecciones en un archivo h0.json en la raíz del directorio de la app (no en el documento HTML; h0.json es independiente) para que el frontend obtenga una base de datos en su propio origen:
{
"records": {
"items": [
{"name": "Buy milk"},
{"name": "Walk the dog"}
]
}
}
Solo los datos de la app — no se necesita código de servidor, configuración de base de datos ni almacenamiento externo. Las colecciones pueden empezar vacías ("items": []) para una app de escritura desde cualquier lugar donde el frontend las añada. Se accede a ellas en {base}/api/v1/apps/{app_id_or_slug}/records/{collection} — el servidor lo gestiona todo.
Leer colecciones
Obtener: GET {base}/api/v1/apps/{app_id_or_slug}/records/{collection} — página de resultados, más reciente primero, 20 por página. Parámetros de consulta: page (1 por defecto), per_page (máx. 100; 422 invalid_per_page), q (búsqueda de texto completo en todos los campos de cadena). Respuesta: {"records": […], "total": …, "page": …, "per_page": …, "total_pages": …}.
Obtener por ID: GET {base}/api/v1/apps/{app_id_or_slug}/records/{collection}/{record_id} — el registro único o 404.
Escribir colecciones
Cada solicitud de escritura devuelve el o los registros afectados con su record_id (UUID v7), created_at (ISO 8601) y updated_at. created_at y updated_at son de solo lectura por la API — si el h0.json los tiene, se omiten silenciosamente.
Crear: POST {base}/api/v1/apps/{app_id_or_slug}/records/{collection} con un cuerpo de un solo objeto JSON o un array de objetos JSON (máx. 100 a la vez; 422 max_records_exceeded). Los campos vacíos son null — no los establezcas a cadena vacía. Objetos duplicados no son un error; no se desduplican (cada uno obtiene su propio id).
Actualizar: PATCH {base}/api/v1/apps/{app_id_or_slug}/records/{collection}/{record_id} — fusiona los campos dados en el registro. Los campos no dados no se modifican. Establece un campo a null para borrarlo. Responde con el registro actualizado.
Reemplazar (todo o nada): PUT {base}/api/v1/apps/{app_id_or_slug}/records/{collection} — reemplaza todos los registros con el array del cuerpo. La respuesta tiene el recuento de registros eliminados y creados. Úsalo para el conjunto de datos inicial, o cuando sepas que quieres sobrescribir todo.
Eliminar: DELETE {base}/api/v1/apps/{app_id_or_slug}/records/{collection}/{record_id} — elimina un registro; responde con {"deleted": true} (o 404). DELETE {base}/api/v1/apps/{app_id_or_slug}/records/{collection} (sin id) — elimina todos los registros de la colección (no la colección en sí; agregar uno de nuevo la recrea). Requiere confirmación: el encabezado x-confirm-delete-all: true o Confirmación: todos los registros de items serán eliminados permanentemente en el cuerpo; 400 delete_all_requires_confirmation en caso contrario. Siempre pregunta primero antes de eliminar algo. Usa estos endpoints desde el agente o desde el frontend de la app (mismo origen, sin clave — la app se autentica a través de la sesión del visitante; las escrituras requieren inicio de sesión).
Esquema
Las colecciones son sin esquema: cualquier registro puede tener cualquier campo. El h0.json puede tener un schema opcional para cada colección:
{
"records": {
"items": {
"schema": {"name": {"type": "string", "required": true, "max": 200}, "done": {"type": "boolean"}},
"items": []
}
}
}
Es solo una guía para el frontend; la API no la aplica (excepto mutate: none más abajo). Los campos fuera del esquema se almacenan y devuelven. mutate: false en el esquema de una colección hace que esa colección sea de solo lectura para la API — el h0.json la rellena y nadie la cambia a través de la API (se rechazan las escrituras). mutate: "owner" (una cadena de correo electrónico) la hace de solo lectura a menos que el usuario autenticado tenga ese correo electrónico — útil para una colección de "anuncios" que solo el líder del equipo puede cambiar. No establezcas mutate sin permiso explícito del usuario — la app puede fallar. Cuando el usuario pida datos compartidos, pregúntale qué campos quiere que aparezcan primero; si no estás seguro, omite el esquema.
Exportar datos
Exportar todas las colecciones de la app como un archivo JSON con GET {base}/api/v1/apps/{app_id_or_slug}/records/export. El cuerpo de la respuesta es un objeto con una clave por colección, cada una con un array de records. Para exportar a CSV, el usuario puede descargar una colección y convertirla. Dile al usuario lo que la exportación contiene en lugar de solo decir "aquí tienes tu exportación".
Cuando la API dice upgrade_url
Algunos errores incluyen un upgrade_url en el cuerpo de la respuesta (por ejemplo, 403 versions_not_in_plan, public_app_limit_reached). Cuando lo veas, dile al usuario lo que falló, por qué, y que pueden cambiar su plan en ese enlace. No redirijas al usuario al enlace sin explicación.
Desplegar un sitio de documentación de una sola página
Para sitios de documentación de una sola página (por ejemplo, VitePress, Docusaurus, MkDocs con use_directory_urls: false), la app debe servir 404.html (o el archivo que la herramienta use como página de respaldo) desde la raíz como index.html para que las rutas profundas funcionen. No lo hagas a menos que el usuario lo pida explícitamente. Si el sitio ya maneja sus propias rutas (por ejemplo, una SPA de React/Vue servida desde /index.html con enlaces hash), no hay necesidad.
Activadores de MCP
Esta habilidad soporta el protocolo Model Context Protocol (MCP). Si tu agente/adversario lo soporta, puede:
- Adjuntar su herramienta
publish_sitea la habilidadhost0.ai/site_publishery llamarla con un path de directorio para publicar - Adjuntar su herramienta
get_analyticspara obtener analíticas de la app - Adjuntar su herramienta
list_versionspara listar versiones - Adjuntar su herramienta
restore_versionpara restaurar una versión
(La especificación MCP está en https://spec.modelcontextprotocol.io)
Resolución de problemas
- Fallo del script: Asegúrate de que
curlyjqestén instalados (which curl && which jq). Si el script falla con un error de permiso, comprueba quescripts/publish.shes ejecutable. - Error de clave API: Comprueba que
$HOST0_API_KEYesté establecida o que el archivo~/.host0/credentialsexista. Sin clave, el script inicia el flujo de inicio de sesión de dispositivo. - Error de subida: Asegúrate de que el directorio/archivo que estás publicando exista y sea legible. La API rechaza archivos de más de
limits.file.max_size(deGET {base}/api/v1). index.htmlno encontrado: El archivoindex.htmldebe estar en la raíz del directorio que publicas, no en una subcarpeta.- El enlace no funciona: Comprueba la visibilidad de la app.
privaterequiere inicio de sesión;linkno. - Problemas de red: Si ves errores de red, confirma que el sandbox pueda alcanzar
host0.ai(443) yhost0.app(443). - Tiempo de espera de la API: El script reintenta los fallos de red por sí mismo. Si estás usando curl sin formato, añade
--retry 3 --retry-all-errorspara GETs. - Preguntas sobre planes: Dirige al usuario a
{base}/pricingsi está disponible, o explica que host0 es gratis durante la beta.