How to spin up the local main-branch czcode backend and test it with curl / fetch. Aimed at a running czcode agent iterating on backend fixes without rebuilding the TUI.
All examples use plain shell + curl. Writing TypeScript files is a last resort (see Section 8).
# From repo root. Starts the LOCAL main-branch backend in the background.
PASS=$(openssl rand -hex 16)
CZCODE_SERVER_PASSWORD="$PASS" ~/.bun/bin/bun dev serve --port 0 >/tmp/czcode-serve.log 2>&1 &
echo $! >/tmp/czcode-serve.pid
while ! grep -q "kilo server listening" /tmp/czcode-serve.log 2>/dev/null; do sleep 0.1; done
PORT=$(grep -oE "listening on http://[^:]+:[0-9]+" /tmp/czcode-serve.log | grep -oE "[0-9]+$")
AUTH="Authorization: Basic $(printf 'kilo:%s' "$PASS" | base64 | tr -d '\n')"
BASE="http://127.0.0.1:$PORT"
# Call any endpoint
curl -sS -H "$AUTH" -H "x-kilo-directory: $PWD" "$BASE/global/health"
# Stop when done
kill "$(cat /tmp/czcode-serve.pid)" 2>/dev/null || true
rm -f /tmp/czcode-serve.pid /tmp/czcode-serve.logTesting local backend fixes against a real running server, talking to it over HTTP the same way the TUI and czcode run --attach do — but without any of those clients. Every request is a curl the agent can copy-paste.
For in-process tests (no socket, fastest loop) see packages/opencode/test/kilocode/server/permission-allow-everything.test.ts for the Server.Default().app.request(...) pattern. That's the right tool inside the packages/opencode/ test suite; this doc is for out-of-process HTTP testing.
| Command | What it runs |
|---|---|
czcode serve |
The installed production CLI on $PATH. Not the code in this repo. |
~/.bun/bin/bun dev serve … (repo root) |
The local main-branch backend from this worktree. This is what you want. |
Root package.json defines "dev" as the full bun run --cwd packages/opencode --conditions=browser src/index.ts invocation, so bun dev <args> forwards <args> to the local CLI entry point without touching the installed binary.
bun dev imports the source directly — no rebuild is needed between code edits. Just kill the running server and relaunch.
CZCODE_SERVER_PASSWORD=$(openssl rand -hex 16) \
~/.bun/bin/bun dev serve --port 0 >/tmp/czcode-serve.log 2>&1 &
echo $! >/tmp/czcode-serve.pid
while ! grep -q "kilo server listening" /tmp/czcode-serve.log; do sleep 0.1; done
PORT=$(grep -oE "listening on http://[^:]+:[0-9]+" /tmp/czcode-serve.log | grep -oE "[0-9]+$")CZCODE_SERVER_PASSWORD=secret \
~/.bun/bin/bun dev serve --port 4096 --hostname 127.0.0.1 \
>/tmp/czcode-serve.log 2>&1 &
echo $! >/tmp/czcode-serve.pid
while ! grep -q "kilo server listening" /tmp/czcode-serve.log; do sleep 0.1; done
PORT=4096Omit CZCODE_SERVER_PASSWORD entirely. The server prints a warning and the auth middleware is bypassed — fine for throwaway local testing, never for anything else.
~/.bun/bin/bun dev serve --port 0 >/tmp/czcode-serve.log 2>&1 &
echo $! >/tmp/czcode-serve.pid
while ! grep -q "kilo server listening" /tmp/czcode-serve.log; do sleep 0.1; done
PORT=$(grep -oE "listening on http://[^:]+:[0-9]+" /tmp/czcode-serve.log | grep -oE "[0-9]+$")
BASE="http://127.0.0.1:$PORT"
# no AUTH var needed| Flag | Default | Notes |
|---|---|---|
--port |
0 (OS-assigned) |
Must be passed literally when overriding opencode.json's server.port. |
--hostname |
127.0.0.1 |
Becomes 0.0.0.0 when --mdns is set without an override. |
--mdns |
false |
Publishes an mDNS SRV record. |
--mdns-domain |
kilo.local |
|
--cors |
[] |
Extra allowed origins. |
AUTH="Authorization: Basic $(printf 'kilo:%s' "$CZCODE_SERVER_PASSWORD" | base64 | tr -d '\n')"The username is literally kilo (inherited from kilocode). Skip this whole block if you launched without a password.
DIR_HEADER="x-kilo-directory: $PWD"InstanceMiddleware uses this to scope the request to a project. For GET / HEAD, pass ?directory=<urlencoded> in the URL instead — that's what the SDK does internally.
All of the below assume BASE, AUTH, DIR_HEADER are set. Drop -H "$AUTH" if you're running without a password.
curl -sS "$BASE/global/health"curl -sS "$BASE/doc" | jq .This is the source of truth — anything not in this doc is discoverable from /doc without reading source.
SID=$(curl -sS -X POST "$BASE/session" \
-H "$AUTH" -H "$DIR_HEADER" -H "Content-Type: application/json" \
-d '{}' | jq -r .id)
echo "$SID"curl -sS -H "$AUTH" \
"$BASE/session?directory=$(printf %s "$PWD" | jq -sRr @uri)"curl -sS -X POST "$BASE/session/$SID/prompt_async" \
-H "$AUTH" -H "$DIR_HEADER" -H "Content-Type: application/json" \
-d '{"parts":[{"type":"text","text":"hello"}]}'curl -sS -H "$AUTH" \
"$BASE/session/$SID/message?directory=$(printf %s "$PWD" | jq -sRr @uri)"curl -sS -X POST -H "$AUTH" -H "$DIR_HEADER" "$BASE/session/$SID/abort"curl -sS -H "$AUTH" \
"$BASE/config?directory=$(printf %s "$PWD" | jq -sRr @uri)"curl -N -sS -H "$AUTH" \
"$BASE/global/event?directory=$(printf %s "$PWD" | jq -sRr @uri)"-N disables curl's output buffering so events appear live. Expect lines like data: {"directory":"…","payload":{"type":"…",…}}.
kill "$(cat /tmp/czcode-serve.pid)" 2>/dev/null || true
rm -f /tmp/czcode-serve.pid /tmp/czcode-serve.logServeCommand handles SIGTERM / SIGINT / SIGHUP and runs Instance.disposeAll() + server.stop(true) before exiting (packages/opencode/src/cli/cmd/serve.ts:29-31). -9 is only needed if the process hangs past ~5 s.
| Var | Why you'd set it |
|---|---|
CZCODE_SERVER_PASSWORD |
Enable Basic auth. Omit for auth-bypassed local testing. |
KILO_DB=":memory:" |
Skip on-disk SQLite — hermetic runs. |
KILO_DISABLE_DEFAULT_PLUGINS=true |
Don't auto-load bundled plugins. |
KILO_TELEMETRY_LEVEL=off |
Disable telemetry during tests. |
KILO_CONFIG_CONTENT='{…}' |
Inline JSON config without writing a file. |
Use this only when curl can't express what you need — typed request/response shapes, complex multi-turn orchestration, SSE consumers that need to coalesce events. Keep the file short and delete it after.
// /tmp/probe.ts — delete after use. Talks to an already-running backend.
import { createKiloClient } from "@kilocode/sdk/v2"
const port = process.env.PORT!
const pass = process.env.KILO_SERVER_PASSWORD
const headers = pass ? { Authorization: "Basic " + Buffer.from("kilo:" + pass).toString("base64") } : undefined
const client = createKiloClient({
baseUrl: `http://127.0.0.1:${port}`,
headers,
directory: process.cwd(),
})
const { data: session } = await client.session.create({}, { throwOnError: true })
await client.session.promptAsync({
sessionID: session.id,
parts: [{ type: "text", text: "hello" }],
})
const events = await client.global.event({})
for await (const ev of events.stream) {
const e = ev as { payload: { type: string } }
console.log(e.payload.type)
if (e.payload.type === "session.idle") break
}PORT="$PORT" KILO_SERVER_PASSWORD="$KILO_SERVER_PASSWORD" bun /tmp/probe.ts
rm /tmp/probe.tsReminder: this script connects to the server you launched in Section 3 — it does not start one. createKiloServer() from the SDK would spawn the PATH kilo binary (production CLI), which defeats the point of testing local code.
- Running
czcode serveinstead ofbun dev serveruns the installed prod binary, not your edits. - Missing
x-kilo-directory(or?directory=) returns400fromInstanceMiddleware. curlwithout-Nbuffers SSE output — you won't see events until the connection closes.- Hardcoding port
4096breaks when a previous run didn't exit cleanly. Parse the log instead. CZCODE_SERVER_PASSWORDmust be set before launch — changing it after doesn't rotate credentials.- When sharing
/tmp/czcode-serve.log/.pidacross terminals, unique-suffix the paths to avoid clobbering parallel runs.
Regenerate the SDK and OpenAPI spec so /doc and typed clients stay in sync:
./script/generate.ts # from repo rootSee AGENTS.md for the full rationale.