Skip to content

Command-line (kd)

kd is the Kilden command-line client. Sign in once with your browser, then manage your teams, projects, write keys and identity secrets — and stream events live — without leaving the terminal. It’s open source (kildenhq/kilden-cli, MIT).

Authentication uses the OAuth 2.0 Device Authorization Grant (RFC 8628): no passwords, and nothing is stored beyond the tokens the browser hands back.

Terminal window
curl -fsSL https://raw.githubusercontent.com/kildenhq/kilden-cli/main/install.sh | bash

The installer detects your OS and CPU, downloads the latest release, verifies its checksum, and installs kd into ~/.local/bin. If that directory isn’t on your PATH, the installer tells you how to add it.

Pin a version or pick a different install directory with env vars:

Terminal window
curl -fsSL https://raw.githubusercontent.com/kildenhq/kilden-cli/main/install.sh \
| KILDEN_VERSION=v0.1.0 KILDEN_INSTALL_DIR="$HOME/bin" bash

Download kd_windows_amd64.zip from the latest release, unzip it, and put kd.exe on your PATH.

Terminal window
go install github.com/kildenhq/kilden-cli/cmd/kd@latest

Prefer typing kilden? Add an alias: alias kilden=kd.

Terminal window
kd login

kd prints a short code and opens your browser. Confirm the code matches, approve, and you’re in. The session is stored under your OS config dir (~/.config/kilden/credentials.json, mode 0600) and refreshes itself.

Self-hosting the panel? Point the CLI at it:

Terminal window
kd login --host https://panel.example.com
# or: export KILDEN_HOST=https://panel.example.com

Sign out with kd logout (forgets the stored credentials on this machine).

Most commands act on your active team and project. Inspect and switch them:

Terminal window
kd whoami # who am I, and which team/project is active
kd team # list your teams (the active one is marked *)
kd team use # pick the active team (interactive)
kd team use 42 # …or by id
kd project # list the active team's projects
kd project use <project-id> # switch the active project
kd project create --name "Web" --timezone America/Santiago

Every data command also takes --project <id> to override the active project for that one call.

A project has two kinds of write key: a public key (wk_) for the browser, and a secret key (sk_) for your backend.

Terminal window
kd key # list the active project's keys
kd key create --kind public --label web # public write key (wk_)
kd key create --kind secret --label ci # server key (sk_), shown once

A secret key’s full value is shown only once, at creation — store it then.

For identity verification, a project signs trait tokens with a per-project HS256 secret (is_). You can run several at once and rotate without downtime.

Terminal window
kd identity-secret # list secrets: id, kid, active/revoked
kd identity-secret create --kid v1 # mint one under a kid; value shown once
kd identity-secret create # kid auto-generated if you omit it
kd identity-secret disable <id> # revoke by id (never deleted)

The kid (key id) identifies a secret so you can rotate: create the next kid, move your backend to it, then disable the old one. Like secret keys, the value is shown only once on creation.

List a project’s most recent events, newest first:

Terminal window
kd events # recent events
kd events --event signup # only a given event name

To see the event names and property keys a project has actually received:

Terminal window
kd properties

Kilden properties are schema-less by design — there’s nothing to “create”. kd properties shows what your events actually carry.

Stream events as they arrive, in real time:

Terminal window
kd tail # stream live events (Ctrl+C to stop)
kd tail --event signup # only stream a given event
kd tail --json | jq # raw JSON per event, one per line

kd tail connects to the live-tail websocket with a short-lived ticket and prints each event as HH:MM:SS event distinct_id. It reconnects on its own as tickets refresh; status lines go to stderr, so --json | jq stays clean.

Prefer to keep your Kilden setup in version control — or let an AI agent build it — instead of clicking through the panel? Describe it in a YAML (or JSON) file and apply it. One document holds cohorts, feature flags, saved insights, in-app units and tours, campaigns and experiments, a key per collection:

kilden.yaml
cohorts:
- slug: power-users # stable identity — how apply finds it again
name: Power users
definition:
conditions:
- type: behavior
event: checkout_completed
count_gte: 3
days: 30
flags:
- key: checkout-v2
name: New checkout
active: true
rollout_percentage: 50
variants:
- {key: control, rollout_percentage: 50}
- {key: treatment, rollout_percentage: 50}
insights:
- slug: signup-funnel
type: funnel
name: Signup funnel
config:
steps: ["$pageview", "signup_started", "signup_completed"]
window_days: 14
campaigns:
- slug: welcome-series
name: Welcome series
status: active
reentry: never
nodes:
- key: signup-trigger # the step's stable identity — see below
type: trigger
config: {mode: event, event: signup}
# …or enter on joining a cohort, named by its slug:
# config: {mode: cohort, cohort: power-users}
- key: welcome-email
type: email
config: {subject: Welcome, body_template: "<p>Hi</p>"}
edges:
- {from: signup-trigger, to: welcome-email}
Terminal window
kd apply -f kilden.yaml # create or update everything; - reads stdin
kd cohort ls # every resource gets the same four:
kd flag get checkout-v2 # ls / get / rm / export
kd campaign export -o yaml
kd experiment rm old-test
kd cohort materialize power-users # recompute membership now, not on the sweep

apply is idempotent: each item is matched by its stable identity, so re-running the same file updates in place and never duplicates. export emits that same identity, so exportapply round-trips.

Collections are applied in dependency order (cohorts → flags → insights → units → campaigns → experiments), because a campaign can name a cohort and an experiment names a flag. The order you write them in does not matter.

Cohorts, insights, in-app units and campaigns use a slug, which exists for this purpose: anything you created in the panel has none, and is never listed or overwritten by an apply. Feature flags and experiments use the key the product already had, so your file and the panel address the same rows — on purpose.

A campaign step’s key is not just a label

Section titled “A campaign step’s key is not just a label”

The engine derives each step’s internal id from its key, and people who are mid-flow are parked on that id. Keep a key and they stay exactly where they were; rename it and that is a genuinely different step, so whoever was waiting there leaves the campaign. Re-applying an unchanged campaign is safe.

Setting a campaign’s status to active runs the same checks the panel does: a valid flow, an audience, and a live server key when the flow writes back through capture. If one fails you get the reason and the flow is still saved as a draft.

Some things are acts rather than desired state, so they are not part of the document — broadcasting a campaign, sending a test email, and concluding an experiment or rolling its winner out. Those stay in the panel, where you can see what you are about to do.

Applying config needs a token with the config:write scope (reading needs config:read); kd login requests both.

  1. kd login asks the panel for a device code and opens the approval page.
  2. You approve in the browser; kd polls until it receives an access + refresh token, then stores them locally.
  3. Later commands call the panel’s management API at /api/v1 with the bearer token, refreshing it transparently when it expires.

The CLI ships a public OAuth client id (a CLI can’t keep a secret). What you can do is enforced server-side against your team membership and role — the same permissions as the panel.