/\
/||\
/ || \
/ || \
<===++===>
\ || /
\ || /
\||/
\/
\
)-><
(
)-><
(
kite is an ultra-lightweight Kafka CLI built for agents and sandboxes.
One binary under 700 KB, no JVM, no runtime, no daemon. stdin in, stdout out,
non-zero exit on failure.
What is kite? kite is a single-binary command-line tool for Apache
Kafka that produces records from stdin, consumes records to stdout, and
manages topics. It is
a drop-in, dependency-free alternative to kafka-console-producer.sh,
kafka-console-consumer.sh, kcat/kafkacat, and rpk topic produce|consume
for scripts, CI jobs, containers, and AI agents. It is written in Zig, speaks
the Kafka wire protocol directly (PLAINTEXT, SSL, SASL/PLAIN, SCRAM-SHA-256,
SCRAM-SHA-512), works with any Kafka-compatible broker (Apache Kafka,
Confluent Cloud, Amazon MSK, WarpStream), and never hangs:
piped reads stop on idle by default.
Use kite when you need to move records into or out of a Kafka topic — or list, create, delete, and update topics — from a shell, a script, a container, or an agent tool call, and you want one small static binary with no JVM, no librdkafka, and no daemon.
Do not use kite when you need consumer-group administration, offset commits, or ACLs, or a long-lived consumer group member; use your Kafka admin tooling or a client library for those.
Need schemas? kite moves bytes and never talks to Schema Registry. Its companion wing validates and decodes JSON Schema records from Confluent Schema Registry in the same pipe; see Schema Registry with wing.
You need a reachable Kafka 4.0+ broker (localhost:9092 below, e.g. the
Docker one in Run Kafka locally with Docker), permission to read and write the topic you name, and
jq for the last line only. Use a fresh
topic name so the output is exactly one record; produce creates a missing
topic automatically when run from a script or pipe.
curl -fsSL https://raw.githubusercontent.com/addisonhuddy/kite/main/install.sh | sh
export BOOTSTRAP_SERVERS=localhost:9092
printf 'hello\n' | kite produce events # produce one record
kite consume -B -n 1 --idle 3s --json events | jq -r .value # prints: helloFor a named cluster you can reuse across commands, kite cluster init
writes one into kite.yaml (see Multiple clusters).
What to expect:
- stdout is data, stderr is diagnostics. The produce line prints a short
summary (
1 record(s) produced to 'events' ...) on stderr and nothing on stdout; the consume line prints the record on stdout and its summary on stderr, so| jqonly ever sees records. -Bis required here. Without--from-beginninga consumer starts at the latest offset and would skip the record produced before it started.-n 1 --idle 3sstops the read as soon as the one record arrives, so the run takes milliseconds;--idle 3sis the fallback that ends it if nothing arrives. Without either, a piped consume stops after 5 s idle.jqis optional. It only extracts.valuefrom the JSON record; drop--json | jq -r .valueto print plain values.- Topic creation depends on the broker. kite asks the broker to create a missing topic when producing from a script or pipe; a broker policy or missing ACL can refuse that, in which case create the topic with your admin tooling first.
See Troubleshooting if the roundtrip does not print
hello.
If you are an agent reading this (or a human who also likes Kafka), here's why I think you will love kite.
- Small. The stripped binary is under 700 KB (CI-gated by
scripts/check-size.sh). It fits in a container layer, a sandbox, or a tool call without anyone noticing. - Unix philosophy. kite does one thing per invocation and composes with
everything else:
tail -f app.log | kite produce logs,kite consume src | jq | kite produce dst. Records are lines. Diagnostics go to stderr, data goes to stdout. - Fast. Written in Zig: native code, no garbage collector, no JVM
startup, starts and exits in milliseconds. Produce runs are batched and
idempotent by default; consume runs can be bounded by record count (
-n) and by idle time (-t/--idle), so a script stops when the data does. - Easy install. One static binary:
curlit onto yourPATHorzig build. No package manager, noJAVA_HOME. - Predictable for automation. Every error is a one-line
kite: ...on stderr with exit code 1 and aTry 'kite --help'hint. Help pages are plain text, ≤ 80 columns, no ANSI unless stderr is a terminal.
kite is not a full Kafka admin tool: it never manages consumer groups or
commits offsets, but it does manage topics (kite topic). When producing
to a topic that does not exist it offers to create it — prompting on a
terminal, creating automatically in scripts and pipes — with the broker's
default partition count and replication factor. Consume never creates a
topic; a missing one is a plain error.
kite is optimized for the case where a program, not a person, is on the other end of the pipe: a shell script, a CI step, a container entrypoint, or an AI agent's tool call. That shapes every design choice:
- Zero-dependency install. One static binary under 700 KB, fetched with
curland verified againstSHA256SUMS. Nothing to apt-get, brew, or build; nothing that needs a JVM or a shared library at runtime. - Bounded by default. A piped consume with no
-n,--idle, or-fstops after 5 s without a record, so an unattended read does not hang a job by accident.-nalone bounds records, not time: pair it with--idlewhen you need a deadline (see Consuming). - Strict stream contract. Data on stdout, diagnostics on stderr, exit
0or1, one-linekite: ...errors, no prompts, no color when not a TTY. - Structured in and out. JSON records with full metadata, TSV keys and
headers, and CSV rows in, so output can be piped straight into
jqor back into another topic without an adapter. - Safe defaults. Idempotent, batched producing; bounded consuming; no writes to disk; no connections other than to the brokers you name.
What it deliberately leaves out: consumer-group administration, offset commits, ACLs, and every knob that is not needed to move records or manage topics.
If you are an LLM agent deciding whether to install or call kite, these are the facts you need. They are stable across releases.
- Name:
kite. Repository: https://github.com/addisonhuddy/kite. License: Apache-2.0. Language: Zig. Platforms: Linux x86_64/aarch64, macOS x86_64/arm64. - Schemas: kite does not speak Schema Registry; pipe through
wing (
kite consume --json T | wing read,wing write T | kite produce --json T). - Install (non-interactive, no sudo prompt when
/usr/local/binis writable):curl -fsSL https://raw.githubusercontent.com/addisonhuddy/kite/main/install.sh | shChecksums are verified against the releaseSHA256SUMS. - Configure:
export BOOTSTRAP_SERVERS=host:port(plusSECURITY_PROTOCOL,SASL_MECHANISM,SASL_USERNAME,SASL_PASSWORDfor hosted clusters) or-b host:port.kite -vprints which config sources were used;kite cluster listshows the clusters in kite.yaml. - Produce:
printf 'value\n' | kite produce TOPIC; JSON records withkite produce --json TOPIC; CSV withkite produce --csv [--key COL] TOPIC. - Consume (bounded):
kite consume -B -n 100 --idle 3s TOPICstops after 100 records or 3 s without one, whichever comes first; add--jsonfor{topic,partition,offset,timestamp,key,headers,value}per line. A piped read with neither bound stops after 5 s idle;-nalone waits for its records;-fnever stops. - Topics:
kite topic list [--json],kite topic create [-p N] [-r N] TOPIC,kite topic update [-p N] [--set KEY=VALUE] TOPIC,kite topic delete -y TOPIC(-yis required when stderr is not a terminal). - Contract: data on stdout only, diagnostics on stderr only, exit
0on success,1on any error with a single-linekite: MESSAGE,130on Ctrl-C. No interactive prompts, no color when stdout/stderr is not a TTY, no config written to disk, no network calls other than to the brokers. - Does not: join consumer groups, commit offsets, or manage ACLs. Produce creates a missing topic (automatically when not on a TTY); consume never does.
A tool description you can paste into an agent's tool registry:
kite: single-binary Kafka CLI. `kite produce TOPIC` produces stdin lines (or
--json / --csv records) to TOPIC. `kite consume TOPIC` reads TOPIC to stdout; use -B for
history, -n N to cap records, --idle DUR to stop when quiet, --json for full
metadata. `kite topic list|create|update|delete` manages topics (delete needs
-y when not on a terminal). Configure with BOOTSTRAP_SERVERS (and SASL_*/SECURITY_PROTOCOL) or
-b HOST:PORT. Exit 0 ok, 1 error (message on stderr). A piped consume with
no -n/--idle/-f stops after 5s idle; pass -n and --idle together for a
bounded read. For Confluent Schema Registry JSON Schema, pipe records through
wing: `kite consume --json TOPIC | wing read` or `wing write TOPIC | kite produce --json TOPIC`.
A machine-readable summary also lives in llms.txt.
curl -fsSL https://raw.githubusercontent.com/addisonhuddy/kite/main/install.sh | shThe script downloads the latest release binary for your platform and
installs it to /usr/local/bin, which is on the default PATH of every
supported OS (it uses sudo only if that directory is not writable). Pass
options after sh -s --:
curl -fsSL https://raw.githubusercontent.com/addisonhuddy/kite/main/install.sh | sh -s -- --bin-dir ~/.local/bin --version v0.4.0| Option | Env | Meaning |
|---|---|---|
-b, --bin-dir DIR |
KITE_BIN_DIR |
Install into DIR (default /usr/local/bin). If DIR is not on your PATH, the script prints the line to add. |
-v, --version VER |
KITE_VERSION |
Release tag to install (default latest). |
The script downloads the release's SHA256SUMS and refuses to install on a
checksum mismatch. kite is a single static binary, so you can also skip the
script and fetch the latest release directly (assets are named
kite-{linux,macos}-{x86_64,aarch64}):
curl -fsSL -o kite https://github.com/addisonhuddy/kite/releases/latest/download/kite-linux-x86_64
chmod +x kite && sudo mv kite /usr/local/bin/Prebuilt binaries plus SHA256SUMS for every version are on the
releases page.
kite update # latest release; no-op when already current
kite update v0.2.0 # a specific releasekite update asks GitHub for the latest release and, if it differs from the
running binary, runs the same install.sh with --bin-dir set to the
directory the running kite lives in, so it replaces itself in place (with
the same checksum check, and sudo only if that directory is not writable).
It needs /bin/sh and curl or wget.
Requires Zig 0.16.x.
zig build
sudo install -m755 zig-out/bin/kite /usr/local/bin/kiteCross-compile with, for example, zig build -Dtarget=aarch64-macos or
zig build -Dtarget=x86_64-linux.
Static completion scripts for bash, zsh, and fish live in
completions/ in the repository. The curl installer ships
only the binary, so clone the repo (or download the one file you need from
GitHub) first; the snippets below assume you are in the checkout. They only
need the kite binary on your PATH and are safe to run in a clean home
directory.
bash (needs the bash-completion package for the user directory to be
picked up automatically; otherwise source the file from ~/.bashrc):
mkdir -p ~/.local/share/bash-completion/completions
cp completions/kite.bash ~/.local/share/bash-completion/completions/kite
# or, without bash-completion:
echo 'source /path/to/kite/completions/kite.bash' >> ~/.bashrczsh (the function file must be named _kite and sit on fpath before
compinit runs):
mkdir -p ~/.zfunc
cp completions/kite.zsh ~/.zfunc/_kite
cat >> ~/.zshrc <<'EOF'
fpath=(~/.zfunc $fpath)
autoload -Uz compinit && compinit
EOFIf ~/.zshrc already calls compinit, put the fpath line above it instead
of appending. Delete ~/.zcompdump* if a stale cache hides the new function.
fish (uses $__fish_config_dir when set, ~/.config/fish otherwise):
set -q __fish_config_dir; or set __fish_config_dir ~/.config/fish
mkdir -p $__fish_config_dir/completions
cp completions/kite.fish $__fish_config_dir/completions/Open a new shell and type kite --<TAB> to check.
scripts/completion-check.sh performs these installs in a temporary HOME
for every shell present on the machine and asserts that options complete.
The first argument is the command. @NAME selects a cluster from
kite.yaml and may appear anywhere among a command's arguments; p and
c are short for produce and consume.
kite produce [OPTIONS] [@CLUSTER] TOPIC Write stdin lines to TOPIC.
kite consume [OPTIONS] [@CLUSTER] TOPIC Read TOPIC to stdout.
kite topic [ACTION] [OPTIONS] [@CLUSTER] [TOPIC...] List, create, delete, or update topics.
kite cluster [list|set NAME|init] Pick, list, or set the current cluster.
kite update [VERSION] Install the latest (or VERSION) release in place.
kite --version Print the version.
| Mode | Option | Meaning |
|---|---|---|
| produce, consume | -b, --bootstrap HOSTS |
Comma-separated host:port brokers (overrides env and file). |
| all | --config FILE |
Read this properties file instead of searching. |
| produce, consume | @NAME |
Use cluster NAME from kite.yaml for this run only (also $KITE_TARGET). |
| produce, consume | --format FMT |
Record shape: value, tsv, json (default auto). |
| produce | --format csv |
RFC 4180 CSV input (produce only); same as --csv. |
| produce, consume | --json |
Alias for --format json. |
| produce | -H 'name: value' |
Add a header to every record (repeatable). |
| produce | --csv |
Read RFC 4180 CSV; each row becomes a JSON object value. |
| produce | --key COL |
Use CSV column COL as the record key (requires --csv). |
| consume | -B, --from-beginning |
Start at the earliest offset. |
| consume | --offset N |
Start at offset N in every selected partition; errors if N is out of range. |
| consume | --partition P |
Read one partition only. |
| consume | -n, --max MAX |
Stop after MAX records. |
| consume | -t, --idle DUR |
Stop after DUR without a record (3s, 500ms, 1m; bare number = ms). |
| consume | -f, --follow |
Never stop on idle, even when stdout is a pipe. |
| all | -q, --quiet |
Suppress the summary and progress lines on stderr. |
| produce, consume | -v, --verbose |
Connection, retry, and fetch diagnostics on stderr. |
| all | -h, --help |
Plain-text help for the selected mode. |
kite --help prints the command overview; kite produce --help,
kite consume --help, kite topic --help, and kite cluster --help
print the full pages.
kite topic manages topics; a bare kite topic is kite topic list.
kite topic list # aligned table on a terminal; one name per line when piped, sorted
kite topic list -a # include internal topics (__consumer_offsets)
kite topic list --json | jq -r .name
# -> {"name":..,"partitions":N,"replication_factor":N,"internal":BOOL}
kite topic create events # broker defaults
kite topic create -p 6 -r 2 -s retention.ms=86400000 events
kite topic create --if-not-exists events # note instead of an error
kite topic update -p 12 events # partitions can only grow
kite topic update --set cleanup.policy=compact events
kite topic update --unset retention.ms events
kite topic delete events # prompts for confirmation on a terminal
kite topic delete -y old-a old-b # scripts must pass -y (records are lost)
kite topic delete -y --if-exists goneDelete confirms on a terminal; when stderr is not a terminal it refuses
unless -y/--yes is given. list writes topic names (or JSON lines) to
stdout; all notes go to stderr.
The files in examples/data/ are ready-to-use inputs.
# plain values, one per line
kite produce events < examples/data/lines.txt
# key<TAB>value
kite produce events < examples/data/keyed.tsv
# key<TAB>headers<TAB>value
kite produce events < examples/data/headers.tsv
# attach a header to every record (-H is repeatable)
kite produce -H 'source: import' events < examples/data/lines.txt
# CSV rows as JSON values, optionally keyed by a column
kite produce --csv events < examples/data/users.csv
kite produce --csv --key user_id events < examples/data/events.csv
# stream a log as it grows
tail -f app.log | kite produce logs
# follow new records on a terminal (Ctrl-C to stop; summary printed)
kite consume events
# follow into a pipe (without -f a piped read stops after 5 s idle)
kite consume -f events | grep ERROR
# read one partition from an offset, at most 10 records
kite consume --partition 0 --offset 42 -n 10 --idle 3s events
# snapshot a topic into a file for an agent to read (stops after 5 s idle)
kite consume -B events > events.txt
# structured records with partition/offset/timestamp/headers
kite consume -B --json events | jq -c 'select(.partition == 0) | .value'
# copy a topic, preserving keys and headers
kite consume -B --json src | kite produce --json dst
# transform in flight
kite consume -B raw | jq -c '{id, ts}' | kite produce clean
# one-off broker without a config file
kite consume -b broker1:9092,broker2:9092 -B -n 5 events
# named cluster from kite.yaml (see "Multiple clusters" below)
kite produce @prod events < examples/data/lines.txt
# which clusters are there, and which one is selected?
kite cluster list
# topic management
kite topic list
kite topic create -p 6 events
kite topic update --set cleanup.policy=compact events
kite topic delete -y eventsThe consumer output is in the same textual shape accepted by the producer, subject to the format boundaries described below.
kite moves Kafka records; wing handles
Confluent Schema Registry and JSON Schema validation. Put wing write TOPIC
before kite produce --json TOPIC, and wing read after kite consume --json TOPIC; records stay in kite's JSONL format, and kite v0.4.0+ keeps
the schema header safe through jq. kite never connects to the Registry, and
wing never reads kite.yaml: configure Kafka with BOOTSTRAP_SERVERS (or
kite.yaml) and the Registry with SCHEMA_REGISTRY_URL (or wing.yaml).
You need Kafka at localhost:9092, Schema Registry at localhost:8081 (see
the Docker commands),
and jq. Install kite separately if it is
not already on your PATH.
curl -fsSL https://raw.githubusercontent.com/addisonhuddy/wing/main/install.sh | sh
export BOOTSTRAP_SERVERS=localhost:9092 SCHEMA_REGISTRY_URL=http://localhost:8081
wing push orders < examples/wing/orders.schema.json # register orders-value
# prints the new schema GUID
wing write orders < examples/wing/orders.jsonl | kite produce --json orders # validate, add schema header, produce
# prints: 2 record(s) produced to 'orders' across 1 of 1 partition(s)
kite consume -B -n 2 --idle 3s --json orders | wing read | jq -c .value
# prints: {"order_id":1,"customer":"Ada","total":12.5}
# prints: {"order_id":2,"customer":"Grace","total":21}An invalid record exits 2 from wing write; its empty stdout means kite
receives no record (use pipefail to surface the upstream exit from a
pipeline):
set -o pipefail
printf '%s\n' '{"order_id":"three","customer":"Lin","total":5}' | wing write orders | kite produce --json orders
# wing write: line 1: /order_id: expected integer, got string [/properties/order_id/type]
# wing write: 0 written, 0 fitted
# prints: 0 record(s) produced to 'orders' across 0 of 1 partition(s)
# exit 2wing write is streaming: this single invalid record sends nothing, but
valid lines earlier in a mixed input may already have been produced before
a later line is rejected.
- CSV fields arrive as strings; first import them to a raw topic, then fit
them to the
ordersschema before producing validated records:kite produce --csv orders-raw < examples/wing/orders.csv, thenkite consume -B -n 2 --idle 3s --json orders-raw | wing write orders --fit | kite produce --json orders. - Edit decoded values with jq, copy the schema to give the destination its
own
orders-copy-valuesubject, then validate and produce the changes:wing get orders --meta | wing push orders-copy --meta, thenkite consume -B -n 2 --idle 3s --json orders | wing read | jq -c '.value.total *= 2' | wing write orders-copy | kite produce --json orders-copy.
See the full walkthrough in examples/wing/.
By default, kite consume starts at the latest offset and selects all partitions.
-B/--from-beginning starts at the earliest available offset; --offset N
starts at offset N in every selected partition and fails with the valid range
(valid offsets are 0..334 (next offset 335)) if N is outside it, rather
than silently replaying from the beginning. --partition P selects one
partition.
When the read stops depends on stdout:
- Terminal: follow new records until Ctrl-C. Ctrl-C stops cleanly, prints the summary, and exits 130.
- Pipe or file, no bound given: stop after 5 s without a record, so
scripts and agents never hang by accident. The reason is stated in the
summary (
... (idle timeout)). -n/--max MAXstops after MAX records. This bounds the record count, not the wall clock:-n 1on an empty topic waits until a record arrives, and giving-ndisables the 5 s default idle stop.-t/--idle DURstops after DUR without a record (3s,500ms,1m, or a bare number of milliseconds). This bounds idle waiting, not total runtime: a topic that keeps producing keeps the read alive.-f/--follownever stops on idle.--followand--idleare mutually exclusive.
For a finite read in automation, give both bounds
(kite consume -B -n 100 --idle 3s TOPIC): the read ends at 100 records or 3 s
of silence, whichever comes first. Neither flag is an overall deadline; if
you need one, wrap the command in timeout.
An idle-bounded read is not a guarantee of a complete topic snapshot. If the
downstream process closes the pipe (kite consume -f events | head), kite exits
quietly with status 0 instead of dying from SIGPIPE.
--format FMT selects the record shape for both directions:
auto(default): produce sniffs TAB-separated fields on each input line; consume writes the value alone when the record has no key or headers, otherwise the TAB shape below.value: produce sends the whole line as the value (TAB is not special); consume writes only the value. Keys and headers are dropped on output.tsv: produce parses the TAB shapes below (same asauto); consume always writeskey<TAB>[h: v<TAB>]value, with an empty key field for null keys and header fields only when present.json: one JSON object per line/record; aliases--json.csv(produce only): RFC 4180 input; alias--csv.
TAB-separated input has these shapes:
value
key<TAB>value
key<TAB>name: value<TAB>...<TAB>value
The TAB and newline delimiters are not escaped. Binary data or records
containing delimiters are therefore not generally safe to roundtrip. Null and
empty keys, values, and header values may not remain distinct: a null key is
printed as an empty field, and a null header value as name: . Compatible
textual keys and headers can roundtrip: kite consume --format tsv src | kite produce --format tsv dst preserves keys and headers, while --format json
preserves key, headers, and value exactly and value drops keys and
headers. A consume-to-produce pipe does not preserve ordering across
partitions, offsets, or timestamps.
--csv reads RFC 4180 CSV. The first row supplies column names and each later
row becomes a JSON object value. --key COL uses a CSV column as the Kafka
record key while retaining it in the JSON value. Quoted commas, escaped
quotes, embedded newlines, CRLF endings, and a UTF-8 BOM are supported; all
JSON fields are strings.
kite consume --json writes one object per record with full metadata, so bytes
containing TABs or newlines and null-vs-empty distinctions survive. Valid
UTF-8 fields remain strings; fields containing invalid UTF-8 bytes use a
sibling *_b64 field with standard padded Base64 instead. Header names remain
plain strings. The output is valid UTF-8 JSON and safe to pass through jq:
{"topic":"events","partition":1,"offset":7,"timestamp":1789579403024,"key":null,"headers":[{"key":"h","value":"v"}],"value":"line one"}
{"topic":"events","partition":1,"offset":8,"timestamp":1789579403025,"key_b64":"AP+A","headers":[{"key":"trace","value_b64":"gA=="}],"value_b64":"AYD+/w=="}kite produce --json TOPIC reads the same shape (only value or value_b64 is required):
{"key":"user-1","value":{"a":1},"headers":{"source":"import"}}
{"key_b64":"AP+A","value_b64":"AYD+/w==","headers":[{"key":"trace","value_b64":"gA=="}]}A string value is sent as its decoded bytes; any other JSON value (object,
array, number, boolean) is sent verbatim, so JSON payloads can be embedded
without double encoding. The _b64 fields must be strings containing standard
RFC 4648 Base64 with padding; they are decoded as record bytes. headers may
be an object or an array of {"key","value"}; array entries also accept
value_b64. -H headers are added to every record. Malformed lines fail with
kite: line N: .... --json and --csv are mutually exclusive.
Settings are resolved in this order, highest precedence first:
- Flags:
-b/--bootstrap HOSTS. - Environment variables (below).
- A config file:
--config FILE, else$KAFKA_PROPERTIES, else the first of./kite.yaml,./kite.properties,$XDG_CONFIG_HOME/kite/kite.yaml,$XDG_CONFIG_HOME/kite/kite.properties,~/.config/kite/kite.yaml,~/.config/kite/kite.properties.
A file is optional when -b or BOOTSTRAP_SERVERS supplies the
brokers. With none of these, kite fails with a message listing all three
ways to configure it. A --config/KAFKA_PROPERTIES path that does not exist is
an error rather than a silent fallback. -v prints which sources were used.
The properties parser supports a key=value subset of Java properties.
Blank lines and #/! comments are accepted; unknown keys are ignored
with a warning. .yaml/.yml files are parsed as a strict YAML subset
(see "Multiple clusters" below).
Environment variable names are the Kafka property names upper-cased with .
replaced by _, so bootstrap.servers becomes BOOTSTRAP_SERVERS.
| Key | Environment variable | Default | Meaning |
|---|---|---|---|
bootstrap.servers |
BOOTSTRAP_SERVERS |
required | Comma-separated host:port brokers. |
security.protocol |
SECURITY_PROTOCOL |
PLAINTEXT |
PLAINTEXT, SSL, SASL_SSL, or SASL_PLAINTEXT. |
sasl.mechanism |
SASL_MECHANISM |
none | PLAIN, SCRAM-SHA-256, or SCRAM-SHA-512. |
sasl.username |
SASL_USERNAME |
none | Required for SASL. |
sasl.password |
SASL_PASSWORD |
none | Required for SASL. |
ssl.truststore.location |
SSL_TRUSTSTORE_LOCATION |
system trust store | Optional PEM CA bundle for TLS. |
batch.size |
— | 1048576 |
Per-partition producer buffer cap in bytes. |
linger.ms |
— | 50 |
Producer flush delay when stdin stalls. |
fetch.max.bytes |
— | 8388608 |
Maximum bytes requested per fetch. |
fetch.max.wait.ms |
— | 500 |
Maximum broker wait for a fetch. |
enable.idempotence |
— | true |
Broker deduplicates retried producer batches. |
Templates are in examples/config/. Idempotence means
broker deduplication of retried batches, not end-to-end exactly-once
processing.
kite.yaml holds several clusters. clusters: maps each name to its
settings (any key from the table above); defaults: holds shared
settings applied to every cluster; default: names the cluster used
when nothing selects one. Cluster names match [A-Za-z0-9_-]+.
default: dev
defaults:
linger.ms: 20
clusters:
dev:
bootstrap.servers: localhost:9092
prod:
bootstrap.servers: pkc-xyz.us-east-1.aws.confluent.cloud:9092
security.protocol: SASL_SSL
sasl.mechanism: PLAIN
sasl.username: KEY
sasl.password: SECRETOnly a strict YAML subset is parsed: block mappings, plain and quoted
scalars, comments. Lists, flow syntax, anchors, and block scalars are
rejected with a file:line error.
The cluster is selected by, in order: the @NAME positional,
$KITE_TARGET, the stored current cluster (see below), the file's
own default: key, else no cluster and only defaults:/unprefixed
keys apply. Passing @NAME without any config file is an error, as is
selecting a name the file does not define — including any name on a
properties file, which always describes a single cluster.
@NAME selects a cluster and goes anywhere among a command's
arguments, so switching clusters is one word: kite produce @prod events, kite consume @local-b -B events. @NAME applies to this run
only and never changes the stored current cluster; use kite cluster set for that. A bare @ is an error; two different clusters on one
command line (@prod @dev) are rejected rather than letting the last
one win. Topics never start with @, so there is no ambiguity.
kite cluster list lists the clusters, one per line and sorted, with
* after the effective one (* before it on a terminal; the same
selection order as above), and
a note on stderr naming the file it read (-q drops the note). It
reads only the clusters: keys and never resolves a configuration or
connects to a broker, so it works even while a cluster is still half
written or lacks bootstrap.servers. --json yields
{"file":..,"current":..,"clusters":[..]}. It exits 1 when there is
no config file at all.
$ kite cluster list
dev *
prod
kite: clusters from ./kite.yaml
$ kite cluster list --json | jq -r '.clusters[]'
dev
prodShell completion follows suit: after @, the completions offer the
cluster names from ./kite.yaml.
Within a cluster, value precedence is: flags > the cluster's keys >
environment > defaults:/base keys > built-in defaults. A named cluster
is a complete definition, so ambient BOOTSTRAP_SERVERS cannot silently
redirect kite produce @prod; environment variables still fill in keys the
cluster omits (for example SASL_PASSWORD while the cluster supplies
the brokers).
See kafka-configs.yaml for a fuller sample
(plaintext dev, TLS staging, SASL prod) and
examples/config/kite.yaml.
The current cluster is the name kite uses when @NAME and
$KITE_TARGET are absent. It lives in
$XDG_CONFIG_HOME/kite/current (or ~/.config/kite/current when
XDG_CONFIG_HOME is unset) and is written by kite cluster set (or the
picker).
kite cluster # interactive picker (arrows/j/k, Enter, q to quit)
kite cluster list # names, with ` *` after the effective cluster
kite cluster set prod # store 'prod' as the current cluster
kite produce @prod t # use 'prod' for this run onlykite cluster init asks a few questions on stderr — cluster name,
bootstrap servers, and SASL credentials — and writes the answers as a
new entry under clusters: in kite.yaml, then makes it the current
cluster. Entering a SASL username produces a Confluent Cloud-style
SASL_SSL/PLAIN cluster; leaving it empty produces a PLAINTEXT one.
Other protocols, SCRAM mechanisms, and CA bundle paths are set by
editing kite.yaml directly. The rest of the file (comments, default:,
defaults:, other clusters) is left untouched; an existing name is
replaced only after confirmation. The target is the config file kite
would read (--config picks another), or $XDG_CONFIG_HOME/kite/ kite.yaml when none exists. Answers can also be piped, so scripts can
run the wizard non-interactively.
On a terminal with both stdin and stderr attached, bare kite cluster
opens a picker drawn on stderr (stdout stays clean); without a terminal
it prints the same output as kite cluster list. A stored name that the
config file no longer defines is ignored with a warning, and kite falls
back to default:.
To see which settings are actually in effect — flags over environment
over the config file — run any command with -v: it prints the config
sources on stderr before connecting.
- Producer: stdout is never written; the summary and diagnostics go to stderr.
- Consumer: records on stdout; diagnostics and the end-of-run summary on stderr.
- Exit 0 on success, 1 on any error (usage, configuration, connection,
broker), 130 when a consume is stopped with Ctrl-C. Usage errors print
kite: MESSAGEfollowed by a one-lineTry 'kite --help'hint.
On a terminal the output is dressed up for readability: the end-of-run
summary becomes an aligned block, kite topic list prints an aligned
table of partitions and replication factors, kite cluster list marks
the current cluster with a leading *, and consume output colors the
record keys, headers and JSON syntax. Pipes and files get the stable
plain forms described above, byte for byte.
Every run ends with a summary on stderr. Produce reports the record count,
partitions used, bytes, elapsed time, message and byte rates, the last
acknowledged offset per partition (p0=333, p1=332), the number of produce
requests and retried batches, average send-to-ack latency per request
attempt (excludes waiting on stdin and connection setup), and connections
used. Consume reports the record count and why it stopped ((idle timeout),
(interrupted)), plus the same throughput line and per-partition offsets.
While stderr is a terminal, a live one-line status on stderr shows the topic,
count, message rate, byte rate, current offset, and elapsed time; a consumer
that has not yet received anything shows waiting for records ... Ctrl-C to stop with a hint such as new records only; use -B for history. The live
line is disabled by -v / --verbose. Terminal colors can be disabled with
a non-empty NO_COLOR, forced with KITE_COLOR=always, or disabled
explicitly with KITE_COLOR=never. Nothing is colored when the stream is a
pipe.
-q/--quietsuppresses the live status line and the end-of-run summaries; data, warnings, and errors are unchanged.-v/--verboseenables connection, retry, and fetch diagnostics.KITE_DEBUG=1enables frame and TLS diagnostics.KITE_TIME=1prints producer timing totals and connection count.
The default stripped binary is under 700 KB (CI-gated by
scripts/check-size.sh).
scripts/pack.sh can produce an optional UPX/LZMA artifact of about 200 KiB.
No Kafka 4.0+ broker for the Quickstart? A single-node broker
on your laptop is one docker run away. The
commands below need Docker, curl,
and (for the JSON example only) jq:
docker --version && curl --version | head -1 && jq --versionStart Kafka with the Apache Kafka native image (bound to loopback only) and wait until it answers:
docker run -d --name kite-kafka -p 127.0.0.1:9092:9092 apache/kafka-native:latest
until docker exec kite-kafka /opt/kafka/bin/kafka-topics.sh \
--bootstrap-server localhost:9092 --list >/dev/null 2>&1; do
sleep 2
done
echo "kafka ready"Check on it or tear it down later with:
docker ps --filter name=kite-kafka # status
docker logs kite-kafka | tail # broker log
docker rm -f kite-kafka # stop and removeThe default image auto-creates topics, so the Quickstart works
unchanged. For a fuller check against the same image, scripts/e2e-docker.sh
runs the whole broker-backed suite in a throwaway container; see
TESTING.md.
-b HOST:PORT is only how kite finds the first broker. The metadata
that broker returns lists every broker by its advertised listener
(advertised.listeners), and kite connects to those addresses for the
actual produce and fetch traffic. If they are not reachable from where
kite runs, the bootstrap connection succeeds and the very next step
fails with connection refused or a timeout.
The default image advertises localhost:9092, which is why the
-p 127.0.0.1:9092:9092 mapping above works and why simply remapping
the host port does not: with -p 19092:9092 the broker still tells kite
to come back on localhost:9092. Set the advertised address to what the
host sees. Any KAFKA_* variable replaces the image's whole default
config, so the KRaft basics have to come along:
docker run -d --name kite-kafka -p 127.0.0.1:19092:9092 \
-e KAFKA_NODE_ID=1 \
-e KAFKA_PROCESS_ROLES=broker,controller \
-e KAFKA_CONTROLLER_QUORUM_VOTERS=1@localhost:9093 \
-e KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER \
-e KAFKA_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 \
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:19092 \
-e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \
apache/kafka-native:latest
until kite produce -q -b localhost:19092 probe </dev/null 2>/dev/null; do sleep 2; doneThe readiness loop from the previous section will not work here:
kafka-topics.sh inside the container follows the same advertised
address, and localhost:19092 means nothing in there. Probe from the
host with kite instead, as shown (an empty produce creates the topic
and exits 0 once the broker answers).
With several brokers each one needs its own host port and its own
advertised address on that port, while brokers keep talking to each
other over the Docker network. Two listeners per broker do that:
INTERNAL (container names, used between brokers) and EXTERNAL (what
the host sees). The common settings, then one container per broker:
docker network create kafka-net
common=(
-e KAFKA_PROCESS_ROLES=broker,controller
-e KAFKA_CONTROLLER_QUORUM_VOTERS=1@kafka-1:9093,2@kafka-2:9093
-e KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER
-e KAFKA_LISTENERS=INTERNAL://:29092,EXTERNAL://:9092,CONTROLLER://:9093
-e KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT
-e KAFKA_INTER_BROKER_LISTENER_NAME=INTERNAL
-e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=2
-e KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=2
-e KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1
-e CLUSTER_ID=5L6g3nShT-eMCtK--X86sw
)
docker run -d --name kafka-1 --network kafka-net -p 127.0.0.1:9092:9092 "${common[@]}" \
-e KAFKA_NODE_ID=1 \
-e KAFKA_ADVERTISED_LISTENERS=INTERNAL://kafka-1:29092,EXTERNAL://localhost:9092 \
apache/kafka-native:latest
docker run -d --name kafka-2 --network kafka-net -p 127.0.0.1:9093:9092 "${common[@]}" \
-e KAFKA_NODE_ID=2 \
-e KAFKA_ADVERTISED_LISTENERS=INTERNAL://kafka-2:29092,EXTERNAL://localhost:9093 \
apache/kafka-native:latest
kite consume -b localhost:9092,localhost:9093 -B eventsThe same rule explains the classic symptoms on any deployment, not just
Docker: a broker that advertises an internal hostname (kafka-1, a
private IP, a Kubernetes service name) is reachable for bootstrap through
a tunnel or port-forward yet fails right after, because kite is being
sent to an address only the cluster's own network can resolve. The
error names the advertised address (connection refused by kafka-1:9092),
and kite produce -v logs connected broker N at HOST:PORT for each one it
reaches. Fix it on the broker side; a client cannot rewrite those
addresses.
kite 0.2 replaced the mode flags with subcommands; the old spellings exit
1 with a migration hint. @NAME (or $KITE_TARGET) now selects the
cluster everywhere.
| 0.1 | 0.2 |
|---|---|
kite [OPTIONS] TOPIC |
kite produce [OPTIONS] [@CLUSTER] TOPIC |
kite -c/--consume [OPTIONS] TOPIC |
kite consume [OPTIONS] [@CLUSTER] TOPIC |
kite --show-config [OPTIONS] |
removed; use kite -v / kite cluster list |
kite --targets [OPTIONS] |
kite cluster list |
kite --target NAME ... |
kite ... @NAME or kite cluster set NAME |
First run against a local broker:
connection refused by localhost:9092: nothing is listening on 9092. Start the broker (see Run Kafka locally with Docker) and wait for the readiness loop to finish;docker ps --filter name=kite-kafkashows whether the container is up.- Bootstrap works, then
connection refused by HOST:PORTfor another address (or a hang) on the first produce/fetch: the broker advertises an address kite cannot reach — typical after remapping the Docker port or with several brokers on one host. See advertised listeners. Cannot connect to the Docker daemon/docker: command not found: start Docker Desktop orsudo systemctl start docker, or install Docker; on Linux, add your user to thedockergroup to run withoutsudo.jq: command not found: installjq(apt install jq,brew install jq) or drop--json | jq -r .valuefrom the consume line; kite itself does not need it.- Produce fails with
not authorizedor a topic-creation error: the broker disables automatic topic creation or the principal lacks the ACL; create the topic with your admin tooling and retry. - Consume prints nothing and exits after the idle timeout:
-Bwas omitted, so the read started at the latest offset, after the record you produced. Add-B(or produce again while the consumer is running).
General:
no broker configured: pass-b HOST:PORT, setBOOTSTRAP_SERVERS, or copy a template to one of the search-path locations and editbootstrap.servers.-vprints which sources were used.- Topic does not exist: when producing, kite offers to create it on the spot — it asks on a terminal and creates automatically in scripts and pipes, using the broker's default partition count and replication factor. Consuming a missing topic is an error; kite never creates it there. On some hosted clusters a missing topic surfaces as an authorization error instead.
connection refused by HOST:PORT/cannot resolve host: check the address, DNS, and firewall rules; run with-vfor connection diagnostics.- Authentication or TLS failure: check the SASL settings and CA bundle;
use
KITE_DEBUG=1for handshake details. not authorized to write/read: the principal lacks ACLs for that operation on the topic.batch of N exceeds the broker's max.message.bytes: split the input or raise the topic/broker limit.offset N is out of range: the message shows the valid range and the next offset; use-Bfor the earliest or omit--offsetfor the latest.- Consumer shows
waiting for records: the default starts at latest. Use-B,--offset, or a bounded--idleas appropriate. kite -c TOPICfails with'-c' is now a command: kite 0.2 turned the mode flags into subcommands. See Migrating from 0.1.
How do I produce a message to Kafka from the command line?
printf 'hello\n' | kite produce -b localhost:9092 TOPIC. Each stdin line is one
record; key<TAB>value sets a key, --json and --csv accept structured
input.
How do I read the last N messages from a Kafka topic in a script?
kite consume -B -n N --idle 3s TOPIC reads from the beginning and stops after N
records or 3 s of silence, whichever comes first (-n alone waits until N
records exist). Without -n, a piped kite consume stops on its own after 5 s
without data, so kite consume -B TOPIC > dump.txt produces a snapshot rather
than hanging.
Does kite work with Confluent Cloud or Amazon MSK?
Yes. Any broker that speaks the Kafka protocol works. Set
SECURITY_PROTOCOL=SASL_SSL, SASL_MECHANISM=PLAIN (or SCRAM-SHA-256 /
SCRAM-SHA-512), SASL_USERNAME, and SASL_PASSWORD, or copy a template
from examples/config/.
Does kite support Schema Registry, Avro, or JSON Schema?
kite itself is schema-agnostic and does not connect to Schema Registry. For
Confluent Schema Registry with JSON Schema, use
wing in the pipe, for example
kite consume --json orders | wing read. Avro and Protobuf are not supported
by either kite or wing.
Does kite need Java, the JVM, librdkafka, Docker, or Python? No. kite is one static binary with no runtime dependencies.
How big is kite and how fast does it start? The stripped binary is under 700 KB (CI-gated) and starts in milliseconds.
Can kite create topics or manage consumer groups?
kite produce creates a missing topic — after a y/N prompt on a terminal,
automatically from a script or pipe (a broker policy or missing ACL can
refuse). Declining the prompt exits 1 without creating it. kite consume
never creates topics. kite does not delete or reconfigure topics, join
consumer groups, or commit offsets; each consume run is stateless.
Is kite safe to call from an AI agent or CI job?
Yes: it never prompts, never hangs in a pipe, writes data only to stdout and
diagnostics only to stderr, and returns exit code 1 with a one-line
kite: ... message on any failure.
Which compression codecs can kite consume? gzip, Snappy, and LZ4. zstd is intentionally omitted to keep the binary small.
zig build
zig build test
scripts/cli-check.sh
scripts/check-size.sh
scripts/pack.sh
scripts/smoke.sh TOPICSee TESTING.md for broker-agnostic and broker-backed checks.
Releases are cut by pushing a v* tag; the release workflow cross-compiles
Linux and macOS binaries and attaches them with SHA256SUMS.
Internals:
src/protocol.zig— Kafka encoders, framing, and record batchessrc/decompress.zig— gzip, Snappy, and LZ4 decoders (no zstd, to keep the binary small)src/transport.zig— TCP and TLS transportsrc/client.zig— bootstrap, metadata, SASL, produce, fetch, and retrysrc/consumer.zig— ListOffsets/Fetch loop and output formattingsrc/scram.zig— SCRAM-SHA-256/512 clientsrc/config.zig—kite.propertiesloadersrc/csv.zig— CSV reader and JSON conversionsrc/cli.zig— argument parsing, subcommand dispatch, help text
Kite is licensed under the Apache License 2.0.
