Advanced
Headless mode
No UI, background-only — for servers, CI, webhook handlers.
CLI vs headless
The CLI is one-shot — run, exit. Headless is a long-running daemon — it listens on a socket / HTTP port, accepts tasks, keeps sessions warm, persists state. Rule of thumb: CLI for fire-and-forget scripting, headless for webhooks, chatbots, long-running workflows.
Headless skips the renderer and the Electron shell, so it weighs roughly one-fifth of the desktop process and boots in the 200ms range. Comfortable for 24/7 production.
Architecturally, headless is the "backend half" of the desktop app running on its own — agent core, tool runtime, provider routing are all there; only the React UI is missing.
Starting the daemon
# Start headless daemon, bound to a vault
kition daemon start --vault /var/vaults/team --listen 127.0.0.1:7878
# Submit a task to it
kition daemon submit "process pending tickets"
# Stream events from a running task
kition daemon tail task_19af --follow
# Watch the daemon log
kition daemon logs -f
# Stop gracefully (drains in-flight tasks first)
kition daemon stop --drainHTTP interface
The daemon listens on a local HTTP port and accepts JSON-RPC-style task submissions. Webhook services, Slack bots, internal tools can POST to it directly. Most production setups front it with an auth-aware reverse proxy.
Need an event stream? Set stream: true and the daemon returns NDJSON / SSE — every token and tool call pushed in real time.
curl -s http://127.0.0.1:7878/v1/tasks \
-H 'content-type: application/json' \
-d '{
"prompt": "Triage this issue and label it.",
"context": { "issue_url": "https://github.com/org/repo/issues/42" },
"model": "claude-opus-4-7",
"stream": true
}'
# Submit and poll
TASK=$(curl -s ... | jq -r '.task_id')
curl -s http://127.0.0.1:7878/v1/tasks/$TASKDeployment tips
- Run under systemd / launchd with
Restart=on-failure - Mount the vault on a persistent volume; snapshot regularly (borgbackup / restic / EBS)
- Enable the Prometheus exporter (
--metrics 127.0.0.1:9090) for QPS, p95, token spend - Inject API keys via systemd
EnvironmentFile=, never bake them into the unit - Front with nginx / Caddy for TLS termination and IP allowlisting
- Log to stdout; let journald / vector ship it onward
- An official Docker image ships at
ghcr.io/kition-ai/headless:latest
systemd unit example
[Unit]
Description=Kition headless agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=kition
EnvironmentFile=/etc/kition/secrets.env
ExecStart=/usr/local/bin/kition daemon start \
--vault /var/vaults/team \
--listen 127.0.0.1:7878 \
--metrics 127.0.0.1:9090
Restart=on-failure
RestartSec=3
LimitNOFILE=65536
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/var/vaults/team /var/log/kition
[Install]
WantedBy=multi-user.targetObservability and limits
/healthzreturns 200 / 503 — slot it into a k8s liveness probe/metricsis Prometheus-formatted: queue depth, active sessions, tokens / minute--max-concurrent Ncaps concurrent tasks, default 4--task-timeout 10mceilings a single task to prevent runaways- OpenTelemetry: set
OTEL_EXPORTER_OTLP_ENDPOINTto auto-export traces - Spend budget:
--budget-daily 50caps daily token cost in USD; over-budget submissions return 429
Embedding into your own product
Want to embed the Kition agent inside your own product — without exposing our UI, just borrowing the capability? The headless daemon is the entry point. We also ship a Go sidecar binary (kition-sidecar) that wraps the daemon protocol in gRPC plus a stable SDK, so your Electron / Tauri / server product can integrate cleanly.
This is all part of Kition Pro — the commercial desktop license also covers embedding, billed per end-user seat.
package main
import (
"context"
"log"
"github.com/kition-ai/sidecar-go/kition"
)
func main() {
c, err := kition.Dial("unix:///run/kition.sock")
if err != nil { log.Fatal(err) }
defer c.Close()
resp, err := c.Run(context.Background(), &kition.RunRequest{
Vault: "/var/vaults/team",
Prompt: "Summarize today's incidents",
Model: "claude-opus-4-7",
})
if err != nil { log.Fatal(err) }
log.Println(resp.Text)
}