ck publish end-to-end. How the surface comes up, where it can fail, and how to fix each failure mode. Read this once before your first publish; come back to it when ck publish --diagnose shows a FAIL row.

What ck publish does

ck publish is a single command with three modes — dry-run (default), --execute, and --diagnose.

Dry-run mode (default)

ck publish

Renders the four deploy templates into the project’s .deploy/ directory with token replacement, prints a publish plan, and exits 0. Touches no system state. Always run dry-run first.

The four files rendered are:

PathPurpose
.deploy/serve.pyStatic-HTML FastAPI server with Basic Auth and /_system/<path>. Used by projects that compile content to HTML at build time (architecture-style chapter sites). Projects that render content live from MDX (essay-series) ship their own server and ignore this file.
.deploy/run-service.shWrapper that sources ~/.config/<project>.env and execs the server under uv run.
.deploy/launchd/com.highline.<slug>.plistThe launchd plist that brings the service up at login and respawns it on crash.
.deploy/cloudflared/<slug>-rule.ymlThe single ingress rule the cloudflared config needs.

Execute mode

ck publish --execute

Runs the full pipeline:

  1. Renders the four files (same as dry-run).
  2. Copies the plist to ~/Library/LaunchAgents/com.highline.<slug>.plist.
  3. launchctl bootstrap gui/$(id -u) <plist>.
  4. launchctl kickstart -k gui/$(id -u)/com.highline.<slug>.
  5. Splices the cloudflared rule into ~/.cloudflared/config.yml above the catch-all 404 service. (If a rule for the surface already exists, it is left untouched and step 6 is a no-op.)
  6. launchctl kickstart -k gui/$(id -u)/com.cloudflared to reload the tunnel.
  7. Polls https://<surface>/health once every 3 seconds for up to 90 seconds. Exits 1 with diagnostics if the surface does not return 200 within the window.
  8. Appends a publish row to docs/agent-runtime/ledger.md.

Diagnose mode

ck publish --diagnose

Runs the four standard checks (DNS, cloudflared launchd state, auth-pair env file, surface health) without writing anything. Use this when something is broken; the four-check output narrows the problem to a single sub-system.

Pre-flight (one-time per host)

Before the first ck publish --execute on a new machine:

1. cloudflared tunnel exists

cloudflared tunnel list

If empty, create one:

cloudflared tunnel login                                  # browser auth, one-time
cloudflared tunnel create highline-creator                # name is conventional; pick anything

This writes ~/.cloudflared/<tunnel-id>.json (the credential) and prints the tunnel UUID.

2. Tunnel config exists

If ~/.cloudflared/config.yml does not exist:

cat > ~/.cloudflared/config.yml <<EOF
tunnel: <tunnel-uuid-from-step-1>
credentials-file: $HOME/.cloudflared/<tunnel-uuid>.json
ingress:
  # ck publish splices per-project rules above this catch-all.
  - service: http_status:404
EOF

3. cloudflared as a launchd service

sudo cloudflared service install                          # one-time
launchctl print gui/$(id -u)/com.cloudflared >/dev/null && echo "loaded" || echo "not loaded"

If the print check returns “not loaded,” check sudo cloudflared service status. The launchd service is what ck publish --execute kickstarts in step 6; without it, the tunnel will not reload after the rule splice.

4. DNS routing for the surface

Each new surface needs a one-time DNS record on the tunnel:

cloudflared tunnel route dns highline-creator <surface>   # e.g. tyler-essays.highline.work

ck publish --execute does not run this step automatically because it is a destructive change to the cloudflare zone (creates a CNAME). Do it once before the first publish for a given surface.

Auth-pair env file

The Basic Auth credentials live at ~/.config/<project-slug>.env, mode 600, never committed.

The first-hour runbook generates this file with python3 -c 'import secrets; print(secrets.token_urlsafe(24))'. The format is:

export ARCH_USER=tyler
export ARCH_PASSWORD=...
export ARCH_PORT=18210

run-service.sh sources this file before exec’ing the server. If any of the three keys is missing, ck publish aborts with a remediation message before touching launchd or cloudflared.

To rotate the password without taking the surface down:

PROJECT_SLUG=tyler-essays
NEW_PASS=$(python3 -c 'import secrets; print(secrets.token_urlsafe(24))')
sed -i.bak -E "s|^export ARCH_PASSWORD=.*|export ARCH_PASSWORD=$NEW_PASS|" ~/.config/$PROJECT_SLUG.env
rm ~/.config/$PROJECT_SLUG.env.bak
chmod 600 ~/.config/$PROJECT_SLUG.env
launchctl kickstart -k gui/$(id -u)/com.highline.$PROJECT_SLUG
curl -sIu $USER:$NEW_PASS https://$PROJECT_SLUG.highline.work/   # 200 expected

The full rotation runbook (including tunnel-credential rotation) is at ~/Work/creator-kit/deploy/cloudflared/CREDENTIAL_ROTATION.md.

fonts.candlefish.ai allowlist

The placeholder Candlefish chrome at examples/essay-series/public/_system/style.css pulls Berkeley Mono from https://fonts.candlefish.ai/berkeley-mono/{Regular,Bold}.otf. That worker is CORS-gated: it only serves the font to origins on its allowlist.

Before the first publish from a new surface:

  1. Confirm the font URL works open-net: curl -sI https://fonts.candlefish.ai/berkeley-mono/Regular.otf and expect HTTP/2 200. If it does not, the worker is down (escalate to the Candlefish admin) and ck doctor flags it.
  2. Confirm the surface’s origin is on the allowlist. If not, the page loads with the system monospace fallback (Menlo or SFMono-Regular). The page works, it just does not look right.
  3. To add the surface to the allowlist: edit ~/Work/builders-warehouse/infra/fonts-worker/wrangler.toml, add <surface> to the ALLOWED_ORIGINS array, and re-deploy the worker (cd infra/fonts-worker && wrangler deploy). This requires Cloudflare account access; coordinate with the org admin if you do not have it.

The full vendored Candlefish Design System landed in M2 W6 at design/_system/, but the Berkeley Mono @font-face rules in _system.css still source from fonts.candlefish.ai at runtime. The licensed font binary cannot be redistributed in the kit. The CORS-gated worker stays in the loop.

Common failure modes

SymptomLikely causeFix
ck publish --execute exits 1 with “did not return 200 within 90s”DNS not routed (cloudflared tunnel route dns was never run for this surface), or tunnel did not reloadRun ck publish --diagnose. If DNS check FAILs: cloudflared tunnel route dns <tunnel> <surface>. If tunnel-state check FAILs: launchctl kickstart -k gui/$(id -u)/com.cloudflared. Then ck publish --execute again — it is idempotent.
[FAIL] auth pair: ~/.config/<slug>.env missing one of ARCH_USER, ARCH_PASSWORD, ARCH_PORTEnv file partially writtenRe-run the env-file step from the first-hour runbook. The full file template is in deploy/launchd/com.highline.PROJECT_SLUG.plist.tmpl (top-comment block).
Surface returns 401 even with the right passwordTrailing whitespace in ARCH_PASSWORD (common when copy-pasting from a password manager)bash -c 'echo "[$ARCH_PASSWORD]"' < <(. ~/.config/<slug>.env) — the brackets reveal trailing spaces. Re-write the env file.
Surface returns 502 from cloudflaredThe local server is not running. Either the launchd plist failed to bootstrap, or the server crashed.tail /tmp/<slug>.err for crash logs. launchctl print gui/$(id -u)/com.highline.<slug> for service state. If the plist itself is malformed, plutil -lint .deploy/launchd/com.highline.<slug>.plist.
Surface returns 200 on /health but 404 on /The server is up but the project’s content is missing. For essay-series projects, content/essays/*.mdx must exist. For static-HTML projects, index.html must exist at the project root.Add content. The 404 message includes a list of pages the server saw at startup; cross-check.
Page loads with system monospace instead of Berkeley MonoSurface origin not on the fonts.candlefish.ai allowlistSee the fonts.candlefish.ai allowlist section above. Page is functional in the meantime.
ck publish runs cleanly but ck validate --live FAILs on live.routesThe kit’s live-routes contract (docs/contracts/live-routes.json) names a route the project does not actually serve, or the contract is out of dateEither fix the route or update the contract. The contract is authoritative; the surface should match it, not the other way around.

When ck publish is the wrong tool

ck publish writes to launchd and ~/.cloudflared/config.yml. That is appropriate when the surface lives on a single Mac (laptop or Mac Studio) and the creator owns the cloudflared tunnel.

It is the wrong tool when:

See also