Skip to content

appctl serve

appctl serve starts an HTTP server that does two things:

  1. Serves the embedded web console — an operator UI shipped inside the appctl binary (no separate install, no CDN calls).
  2. Exposes the same agent loop as appctl chat over a JSON HTTP + WebSocket API so you can drive it from your own UI, IDE plugin, or script.

The server runs the same planner, executor, and safety rails as the CLI. All tools, logs, and configuration live in the current app directory (--app-dir, default .appctl).

If the web console shows “Provider missing” or “Tools not synced,” run appctl setup in the same project and restart serve.

Terminal window
appctl serve [OPTIONS]

When the server starts, it prints dev-server style access hints:

appctl serve
Local: http://127.0.0.1:4242
Listening: 127.0.0.1:4242
Token: not set (local-only)
Share: appctl serve --bind 0.0.0.0 --token <secret>
Tunnel: appctl serve --token <secret> --tunnel
Production: keep appctl on loopback behind Caddy/Nginx/Cloudflare

Use the Local URL on the machine running appctl. Use the Network URL only when you intentionally bind beyond loopback and protect the server with a token.

FlagDefaultWhat it does
--bind <ADDR>127.0.0.1Interface to listen on. Use 0.0.0.0 only with --token. Can be set with env APPCTL_BIND.
--port <N>4242TCP port. Use 0 to let the OS pick a free port (the printed URL includes the real port). Env: APPCTL_PORT.
--openonExplicitly open the browser to the web console. This is the default, but the flag is useful in copyable setup steps.
--no-openoffBy default, appctl opens the local UI in your default browser after the server is listening. Pass this to skip that.
--token <STRING>unsetRequire this bearer token on every request. When set, the web UI prompts for it.
--identity-header <NAME>x-appctl-client-idHeader used to tag requests with a caller identity in the activity log.
--tunneloffStart cloudflared tunnel --url ... next to the local server.
--provider <NAME>Override the default provider for this server instance.
--model <NAME>Override the provider’s model.
--read-onlyoffBlock every mutating or destructive tool server-wide.
--dry-runoffSkip real I/O; return simulated events.
--strictoffBlock provenance = "inferred" tools until verified.
--confirmonAuto-approve mutating and destructive tools. serve is non-interactive, so use --read-only or --dry-run when a shared instance must not write.

Flags set on appctl serve are the minimum safety level for every request. Clients may request stricter modes (for example, turning on read_only for one turn), but they cannot relax server-enforced flags.

On macOS, Linux (via xdg-open), and Windows, your default browser opens automatically to the real listening URL (after a very short delay) unless you pass --no-open.

Open http://127.0.0.1:4242/ if you are using the default port. The console ships as a single-page app with four tabs:

  • Chat — streaming conversation with the agent. Tool calls render inline as collapsible cards showing arguments and truncated responses.
  • Tools — searchable list of every tool the agent can call, with its kind, op, safety level, and schema.
  • History — the activity log (same table as appctl history), with expandable rows for arguments and raw response. If clients send the identity header, the session label is recorded there too.
  • Settings — provider status, sync summary, and a field for the auth token when --token is set.

The UI connects over WS /chat for streaming; if WebSocket is blocked it falls back to POST /run for non-streaming completions. Both paths keep multi-turn conversation memory in the server process using the same session_id. WebSocket and HTTP requests can resume the same transcript, and the web console keeps the id across reconnects so the displayed thread matches the model context. If the configured history limit trims older turns, the event stream includes a notice.

All endpoints honour --token (via Authorization: Bearer ... or x-appctl-token) when set. Clients can also send the configured identity header (default x-appctl-client-id) so requests are labeled in history and the web activity panel.

MethodPathPurpose
GET/tools.appctl/tools.json as JSON.
GET/history?limit=<N>Last N audit rows.
GET/schema.appctl/schema.json as JSON.
GET/config/publicRedacted configuration (provider name, model, app name, safety flags). No secrets.
POST/runOne-shot prompt, returns final answer + events.
WS/chatStreaming agent events.

See HTTP endpoints and WebSocket for request and response shapes.

Terminal window
# Local-only operator console
appctl serve
# Share on the LAN behind a token
appctl serve --bind 0.0.0.0 --token "$(openssl rand -hex 24)"
# Start a public tunnel through cloudflared
appctl serve --token "$(openssl rand -hex 24)" --tunnel
# Read-only, dry-run demo instance
appctl serve --read-only --dry-run
# Force a specific provider for a read-only server inside a CI job
appctl serve --provider openai --model gpt-4o-mini --read-only
  • Same machine: open the printed Local URL.
  • Phone or another computer on the same network: run with --bind 0.0.0.0 --token ..., then open the printed Network URL.
  • Outside your network: use --tunnel or run behind a reverse proxy.
  • Never open http://0.0.0.0:...; 0.0.0.0 is a bind address, not a browser destination.

--token protects the appctl web console. It does not log appctl into your target app. Target app/API credentials live in .appctl/config.toml under [target] (for example auth_header = "Authorization: Bearer env:API_TOKEN") or in a target auth/session flow.

For user/session auth, configure a target profile outside chat:

Terminal window
appctl auth target login esubalew --client-id <id> --auth-url <url> --token-url <url>
appctl auth target use esubalew
appctl serve --open

The web Settings page shows the active target profile or auth header state. The browser token for serve and the token appctl sends to your target API are separate credentials.

  • The bind address defaults to 127.0.0.1. Changing it to 0.0.0.0 without also passing --token is a mistake — the server will still start, but anything on your network can use your provider credits.
  • Browser requests with an Origin header must match the daemon host (or forwarded host). This blocks cross-site pages from driving a local daemon.
  • The token is compared byte-for-byte. Pick a long random string.
  • Static assets are embedded into the binary at build time, so there is no need to open any additional ports for asset delivery.