Hawt CLI
Command-line tools for publishing static sites on hawt.site. This repository builds two binaries:
hawt-site(hs): the site publishing CLI. Start here.hawt: the wider Hawt Cloud suite CLI (see below).
Both share one credential store, so logging in with either logs you in to both.
Round Trip in 60 Seconds
Install, create an account, publish a site, look at it, take it down:
# 1. Install hawt-site and the `hs` symlink into ~/.local/bin
curl -fsSL https://api.hawtcloud.com/install-hawt-site-cli.sh | sh
# 2. Onboard: redeem your invite (this saves your token)
hs register -u bob_dev -i hawt_inv_9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c
✔ Registered as bob_dev
✔ Token saved to /Users/bob/.config/hawt/config.json
This token is your only credential — there are no passwords and no
recovery. Save a copy somewhere safe:
hawt_pat_...
Next: hs deploy ./your-site
# 3. Deploy
mkdir my-site && echo '<h1>Hello, hawt.site</h1>' > my-site/index.html
hs deploy ./my-site
🔥 Uploading ./my-site (182 B)...
✔ Deployed
URL: https://hawt.site/hawt/VdUdSS1
Site ID: VdUdSS1
Files: 1
Size: 25 B
Expires: 13 days
# 4. Look at it, then clean up
open https://hawt.site/hawt/VdUdSS1
hs rm VdUdSS1
You don't have to configure an endpoint. https://api.hawtcloud.com/v1 is
compiled in.
Everything hs Can Do
| Command | Description |
|---|---|
hs register |
Redeem an invite code and create an account |
hs login |
Save a Personal Access Token |
hs whoami |
Show the current identity and configuration |
hs deploy <path> |
Deploy a file or directory as a static site |
hs list |
List your deployed sites |
hs info <site-id> |
Show details for one site |
hs rm <site-id> |
Delete a site |
Every command accepts these global flags:
| Flag | Description |
|---|---|
--json |
Machine-readable JSON on stdout; the API response body as-is where there is one |
-q, --quiet |
Just the essential value (a URL, a site ID, a username) |
--no-color |
Disable ANSI styling (also honors NO_COLOR and a non-TTY stdout) |
--version |
Version, commit, and build date |
--json and --quiet can't be combined.
Onboarding
Have an invite? Register. The email is optional unless the invite is bound to one:
hs register -u bob_dev -i hawt_inv_... -e bob@example.com
Registration returns your Personal Access Token exactly once. There are no
passwords and no recovery. If you lose the token, an admin has to mint you a
fresh invite. The raw token is printed only to an interactive terminal, and in
--json output so a script can capture it.
Already have a token? Log in. The token is checked against the API before it is saved, so a bad paste fails right away:
hs login # prompts with echo disabled
echo "$PAT" | hs login # CI: reads one line from stdin
Or skip the config file entirely:
export HAWT_CLOUD_API_KEY=hawt_pat_...
Something not working? hs whoami is the first thing to run. It shows who
you are, which credential is in use and where it came from, the endpoint, and
the config file path:
$ hs whoami
🔥 Hawt Site
Logged in as: bob_dev (bob@example.com)
API key: hawt...4b4c (config file)
Endpoint: https://api.hawtcloud.com/v1 (default)
Config file: /Users/bob/.config/hawt/config.json
Deploying
hs deploy ./dist # → https://hawt.site/hawt/<id>
hs deploy ./dist --slug portfolio # → https://hawt.site/hawt/portfolio
hs deploy ./dist --user-scope # → https://hawt.site/bob_dev/<id>
hs deploy index.html # a single file
hs deploy site.zip # an existing zip, uploaded as-is
open "$(hs deploy ./dist -q)" # -q prints only the URL
-
Directories lose their top-level folder, so
dist/index.htmlis served at the root of the site. -
Uploads are capped at 50 MB. Oversized payloads are rejected locally, before anything is sent, with exit code 5 (the same as the server's
413). Each account has a 500 MB storage quota. -
Sites expire after 14 days.
-
You can't redeploy in place yet. To reuse a slug, remove the old site first:
hs rm portfolio -y && hs deploy ./dist --slug portfolio
Uploads over 1 MB show a progress bar on an interactive terminal.
Managing Sites
hs list # table of your sites
hs list --limit 10 --offset 10 # paginate (limit 1–100, default 50)
hs info VdUdSS1 # full details
hs rm VdUdSS1 # asks for confirmation on a terminal
hs rm VdUdSS1 -y # no prompt
$ hs list
🔥 Deployed Sites
SITE ID URL FILES SIZE EXPIRES STATUS
portfolio https://hawt.site/hawt/portfolio 12 1.4 MB 13 days active
VdUdSS1 https://hawt.site/hawt/VdUdSS1 1 25 B 13 days active
Showing 2 of 2 total sites.
$ hs info VdUdSS1
🔥 Site Details
Site ID: VdUdSS1
URL: https://hawt.site/hawt/VdUdSS1
Status: active
Owner: bob_dev
Path Type: hawt
Files: 1
Size: 25 B
Created: Sun, 13 Sep 2026 16:10:00 UTC
Expires: Sun, 27 Sep 2026 16:10:00 UTC (13 days)
hs rm never prompts when stdin isn't a terminal or when --json is set, so it
won't hang in a pipeline.
Scripting
Results go to stdout. Progress, warnings, and errors go to stderr. Under
--json, stdout is the API response body as-is:
# Deploy and capture fields
hs deploy ./dist --json | jq -r '.url, .expires_at'
# Delete every site you own
hs list -q | xargs -n1 hs rm -y
# Find sites expiring within two days (macOS date; on Linux: date -u -d +2days)
hs list --json | jq -r --arg t "$(date -u -v+2d +%FT%TZ)" \
'.sites[] | select(.expires_at < $t) | .site_id'
# Capture the one-time token at registration
TOKEN=$(hs register -u ci_bot -i "$INVITE" --json | jq -r .api_token)
Under --json, errors are also JSON on stderr: {"error": "...", "status": <int|null>}.
Exit codes tell failures apart:
| Code | Meaning |
|---|---|
| 0 | Success (including a declined confirmation) |
| 1 | Unexpected internal error |
| 2 | Usage error: bad flags or arguments |
| 3 | Local filesystem error: path missing, unreadable, config unwritable |
| 4 | Authentication error: no credential, or 401 |
| 5 | API rejected the request: 400, 403, 404, 409, 413, or the local size check that stands in for a 413 |
| 6 | Network error: DNS, connection refused, timeout |
Configuration
Credentials live in ~/.config/hawt/config.json (mode 0600), shared with
hawt. hs writes only api_key and preserves every other field.
| Setting | Precedence, highest first |
|---|---|
| Credential | HAWT_CLOUD_API_KEY, then api_key in the config file |
| Endpoint | --api-url (hidden, for development and self-hosting), then HAWT_CLOUD_API_URL, then api_url in the config file, then the compiled https://api.hawtcloud.com/v1 |
Whenever the endpoint isn't the default, every command prints a banner to stderr so a local deploy is never mistaken for a production one:
using endpoint http://localhost:8000/v1 (HAWT_CLOUD_API_URL)
The design and full behavior of hs are specified in
docs/specs/SPEC-hs-cli.md.
Installation Options
# Installer: hawt-site + hs symlink into ~/.local/bin
curl -fsSL https://api.hawtcloud.com/install-hawt-site-cli.sh | sh
# HAWT_INSTALL_DIR=/somewhere to install elsewhere; HAWT_VERSION=vX.Y.Z to pin
# Go toolchain
go install git.hawt.cloud/hawtcloud/hawt-cli/cmd/hawt-site@latest
go install git.hawt.cloud/hawtcloud/hawt-cli/cmd/hawt@latest
# From a checkout
just build # both binaries → bin/
just build-hawt-site # → bin/hawt-site
just build-hawt # → bin/hawt
Prebuilt archives for each release are described in Release Assets.
The hawt Suite CLI
hawt is the entry point for the wider Hawt Cloud suite. Its site commands
overlap with hs. hawt site upload is the longer form of hs deploy.
# Authentication & configuration
hawt auth login # prompt for a token
hawt auth info # show the active session
hawt register --username bob_dev --email bob@example.com --invite-code hawt_inv_...
hawt config set token hawt_pat_...
hawt config set api-url https://api.hawtcloud.com/v1
# Sites
hawt site upload ./dist # → /hawt/<site_id>
hawt site upload ./dist --slug portfolio --user-scope
hawt site list --limit 10 --offset 0
hawt site info <site_id> # alias: get
hawt site delete <site_id>
HAWT_CLOUD_API_KEY overrides the saved token for hawt as well.
Running Unit Tests
just test # go test -v ./...
The Hurl HTTP integration suite exercises the API, not these CLIs, and stays in
the hawt-site repository.
Release Assets
Each published release carries one archive per CLI per platform, built by
.gitea/workflows/release-cli.yml. The
naming is a stable contract that install-hawt-site-cli.sh and any other
downloader can rely on:
<cli>_<tag>_<goos>_<goarch>.tar.gz
<cli>_<tag>_<goos>_<goarch>.tar.gz.sha256
| Field | Values |
|---|---|
<cli> |
hawt, hawt-site |
<tag> |
the release tag, e.g. v0.4.0 |
<goos> |
darwin, linux |
<goarch> |
amd64, arm64 |
Eight archives per release. Each holds exactly the one binary it is named for, placed at the archive root — no wrapper directory, so installing is an untar straight into a bin directory:
TAG=v0.4.0
BASE=https://git.hawt.cloud/hawtcloud/hawt-cli/releases/download/${TAG}
NAME=hawt_${TAG}_linux_amd64.tar.gz
curl -fsSLO "${BASE}/${NAME}"
curl -fsSLO "${BASE}/${NAME}.sha256"
sha256sum -c "${NAME}.sha256"
tar -xzf "${NAME}" -C ~/.local/bin
The .sha256 file records the bare archive name, so sha256sum -c works from
whichever directory you downloaded into (on macOS, shasum -a 256 -c).
hs is not a separate asset: it is a symlink to hawt-site that the installer
creates. The two CLIs are built from a single tag and share a credential store,
but they install independently — take only the one you need.
Binaries are static (CGO_ENABLED=0), built with -trimpath, and stamped with
the tag, short commit, and build date:
$ hawt --version
hawt v0.4.0 (commit b32d43a, built 2026-09-12T14:19:09Z)
Layout
hawt-cli/
├── cmd/hawt/ → binary: hawt (suite CLI)
├── cmd/hawt-site/ → binary: hawt-site (installed as hs)
├── hawtcmd/ hawt commands
├── hscmd/ hs commands
└── internal/
├── client/ HTTP client and typed API/network errors
├── config/ config file and credential resolution
├── ui/ lipgloss brand palette and formatting
└── zipper/ payload compression