A Rust CLI that is one stage in a pipeline: prompt and context in on argv/stdin, the answer on stdout, diagnostics on stderr, a verdict in the exit code. It speaks to any OpenAI-compatible endpoint; built against llama.cpp's server, since verified against a remote gateway and an MLX one.
cargo build --release
./target/release/clank [OPTIONS] [PROMPT...]Recorded against a live server by scripts/record-demo.sh: the four read-only
observers, the pipe as context, --each over three items, a schema-constrained
answer through jq, and a failure with its reason and exit 1.
- CHEATSHEET.md — flags, one-liners, integrations
- PROTOCOL.md — the contract: invariants, request sequence, context doctrine, event set, exit codes
- docs/use-cases.md — the job families, each with its gate and its price
- docs/clank-jev.md — typed decisions for routing: providers, gates, six composed examples
- docs/macbook-omlx-local-inference.md — local models on a 16 GB Mac: which model per job, hallucination probes, what does not work
- docs/history/ — closed research records, cited by the docs above
- .jev/README.md — the repository's own design invariants, as rules Jev enforces in an editor
- STATUS.md — where the project is, what is open, how to check it
- .omp/rules/ — the same conventions as stream rules, which steer a model while it writes
Not on crates.io: the crate named clank there is an unrelated project. Two
binaries, clank and its decision-stage sibling clank-jev, six direct
dependencies, and nothing system-provided beyond a C compiler (ring, for TLS;
no OpenSSL to find).
Prebuilt archives are attached to each
release — linux x86_64, macOS
arm64 and macOS x86_64 — each holding both binaries, this README, the LICENSE,
the cheatsheet and the protocol, with a SHA256SUMS-style .sha256 beside it:
gh release download --pattern '*x86_64-unknown-linux-gnu.tar.gz*'
sha256sum -c clank-*.tar.gz.sha256 # macOS: shasum -a 256 -c
tar xzf clank-*.tar.gzFrom source, which is what you want if you are wiring it into something else:
cargo install --locked --git https://github.com/makefunstuff/clank # -> ~/.cargo/bin/clank--locked holds the dependency graph to the committed Cargo.lock;
--rev <sha> pins clank itself. That directory is on PATH if you have
installed anything with cargo before. From a checkout instead — src/ is 2.8k
lines:
git clone https://github.com/makefunstuff/clank && cd clank
cargo build --release # -> ./target/release/clank
cargo test # no model and no network: a stub SSE server and a stub
# JSON one, driven by the real binariesAt run time it needs an OpenAI-compatible endpoint that does tool calling.
llama.cpp's llama-server is what it was built against, and the only piece here
that wants a GPU:
export CLANK_BASE_URL=http://127.0.0.1:8080/v1
export CLANK_MODEL=$(curl -s "$CLANK_BASE_URL/models" | jq -r '.data[0].id')
clank -m 'reply with exactly: pong' # -> pong, exit 0The built-in defaults (http://127.0.0.1:40583/v1, qwen3.8-27b-gsq-rco-iq3xxs)
are one machine's. Those two variables plus the flags in
Configuration are the whole configuration: no config file, no
state on disk. A model the server does not serve is the usual first failure, and
it is not silent — the server's reason goes to stderr and the exit code is 1.
Container instead of a toolchain: In a container.
The surface is stdin, stdout, stderr and an exit code. No daemon, no session
store, no memory you did not hand over, so everything a persistent agent harness
would do internally is a pipeline step you can read: rg, git diff, jq,
sed, xargs -P. The input is visible (the context is exactly the pipe), the
cost is visible (one call is one call, with no loop re-sending a growing
history), and the write stays yours: clank observes and proposes, a gate
decides, you apply. In a container with the tree mounted read-only that boundary
belongs to the kernel rather than to clank's promises, and it bounds what the
model can write, not what it can reach.
What it does not give you: memory across sessions, retrieval you did not construct, or a loop that edits your code while you are away.
Ask a model about text you already have, and get back something your shell can act on:
rg -n -C3 "userData" src/ | clank --thinking off -m "what does this do?"
git diff | clank --thinking off --max-tokens 400 -m "review this diff, one line per issue"
rg -n "TODO" src/ | clank --each --thinking off -m "one line: actionable now, or not?"
clank --json-schema @schema.json -m "extract the findings" | jq -er .
clank --jsonl -m "summarize" | clank -m "what did you say?"
clank --system @prompts/review-sh.md -c script.sh -m "review the script"- stdout is data. The answer, framed
--eachanswers, or--jsonlevents (run,item,tool_call,tool_result,assistant,error), and nothing else. A--jsonlstream opens with arunevent naming the model, the endpoint and the argv (API keys redacted), so a trace says what produced it. - stderr is diagnostics. Tool breadcrumbs, reasoning under
--show-thinking, errors. Token accounting stays in the server's log, where it already lives. - exit codes.
0ok;1failure — model, server, IO, a truncated answer, an empty answer, or any failed--eachitem;2usage. Truncation and emptiness are failures, not results: a stage that reports a success it cannot back is worse than one that fails. - prompt comes from
-m TEXT, positional text, piped stdin (stdin is the prompt only when no other prompt is given), or one line from a TTY. - context is piped stdin, which becomes a context node when a prompt is also
given: a clank trace renders as a transcript, anything else as text.
-c FILE(repeatable) loads a saved context — JSON tree, text, or a trace.-cnodes come first, then the pipe; a pipe that reaches the process is never dropped. - tools are offered only when nothing was piped — no stdin, no
-c, no--eachitems — because with no evidence to hand over, the only honest answer about your workspace is one that looked at it. Every lookup lands on stderr. When evidence was supplied, that is the evidence and nothing else.--no-toolsforces the blind case, and the prompt then forbids citing what it never saw. - no colour, plain text on both channels.
--quietdrops the breadcrumbs. - SIGPIPE restored (Rust sets SIG_IGN by default), so
clank … | headdies cleanly; output is line-buffered and flushed per delta.
A context file is plain text (one text node) or a JSON tree:
[
{ "text": "raw text node" },
{ "file": "path/relative/to/cwd" },
{ "children": [ { "file": "a.md" }, { "text": "more" } ] }
]File leaves are read at render time and emitted as ─── path ─── plus content.
A -c file is validated strictly, because a file is configuration someone wrote
on purpose; the pipe parses leniently, because it is evidence.
One prompt over every item on stdin, serially, one conversation per item. The
items are the context, so the prompt has to come from -m/positional.
git log --format=%s -3 | clank --each -m "one line: rewrite in the imperative mood"
find src -name '*.rs' -print0 | clank --each -0 -m "one line: what is this path for?"Text mode frames each answer with the item it belongs to, so stdout maps back to
stdin (awk '/^─── item /{…}' splits it):
─── item 1/3 ───
readme: add install section and fix name clash
─── item 2/3 ───
use-cases §9: implement loop and goal in eight lines of shell
─── item 3/3 ───
readme: third shorter and remove essay voice
An item is text, not a file. The item is the context, so
rg -l "TODO" src/ | clank --each -m "summarize this file" hands the model a
filename and nothing else, and it answers accordingly — measured on oMLX
(2026-09-19): "This file exists but its purpose is not described in the
provided context." To summarize files, let the shell read them:
for f in src/*.rs; do
clank -c "$f" -m "one line: what is this file responsible for?" </dev/null
echo
done</dev/null because clank reads stdin: without it the first call eats the rest
of the list. Content you already gathered maps fine:
rg -n "TODO" src/ | clank --each -m "one line: actionable now, or not?".
--jsonl adds i (1-based) and of to every event and emits one item event
carrying the input before the work starts, so a consumer never has to guess. A
failed item is an error event and a stderr line, and it does not stop the run;
the exit code is 1 if any item failed, and an empty item list is not a
failure. Everything except the trailing item block is byte-identical from item
to item, which is what lets the server reuse the prompt prefix. There is no
parallelism inside clank: -P belongs to xargs.
The model is the constraint (clank's own share is 1 ms of startup), so the lever is the tokens you pay for:
| rule | measured |
|---|---|
--thinking off for mechanical work |
1.35 s → 0.25 s |
| keep the prompt head stable, vary the tail | 10.4 s → 0.21 s (cached prefix) |
stop reading when you have enough (clank … | head -1) |
7.1 s → 0.6 s |
| one call holding many items beats many small calls | 1.5× on four items |
Per call: ≈0.25 s fixed, ≈0.02 s per output token, prompt tokens at 1.3 ms cold and 0.02 ms cached. docs/history/research-harness-constraints.md §5.2 has the raw numbers and §5.3 the comparison against headless pi.
| tool | description |
|---|---|
read_file(path, start_line?, end_line?) |
numbered file content |
list_dir(path) |
directory entries (name, type, size) |
search(pattern, path?, ignore_case?, context_lines?, glob?) |
regex search, file:line: text |
stat(path) |
file/directory metadata |
A tool call is the wrong way to feed a stage: it is a worse rg with an input
nobody can see in the pipeline. The composable form of a lookup is a pipe, and
the composable form of looking twice is a second stage. Reach for --tools when
one lookup inside one stage is cheaper than a second stage.
search is deliberately weaker than ripgrep: line-based, single-line patterns,
capped at 500 matches and 4 MB per file, skipping binaries, symlinks and .git.
Flags: ignore_case, context_lines (0–10), glob. For anything heavier,
compose with the real tool. Tool errors go back to the model as data; a bad
context file or a server error is a process failure.
A second binary in this crate: ask typed questions about a state, get answers a
shell can branch on. clank writes prose; clank-jev picks one of your
options and reports how sure it is, in the exit code as well as the answer.
route=$(printf '%s' "$task" | clank-jev --ask 'What kind of task is this?' \
--choice code,prose,math --min-prob 0.7) || route=unclear
case "$route" in
code) clank --model local-code -c src/context.rs -m "$task" ;;
*) clank --model local-fast -m "$task" ;;
esacProviders are typesafe, openrouter and a local kev (no credential, no
network); exit codes 0/1/2/3 separate "decided", "a gate failed",
"usage" and "could not ask". docs/clank-jev.md has the
provider table, the closed-choice reason sets, the --min-prob semantics, and
six composed examples with their observed answers.
| flag | meaning |
|---|---|
--ask TEXT |
one question; needs exactly one of the three shapes below |
--choice A,B,C |
unordered options, printed by name |
--boolean |
yes/no, printed as true or false |
--score low,mid,high |
ordered levels, lowest first; prints the level number |
--checks FILE |
a JSON file of questions: {id: {type, instructions, criteria, reasons?}} |
--min-prob P |
exit 1 unless the decision is at least this confident |
--expect VALUE |
exit 1 unless the decision equals this (one question) |
--expect-min N |
exit 1 unless an ordered decision is at least this level |
--print-reason |
print the closed-choice reason instead of the value |
--provider NAME |
auto, typesafe, openrouter, kev |
--model ID |
defaults per provider: jev-latest, typesafe/jev-1.13, kev-latest |
--base-url URL |
move the endpoint (a local kev, a proxy, a stub) |
--timeout SECS |
per-request timeout, default 60 |
--json |
the full result object instead of the bare value |
-q / --quiet |
no diagnostic line on stderr |
Flags override $CLANK_* environment variables, which override built-in
defaults:
| flag | env | default |
|---|---|---|
-m / --message / positional |
— | — |
-c / --context |
— | — |
--each |
— | false |
-0 / --null |
— | false |
--tools |
— | on only when nothing was piped |
--no-tools |
— | false |
--thinking |
— | server default |
--show-thinking |
— | false |
--model |
CLANK_MODEL |
qwen3.8-27b-gsq-rco-iq3xxs |
--base-url |
CLANK_BASE_URL |
http://127.0.0.1:40583/v1 |
--api-key |
CLANK_API_KEY |
none |
--timeout |
CLANK_TIMEOUT |
600 s per request |
--max-tokens |
— | 8192 |
--json-schema |
— | none |
--system |
CLANK_SYSTEM |
— |
--list-tools |
— | — |
--max-rounds |
— | 12 tool rounds per prompt (with --tools) |
--jsonl, -q/--quiet, -h/--help |
— | — |
demo.sh is the reference pipeline. Stage 1 writes a script that renders
fixtures/notes.md as HTML; stage 2 critiques it against the fixture and the
shell's own measurements; stage 3 finalizes it under --json-schema; stage 4
has the model propose a command that verifies the result. The fixture is piped
at every stage, so no stage asks the model to read the filesystem; every stage
is gated, and a non-zero exit, an answer that is not the expected JSON, or a
script that fails bash -n stops the run. One --jsonl trace per stage lands
in local/demo-traces/. Its first version printed "passed" after producing a
broken artifact — what the gates are for.
Dockerfile builds a 32 MB image (33,063,772 bytes by docker image inspect)
holding both binaries and nothing else.
docker build -t clank .
git log -1 --stat | docker run --rm -i --network=host \
-v "$PWD":/w:ro -w /w \
-e CLANK_BASE_URL=http://127.0.0.1:4000/v1 \
-e CLANK_MODEL=deepseek/deepseek-v4-flash \
clank --thinking off -m "The context is git log --stat. One line: what changed and the risk it carries."Verified 2026-09-19 with the repo mounted read-only; it answered:
docs/use-cases.mdgained §8 (87 lines) andPROTOCOL.mdone line, … the risk is that the units are unverified in-place — only hand-tested once, where a truncated run left arunevent with noassistant.
With nothing piped, the read-only observers look around inside the container, each lookup on stderr:
> search context_lines=0 glob=* ignore_case=true path=. pattern=exit.?code|EXIT_|exit\(2\)|return 2
< search ok (3019 B)
> list_dir path=.
< list_dir ok (326 B)
and the answer cited src/main.rs:10.
Four details are not optional. -i is what lets the pipe reach it.
--network=host makes a model on localhost reachable, and is also the limit of
the sandbox. :ro is not decoration: mounted read-only, touch /w/pwned
returns Read-only file system. And not alpine: clank is glibc-dynamic, and
musl has no loader for it; ubuntu:24.04 and debian:stable-slim both work.
Each claim was run; the dated record behind it, with the endpoint, model and quoted output, is in docs/history/verification-log.md.
- Endpoints. llama.cpp on
:40583and:37313, a gateway on:4000, and oMLX 0.7.0 on:8000serving three MLX models: streaming, tool calling and reasoning channels on all of them; a fourth model the runtime refuses with a 409 that clank prints verbatim. json_schema. Enforced by the server where it has been measured, not guaranteed elsewhere: the gateway returned a fenced block and clank exited 1. A schema and tools never share a request (400, curl-probed).- The tools fallback. A no-context question invented
src/renderer.ts:14at exit 0; with the observers offered, the same question answeredsrc/context.rs:68and cap400. A model that fabricates still fabricates — the exit code and the two channels are what survive. --thinking.offsendschat_template_kwargs.enable_thinking=false, a level sendsreasoning_effort, an unknown level is exit 2, and reasoning reaches stderr only under--show-thinking.cargo test. The real binaries against a stub server: one request by default, the schema/tools split, the pipe as context,--eachframing and prefix sharing, failed items, truncation and emptiness as exit 1, argv redaction, a trace as context.tests/docs.rsguards the docs against the code;tests/rules.rsguards.jev/rules.clank-jev. Hosted Jev (both watchdog questions ≥ 0.93, one request) and a localkev-0.6b(16/18 typed decisions, 17% shuffled control, 83 ms, no credentials).demo.sh. Four gated stages end to end: 61 s on:37313, 35 s onQwen3.5-9B-MLX-4bit, and a different gate firing on each of the other two models.- Unexercised. The schema-after-tools sequence against a live model, and the TTY-with-no-prompt path.
Code and docs are largely LLM-written and then read and curated. The commits are the record of what changed, when, and each of the claims under Verified was run.
MIT. See LICENSE.
