---
name: host0
description: >
  host0 lets agents publish static websites and apps to live urls in
  seconds. publish a folder and get a live url at {slug}.host0.app —
  public at an unguessable url by default, private or invite-only on
  request. use when asked to "publish this", "deploy this", "host this",
  "put this online", "deploy to host0", "make this private", or "share
  this site with someone".
---

# host0

host0 is a cloud for small software: deploying an agent-built app should be as easy as sharing a google doc.

the core primitive is an **app**: publish a directory of static files and get a live url at `https://{slug}.host0.app/` (local dev: `http://{slug}.localhost:3000/`). every app is **public at an unguessable url by default** — unlisted, but anyone with the url can view. private and invite-only sharing are opt-in.

## current docs

full docs live at **https://host0.ai/docs** — read them when the user asks how something works, what's possible, or before telling them a feature is unsupported.

the api also describes itself. the machine-readable index:

```bash
curl -sS {base}/api/v1
```

it returns the current endpoint list, file caps, and the platform contract (what host0 serves today). if this skill text and live api behavior disagree, trust the live api.

to install or update this skill, fetch its two files: `https://host0.ai/skill.md` (this file) and `https://host0.ai/publish.sh` (save as `scripts/publish.sh`, then `chmod +x`).

## requirements

- required binaries: `curl`, `jq` (the script dies with a clear message if either is missing)
- an api key, from `$HOST0_API_KEY` or `~/.host0/credentials` (see "getting an api key")
- bundled helper: `./scripts/publish.sh`

## publish an app

```bash
./scripts/publish.sh {dir} [--name "my app"]
```

prints the live url on stdout (e.g. `https://amber-fox-3k2j.host0.app/`).

under the hood this is a three-step flow: manifest → upload files → finalize. the app is not live until finalize succeeds.

**file structure:** `index.html` must sit at the root of the directory you publish — the directory's contents become the site root. publish `my-site/` where `my-site/index.html` exists, not a parent folder containing `my-site/`.

**deploy built output, not source:** if the project uses a build step (react/vite/next export, .ts/.tsx/.vue/.svelte), build first and publish the output directory (`dist/`, `build/`, `out/`). never publish `node_modules/`, `package.json`, or secrets (`.env`, key files) — the api rejects server code and secrets with an actionable message.

## update an app

```bash
./scripts/publish.sh {dir} --slug {slug}     # or --app-id {uuid}
```

without a flag, the script resolves the app from `.host0/state.json` in the published directory, and creates a new app only when nothing matches. unchanged files are hash-skipped server-side — re-publishing a large site with one edited file uploads one file.

## access control

a host0 app uses one visibility at a time:

- **public** (the default): anyone with the url can view. the slug is unguessable and unlisted.
- **link**: anyone with the link — not listed anywhere.
- **private**: only the owner and invited emails; visitors sign in to view.

set visibility on publish with `--visibility private|link|public`, or any time via the api:

```bash
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": "private"}'
```

invite viewers by email with `POST /api/v1/apps/{app_id_or_slug}/shares` (body `{"emails": ["a@b.com"]}`); remove one with `DELETE /api/v1/apps/{app_id_or_slug}/shares/{email}`.

## getting an api key

1. ask the user for their email address.
2. request a one-time sign-in code:

```bash
curl -sS {base}/api/v1/auth/request-code \
  -H "content-type: application/json" \
  -d '{"email": "user@example.com"}'
```

3. tell the user: "check your inbox for a 6-digit host0 code and paste it here."
4. verify the code and get the api key:

```bash
curl -sS {base}/api/v1/auth/verify-code \
  -H "content-type: application/json" \
  -d '{"email": "user@example.com", "code": "123456"}'
```

5. the returned `api_key` (starts `h0_`) is shown once — save it yourself, immediately (do not ask the user to run this), replacing `h0_your_key_here` with the actual key from the response:

```bash
mkdir -p ~/.host0 && printf '%s' "h0_your_key_here" > ~/.host0/credentials && chmod 600 ~/.host0/credentials
```

## api key storage

the publish script reads the key from these sources (first match wins):

1. `--api-key {key}` flag (ci/scripting only — avoid in interactive use)
2. `$HOST0_API_KEY` environment variable
3. `~/.host0/credentials` file (recommended for agents)

## state file

after every publish, the script writes `.host0/state.json` inside the published directory:

```json
{
  "apps": {
    "amber-fox-3k2j": {
      "app_id": "…",
      "slug": "amber-fox-3k2j",
      "url": "https://amber-fox-3k2j.host0.app/",
      "base_url": "https://host0.ai",
      "updated_at": "2026-08-01T10:22:31Z"
    }
  }
}
```

it exists so repeat publishes update the same app. without `--slug`/`--app-id` the script considers only entries whose `base_url` matches the api base it is publishing to, newest `updated_at` first, and uses the first one the active api key can actually read.

an entry the key cannot read (deleted app, or a state file written by another account) is reported on stderr and dropped from the file rather than silently shadowing a good entry — `publish_result.state_pruned` counts how many were dropped this run. if every cached entry is unusable the script says so before creating a new app, so a `state_pruned` above 0 together with `action=create` means "this published somewhere new" — tell the user, and re-run with `--slug <slug>` if they meant to update an existing app.

if the lookup fails for any other reason (network, 5xx) the script exits instead of creating a duplicate app.

treat the file as internal cache only: never present this local file path as a url, and never use it as the source of truth for visibility or auth.

## what to tell the user

read the `publish_result.*` lines from script stderr:

- always share `publish_result.url` from the current run — that is the live url.
- when `publish_result.visibility=public`: tell the user anyone with the url can view it, and that the url is unguessable and unlisted.
- when `publish_result.visibility=link`: same as public — anyone with the link, listed nowhere.
- when `publish_result.visibility=private`: tell the user only they and invited emails can open it, and viewers sign in with the email that was invited.
- when `publish_result.action=create` and `publish_result.state_pruned` is above 0: say plainly that the cached app could not be reached with this key so a **new** app was created at a new url, and offer to re-publish with `--slug <slug>` if they wanted the old one.
- when a publish fails: if the run had just created the app, the script deletes it again so no empty app is left behind, and reports `publish_result.cleanup=deleted` (with `publish_result.cleanup_slug`). if `cleanup=failed`, retry the cleanup yourself with `DELETE {base}/api/v1/apps/{cleanup_slug}` — or tell the user the app name from `publish_result.cleanup_slug` and that the app page in the dashboard has a delete button for apps that never went live. no `cleanup*` line appears on a successful publish, or when the run updated an app it did not create.
- mention the dashboard at the platform origin (e.g. `https://host0.ai/dashboard`) for managing apps in a browser.
- never point users at `.host0/state.json`.

## important — never do these

- never commit `~/.host0/credentials` or `.host0/state.json` to source control.
- never echo the api key into the conversation unless the user explicitly asks for it.
- avoid `--api-key` in interactive sessions; the credentials file is the preferred storage.
- never send credentials to a non-default `--base-url`. the script refuses; only pass `--allow-nonhost0-base-url` when the user has explicitly confirmed the endpoint (e.g. local development).
- deploy built output, never project source, `node_modules/`, or `.env` files.

## publish.sh options

| flag | description |
| --- | --- |
| `--name {text}` | app name when creating (default: directory basename) |
| `--slug {slug}` | update the existing app with this slug |
| `--app-id {uuid}` | update the existing app with this id |
| `--visibility {mode}` | set `private`, `link`, or `public` after publish |
| `--client {name}` | agent name for attribution (sends `x-host0-client: {name}/publish-sh`) |
| `--api-key {key}` | api key override (prefer the credentials file) |
| `--base-url {url}` | api base (default: `https://host0.ai`) |
| `--allow-nonhost0-base-url` | allow sending auth to a non-default `--base-url` |

## beyond publish.sh

everything else — listing apps, app detail, shares, deployment status — is plain rest per the live index at `GET {base}/api/v1`, authenticated with `authorization: bearer h0_…`.

`DELETE /api/v1/apps/{app_id_or_slug}` removes an app that has never gone live (the same cleanup the publish script does for itself). an app with a live deployment returns 409 `app_is_live` — make it private instead. use it whenever you create an app and the deploy then fails or is abandoned: the empty app still counts against the account's app cap. the dashboard app page carries the same delete button for never-live apps, so a user who is stuck has a way out without you.
