Local HTML hosting for AI coding agents.
An agent (Claude Code, Pi the coding agent, anything that can shell out) generates a directory of static HTML, runs crate push ./dir name, and gets back http://localhost:7777/name/. The human opens that URL and sees the rendered artifact — a plan, an explainer, a code review — instead of skimming raw markdown in a chat window.
"Pi" throughout this repo means the Pi coding agent, not Raspberry Pi hardware.
Status: pre-1.0 — laptop-only out of the box, optional Docker for persistence, optional Tailscale exposure via tsdproxy.
Requires Go 1.26+ and Task (brew install go-task).
task build # produces ./bin/crate and ./bin/crated
./bin/crated & # starts the daemon, generates config + token on first run of either binary
./bin/crate status # confirm the daemon is up
./bin/crate push ./some-html-dir my-site
./bin/crate open my-siteDefault URL is http://localhost:7777/. The daemon binds to 127.0.0.1 only (override with CRATE_LISTEN_ADDR).
Release archives for macOS and Linux are attached to each GitHub Release. After placing the crate binary on your PATH, later client updates are one command:
crate updateThe command selects the archive for the current OS and architecture, verifies it against the release's checksums.txt, and replaces the client binary with rollback protection. It does not update a Dockerized crated; update that through its container image.
For a persistent daemon that survives terminal sessions:
task docker:build # build the crate-html image
task docker:up # start crated on :7777 with persistent volumes
task docker:token # print just the bearer token (via `crate token` inside the container)
task docker:logs # tail the container's logs
task docker:down # stop the container (volumes preserved)The host crate CLI talks to the dockerized daemon via env vars — no config-file editing required:
eval "$(task docker:env)" # exports CRATE_TOKEN and CRATE_BASE_URL
./bin/crate ls # now talks to the dockerized daemon
./bin/crate push ./my-site demoThe env vars override anything in config.yaml for the lifetime of the shell. Unset them or open a new terminal to go back to the host-side daemon.
task docker:nuke deletes the volumes too — use when you want a clean slate.
The same image can run as a private broker and a public, read-only web server:
task docker:split:up
eval "$(task docker:split:env)" # API=:7778, public URLs=:7777
./bin/crate push ./my-site demo
task docker:split:downThe broker owns uploads, deletes, tokens, and expiry cleanup. The web process
only serves the index and crate URLs from the shared storage volume, mounted
read-only. They can use separate hostnames or ports; a shared reverse proxy is
optional. The all-in-one crated behavior remains the default.
crated writes structured daemon logs to stderr. Broker API requests include a
request ID, route, status, duration, byte counts, and a secret-safe actor
identity. Successful site and token mutations emit a second event with the
affected resource details. Public site traffic and /healthz checks are not
request-logged.
Text output is the default. Use crated --log-format=json or set
CRATE_LOG_FORMAT=json for ingestion by a log collector. In Docker, view the
same stream with task docker:logs or task docker:split:logs.
For a real hostname like https://crate.<your-tailnet>.ts.net/, add three tsdproxy labels to the crated service in docker-compose.yml:
labels:
tsdproxy.enable: "true"
tsdproxy.name: "crate"
tsdproxy.port.1: "443/https:7777/http"tsdproxy auto-provisions the Tailscale node and TLS cert. Full setup including prerequisites: docs/deploy.md.
crate push <src> <name> upload a site; expires after 24h by default
crate push <src> <name> --expires 90m set a custom lifetime
crate push <src> <name> --expires never retain the site indefinitely
crate push <src> <name> -o push, then open the URL in a browser
crate ls list deployed sites
crate rm <name> remove a site
crate open <name> open the site in your default browser
crate status show daemon version and site count
crate version show the client version (`crate --version` also works)
crate update update the client from the latest GitHub Release
crate token print the root bearer token from the loaded config
crate token create <name> mint a named API token (shown once; root token required)
crate token ls list minted tokens (id, created, expires, last used)
crate token revoke <id|name> revoke a minted token immediately
Site names must match ^[a-z0-9][a-z0-9._-]{0,62}$. A global --config <path> flag (on both crate and crated) overrides the XDG config-file location. crated --role=all|broker|web selects the runtime role; CRATE_ROLE is the container-friendly equivalent.
crated --log-format=text|json selects the daemon log encoding;
CRATE_LOG_FORMAT is the container-friendly equivalent.
CRATE_API_URL tells the CLI where the broker lives. CRATE_PUBLIC_URL is the
human-facing origin printed after a push and used by crate open.
CRATE_BASE_URL remains backward compatible and supplies both when the new
variables are unset.
Public reads reject an elapsed site immediately, even if the broker is down. The broker checks expiry deadlines once a minute and removes elapsed storage. Expiry metadata is stored separately from site assets, so it is not publicly served. Sites created by older versions without expiry metadata are retained indefinitely.
Two kinds of credential:
- Root token — generated into
config.yamlon first run. Authenticates everything, and is the only credential accepted for token management (/api/tokens). - Named API tokens — minted per client (
crate token create pi-agent), shapedcrate_<id>_<secret>. They can push, list, and remove sites but can never mint or revoke tokens. Only a SHA-256 hash is stored (intokens.yamlnext toconfig.yaml), so the secret is shown exactly once at creation. Optional--expires 720h; revocation (crate token revoke) takes effect immediately, no daemon restart.
Give each agent/machine its own named token: crate token ls shows per-client last used, and revoking one client doesn't re-key the others. Uploads are capped at 100 MiB per push by default (max_upload_bytes in config.yaml).
cmd/crate/ CLI (kong) — push, ls, rm, open, status, token, update
cmd/crated/ HTTP daemon
internal/buildinfo/ shared client/daemon release version
internal/updater/ checksum-verified client self-update
internal/wire/ request/response types — the API contract
internal/config/ XDG config loader, first-run token generation
internal/token/ named API tokens — tokens.yaml store, hashed secrets
internal/storage/ filesystem ops, tar in/out, atomic site replacement
internal/server/ net/http handlers — bearer auth on /api/sites (status is public), static serve
internal/cliclient/ HTTP client used by `crate`
internal/builtin/ sites embedded in the binary (e.g. cratesplainer)
testdata/sites/ site fixtures used by smoke tests
.claude/skills/ Claude Code skill wrapping `crate push`
One Go module. internal/wire is the seam — both binaries import it; they don't import each other.
crated ships with one site embedded in the binary via go:embed:
/cratesplainer/— a deliberately-overexplained guide to using crate, useful when a new user lands on the daemon for the first time.
Disk sites win conflicts: if you crate push ./my-version cratesplainer, your copy is served instead. crate rm cratesplainer removes your override and the built-in resurfaces.
Paths resolve via github.com/adrg/xdg, which follows the XDG Base Directory Spec on Linux and the platform-native location on macOS.
| Purpose | Linux | macOS |
|---|---|---|
| Config | ~/.config/crate/config.yaml |
~/Library/Application Support/crate/config.yaml |
| Sites | ~/.local/share/crate/sites/ |
~/Library/Application Support/crate/sites/ |
| Logs | ~/.local/state/crate/log/ |
~/Library/Application Support/crate/log/ |
To force pure-XDG layout on macOS, set XDG_CONFIG_HOME / XDG_DATA_HOME / XDG_STATE_HOME in your shell.
task build # go build ./cmd/...
task test # go test ./...
task vet # go vet ./...
task smoke # process-level CLI/API integration suite
task smoke:s3 # real S3 backend tests using rustfs
task docker:e2e # isolated container E2E: local volume + scoped-IAM rustfs
task tidy # go mod tidydocs/architecture.md— how it works end-to-end (logical view, API, push/serve protocols, deployment topology)docs/deploy.md— four deployment shapes (local, Docker, Docker + tsdproxy on Tailscale, object storage)docs/s3-storage.md— running on S3-compatible object storage: modes, requirements, gotchasdocs/design.md— why the architecture is shaped the way it isdocs/naming.md— naming rationale and availability checkdocs/releases.md— Conventional Commit and Release Please workflowdocs/roadmap.md— near-term work and explicit non-goalsexamples/— optional split-role, S3, custom-index, and tsdproxy configurations
ClawBox from OpenClaw — a plug-and-play appliance hosting an agent-facing service. crate-html is the same shape, specialized for HTML, and runs locally rather than as an appliance.
MIT. Share, modify, ship it in your product — just keep the license notice with any substantial copy.