Skip to content

v0.12.0

Choose a tag to compare

@github-actions github-actions released this 04 Oct 10:22
· 947 commits to main since this release
e8f89dc

Added

  • krowk sync attach opens the TUI. On a terminal, a session another
    machine runs is drawn as your own are: its history, then live. Prompts go
    from the prompt box, approvals are answered in the usual dialog, and Esc
    interrupts. The status line says whether the host is there (prompts wait
    for it when it is not) and whether the session comes by the relay or
    directly. krowk --resume <id> of a synced session opens it the same way.
    The host runs prompts from a viewer in the session's default mode, so
    /mode, /model, /new and /sessions say they are the host's, and a
    viewer allows a call once or for the session, never for the host's
    project. With --json, or stdout not a terminal, it prints stream-json as
    before.

  • krowk sync init sets up your device list, with a 12-word recovery
    kit.
    It asks you to sign in again in the browser, then makes the list's
    first entry: this device and the recovery device the kit derives, with your
    user key wrapped to both. The kit's words are shown once on stderr ([Enter]
    when written down, [s] to skip) or written to --save FILE, 0600, and are
    never typed back. --start-over makes a new list; every other device on
    the old one stops syncing and asks to be paired again. Run from a device
    that holds your key, it keeps the old keys aside and seals the sessions it
    can open again under the new list; running it again (under another
    workspace's key, for that workspace's) goes through what is left, and
    krowk sync recovery discard-old drops the old keys when you say so. The 24-word recovery phrase is gone, and so is
    krowk sync register: a device is registered by being on the list.

  • krowk devices remove NAME takes a device off your list and rotates
    your key away from it. It names every device the new key goes to before it
    asks, needs a fresh sign-in, and removes any device revoked on the dashboard
    in the same post, so no new key reaches one. krowk sync status offers to
    finish a dashboard Revoke. Your other devices take the new key at their next
    sync.

  • krowk sync recover gets back in on a new machine from the kit's
    words, typed at a prompt that doesn't echo them or piped in. It verifies
    the list from its first entry, goes through every device on it with you to
    keep or remove, and only then wraps your key, to what you kept.

  • krowk sync recovery new replaces the kit at once, with the old kit's
    words, or from any device when you have none. krowk sync recovery check tests the words against the list, locally. krowk sync status
    verifies the list against the head this device pinned, and says when there
    is no kit; so does the TUI's status line.

  • Pairing a device by a short code, in the library. krowk_client::pairing
    holds both sides of krowk devices add as sans-IO state machines: an
    eight-character code (Crockford base32 less 0 and 1, shown XXXX-XXXX,
    typed in any case with spaces and dashes ignored), SPAKE2 in asymmetric mode
    bound to the peer kind, the person and both device ids, and key confirmation
    both ways before the new device's name is shown or anything is posted. One
    failed step ends the pairing, and the new device's code is consumed by the
    attempt, so a hostile registry gets one guess against each side. The SPAKE2 crate is held to magic-wormhole's vectors. Nothing
    calls it yet.

  • The client crypto for devices you own, not yet wired to any command:
    a user key per person with generations, each wrapping the one before; a
    12-word recovery kit that derives a recovery device; and a signed,
    chained device list that every client verifies against the head it last
    saw, refusing an older or forked list and any removal of the recovery
    device. Only the current kit can replace the kit, or any device when there
    is none.

  • krowk devices add and krowk sync join pair a machine by a short
    code.
    add shows XXXX-XXXX, valid ten minutes and once; join on the
    new machine takes it at a prompt, never as an argument. Both machines check
    the code, the paired one asks Add '<name>' (<os>) to your devices? [Y/n],
    and the new machine keeps the user key only once the chain it was sent
    adds exactly its keys. A wrong code, or any failure on the new machine, a
    dropped connection included, ends the pairing and asks for a new code. A
    machine that was sent the key but never confirmed it is still listed, so
    krowk devices remove can take it off; ^C on add ends the pairing.

  • krowk sync host seals under the device list as the registry has it
    now,
    extended from this machine's pin, and refuses when the registry
    cannot be asked, rather than sealing under a list a removal left behind.

  • The stand-in registry holds devices you own. krowk-devregistry serves
    a person's signed device list (verified on every post by krowk-client's own
    verifier), the user key wrapped to each device, keys bound to one device,
    a fresh sign-in stamp, and the pairing mailbox: one live pairing per
    person, ten minutes, ended by the first step out of turn. The device
    approval endpoints answer 410 sync_reset.

  • Releases are signed, come with an SBOM, and are proven reproducible.
    Each release carries checksums.txt.sigstore.json, a keyless Sigstore
    signature over the checksums of every archive, bound to the release
    workflow at that tag, and a CycloneDX SBOM of the full and lean builds,
    each signed the same way. Before anything is published, the release builds
    Linux x86-64 a second time on another runner from another path and stops
    unless every binary matches byte for byte. The npm packages carry npm
    provenance. SECURITY.md says how to verify a download and rebuild one.

  • SECURITY.md: where to report a vulnerability (security@krowk.com),
    what happens then, what is in scope, and every network call krowk makes
    without being asked. krowk has no telemetry and no crash reporting.

  • cargo deny on every pull request and at every tag. A dependency
    with a RustSec advisory, a licence outside deny.toml's list, or a
    source other than crates.io fails CI and stops a release.

  • krowk sync attach answers approvals and steers the turn. Besides
    prompts, a line typed on its stdin can be /approve REQUEST_ID,
    /allow-session REQUEST_ID or /deny REQUEST_ID, answering the tool call
    an approval.requested line names, /interrupt, which stops the running
    turn, or /steer TEXT, which adds to it. Each approval.requested line
    carries its requestId, and stderr says which command answers it. Any
    other line starting with / goes as a prompt, so skills and paths still
    work, and //TEXT sends /TEXT. krowk help sync attach lists them.

  • A synced session goes direct over Tailscale when it can. krowk sync host reads this machine's tailnet address, MagicDNS name and LAN address
    from the local tailscaled, listens there, and names the addresses to
    each viewer inside the sealed channel. A viewer tries them all beside the
    relay and moves onto the first that passes the same ticket, challenge and
    handshake the relay does, mid-session and without losing a frame; if the
    direct path goes, or its welcome is more than three seconds late, it
    falls back to the relay by itself and tries again later, less often each
    time nothing is found (10 seconds, doubling to 5 minutes). krowk sync attach prints the path in use (sync.path: relay,
    direct over Tailscale or direct over LAN). Being on the tailnet lets
    nobody in: direct paths are on only with the registry's ticket keys in
    KROWK_RELAY_TICKET_KEYS, and KROWK_TAILSCALE_SAME_USER=1 also turns
    away a connection from another tailnet user. The LAN address is offered
    only with KROWK_DIRECT_LAN=1, and never with the same-user check.
    krowk hosts lists the
    tailnet's machines tagged tag:krowk-host.

  • A file a session publishes now says what made it. Besides the session
    (krowk.session), each artifact publish pushes records the engine that
    ran the turn (krowk.engine: krowk, claude-code or
    codex-app-server), the model (gen_ai.request.model) and its provider
    (gen_ai.system), so its card on krowk.com can link back to the session
    with a krowk --resume command. A subagent's file names the subagent's
    model. Nothing from the session's log is sent; the session id, engine,
    model and provider are, in the clear, even for a synced session.

  • A session running on one machine can be watched and steered from
    another.
    The host keeps the session's lease, renewing it every 20
    seconds, streams it over the relay sealed end to end, and writes its log
    to the registry as sealed chunks with a checkpoint to attach from.
    Another of your machines attaches from the latest checkpoint in well
    under half a second, follows the session live, and can prompt it, steer
    or stop it, and answer its approvals. When the host is away the other
    machine is read-only, and a prompt typed there waits and runs when the
    host is back. A dropped connection resumes by itself with nothing lost.
    Neither the relay nor the registry ever sees the session's content.
    krowk sync host <session> syncs a session of this machine, krowk sync sessions lists what another machine can open, and krowk sync attach <session> — or krowk --resume <session> on a machine without the
    session's log — follows it there. The relay is KROWK_RELAY_URL, or
    krowk relay serve on this machine.

  • Sync calls that act as this machine are signed by its own key.
    Registering or approving a device, a lease call, writing a session or
    its log, and asking for a relay ticket now carry X-Krowk-Device,
    X-Krowk-Timestamp and X-Krowk-Signature, an Ed25519 signature by
    this machine's signing key. The registry refuses them unsigned, signed by
    another key, more than five minutes off its clock, or sent twice, so a
    workspace API key alone can no longer act as one of its devices. A
    clock more than five minutes out makes these calls fail with
    signature_stale.

  • A file a session publishes now says what made it. Besides the session
    (krowk.session), each artifact publish pushes records the engine that
    ran the turn (krowk.engine: krowk, claude-code or
    codex-app-server), the model (gen_ai.request.model) and its provider
    (gen_ai.system), so its card on krowk.com can link back to the session
    with a krowk --resume command. A subagent's file names the subagent's
    model. Nothing from the session's log is sent; the session id, engine,
    model and provider are, in the clear, even for a synced session.

  • Setting up sync now registers this machine's relay signing key.
    krowk sync init, recover and register, and krowk devices approve,
    send the public half of the signing key beside the device key, and krowk sync join sends it with the approval request, so the approval registers
    it. krowk's hosted relay uses it to tell this machine's connections from
    anyone else's. The key is set once: a different one for the same device
    is refused (signing_key_mismatch).

  • The code krowk sync join shows to approve a machine changed. It
    now covers both of the new machine's keys, so a request that copied its
    device key with someone else's signing key shows a different code, and
    krowk devices approve approves only the one request whose code is
    exactly the one you typed. Only one approval request per machine may
    wait at a time.

  • Relays admit devices on tickets the registry signs. A device joins
    a relay with a short-lived ticket, which comes with the session's lease
    for the host and on request for a viewer. krowk relay serve now takes
    --ticket-keys FILE, the registry's ticket-signing public keys, instead
    of --roster. The ticket travels in the X-Krowk-Ticket header of the
    WebSocket upgrade, and a connection without a good one is turned away
    before it takes any room on the channel, so a flood of connections
    cannot keep a session's devices off it.

  • krowk relay serve --state DIR keeps each channel's fence across
    restarts.
    It is required when --addr is reachable from the network,
    like --origin, so a restart never lets a machine that lost the lease
    host again. On loopback the relay may keep it in memory.

  • A relay keeps development sessions apart from production ones.
    A relay join now says which it is, env "production" or "development",
    and krowk relay serve gives each its own channel of a session, so a
    developer's session never shares a relay buffer or a viewer with a
    user's. krowk says "development" for a debug build, for
    KROWK_ENV=development, or for any registry but api.krowk.com.

  • krowk relay serve runs a relay of your own. It carries a synced
    session between the machine running it and the devices watching it, and
    only ever sealed bytes: it refuses anything not encrypted, and never
    stores it. A device joins by signing the relay's challenge with a new
    per-device signing key (signing.json in krowk's home, made the first
    time it is needed); only the machine holding the session's lease may
    host it. A device that reconnects picks up where it left off from the
    relay's short buffer, and the relay answers heartbeats itself, so a busy
    or quiet host never looks gone. It listens on 127.0.0.1:7790 unless
    --addr says otherwise; an address reachable from the network also
    needs --origin, the URL devices dial it by, and the banner says it is
    open. Nothing connects to it on its own yet; the host daemon and the
    terminal will once syncing sessions lands. krowk help --all lists it.

  • A synced session's log can now be stored, encrypted, in the registry.
    The machine holding a session's lease seals each piece of the log on its
    own side and uploads it straight to storage. Each piece is numbered,
    names the lease it was written under, and is chained to the piece before
    it, and the last one is marked. A device reading the log back refuses a
    piece that is repeated, out of order, missing, or spliced in from another
    machine's copy of the log, and can tell a finished log. A piece is
    refused unless it comes with the lease's current token, and one a
    previous lease holder left half-uploaded is replaced rather than blocking
    the log. Pieces are at most 64 MiB. The registry and storage only ever hold the sealed bytes.
    The pieces count on your workspace's storage meter, and never show up in
    krowk uploads list, on a card, or anywhere else a push's artifacts do.
    Nothing writes them yet on its own; the host daemon will once syncing
    sessions lands.

  • A device the workspace's owner revoked by resetting sync can come back.
    Run krowk sync register on it after the reset. The fix line on a
    device_revoked refusal now says so, where it used to point at
    krowk sync recover, which could not help.

  • Add a machine to sync by approving it from one that already syncs,
    with no recovery phrase.
    On the new machine, krowk sync join shows a
    32-character code and waits. On a machine that already syncs, krowk devices approve asks for that code, shows which machine it belongs to,
    and hands the account key over, encrypted to the new machine's key, once
    you say yes. The new machine then asks for the account key id approve
    printed (or takes it as krowk sync join <id>), asks you to confirm, and
    keeps nothing unless the ids match. Read that id off your other machine,
    never from an error message or a web page. Both commands need a person at
    a terminal: an agent told to run them is refused. krowk devices list
    shows the workspace's machines and the account key id this one holds.
    All of it needs a key to a Pro workspace; on a free one the commands
    refuse with a fix line. krowk sync init and recover still work with
    no account, on a free plan, or with the registry unreachable: the keys
    stay on this machine, and krowk sync register registers it later. With
    a Pro key they register this machine themselves, and recover then
    refuses a phrase that restored a different account key from the
    workspace's, which is how a mistyped word that happens to pass the
    checksum is caught. --name (or KROWK_DEVICE_NAME) sets what the device
    list calls this machine instead of its host name.

  • krowk sync init sets up the end-to-end encryption keys sync will
    use, and shows your recovery phrase.
    It makes a key for this machine
    and an account key, prints the account key as 24 words, and keeps it
    only once you type the words back (they are not echoed). Write them
    down, with the key id shown beside them: krowk never stores the phrase
    and cannot show it again, and it is the only way back to your sessions
    if every device is lost. On another machine, krowk sync recover takes
    the 24 words — typed at its prompt, or piped from a file (< phrase.txt),
    never with echo, which keeps them in your shell history — restores the
    same account key there and shows its key id; if the id differs, a word
    was wrong, and running recover again with the right words replaces it. The keys
    stay in krowk's home, device.json and account-key.json, both
    0600, and the wrapped account key opens only with that machine's own
    device key.

  • The host daemon can serve its sessions over a WebSocket on this
    machine
    , the transport other devices will reach it through once the
    relay lands. It is off unless KROWK_HOST_WS or host.websocket in
    config.json names a loopback address (127.0.0.1:7788; port 0 picks
    one, and krowk host status shows it); anything but loopback is
    refused. A client proves it is you with the token in host.token beside
    the daemon's socket, which is new each time the daemon starts. The
    frames are the unix socket's, sent in compressed batches.

  • The TUI's sessions now run in the host daemon, so closing the
    terminal no longer ends them.
    Bare krowk starts the daemon if none is
    running and runs every turn there. When the window closes mid-reply, the
    turn keeps going. Reopen krowk in the same directory and it tells you
    which session is still running; krowk --resume <id> (or
    /sessions <id>) then shows what happened while you were away and
    follows the rest live. Several TUIs can follow one session and see the
    same reply. /connect updates the daemon's providers too. Set
    KROWK_TUI_HOST=local to keep the TUI's sessions in its own process, as
    before. krowk host stop stops the daemon once no turn is running.

  • A session can outlive its terminal: krowk -p --daemon runs the turn
    in a per-user host daemon.
    The first krowk that needs the daemon
    starts it in the background; it listens on a private unix socket
    ($XDG_RUNTIME_DIR/krowk/host.sock, or under $TMPDIR on macOS) and
    exits after ten idle minutes with no session running — host.idleMinutes
    in config.json, or KROWK_HOST_IDLE in seconds, changes that. Kill the
    process or close the terminal mid-turn and the turn goes on; krowk host attach <session> shows it — what it has done so far, the reply being
    typed, then the rest live — and any number of clients can follow one
    session and see the same events. krowk host status says whether the
    daemon runs and what it holds. The daemon asks nobody whether to trust a
    repository, so a backend runs there only once it is trusted. The TUI
    still runs its sessions in its own process for now.

  • krowk host enable keeps the daemon running on an always-on machine,
    as a systemd user service on Linux or a launchd agent on macOS, with no
    idle exit; krowk host disable stops and removes it. A service has no
    shell variables, so give it keys with krowk connect rather than an
    exported ANTHROPIC_API_KEY.

Changed

  • A synced session opens on one line, not the splash. krowk sync attach draws the session's history, then ⇄ Attached to "<title>" on <host> (or Attached to <host> for an untitled session) — or that the
    host is away and prompts wait — with the host named as the device list
    names it. The status line says the host in
    a glyph and its name: ● <host> there, ○ <host> away, ◌ <host>
    connecting.

  • krowk sync attach --json ends when its stdin does, once every
    command sent has been answered. Before, it ran until interrupted.

  • Nothing asks you to sign in again in the browser any more. Starting
    over, krowk sync recovery new, krowk devices remove and krowk sync recover use the key you're signed in with; a browser login opens only
    when there is none, and still says what it is for.

  • Sync's device and pairing calls are under /v1/users/:user_id. The
    device list, its append and start-over (…/devices, …/devices/reset),
    a device's wrapped user key (…/devices/:id/key) and the person's one
    pairing (…/pairing), for the user the key names. The payloads are the
    same; the stand-in registry answers the new routes.

  • Synced sessions are sealed under your user key. krowk sync host
    seals a new session's key under the newest user key generation this machine
    holds, and records the generation in the wrapped key, which stays 74 bytes.
    sync attach, sync sessions and --resume open any older generation down
    the chain of wraps. A machine holding only an older generation, such as one
    removed before a rotation, can't open a newer session, and is told which
    generation it would need. The user keys a machine holds live in
    user-keys.json (0600, wrapped to its device key, replaced by rename),
    and a save never drops an older generation's wrap it already holds.
    Each session's record is signed by the machine that published it. Every
    machine that opens a session — sync host, attach, sessions and
    --resume — checks that signature against your verified device list
    (device-list.json), so any machine of yours can take a session up again,
    and a record no device of yours signed opens nowhere. A session published
    by a machine since removed from your devices still opens to read, but no
    machine hosts it again: start a new one.
    Sessions sealed under the account key no longer open: clean break. No
    command puts a user key or a device list on a machine yet, so these
    commands say it holds none until adding a device does.

  • The client protocol's types live in krowk-client. The commands,
    events and log lines, and the daemon's 28-byte frame header, are
    declared in the crate the desktop app and the phones will link, which
    pulls in no engine, tokio or reqwest; krowk_harness::protocol
    still names the same types. The generated JSON Schema is unchanged.

  • Leaving the TUI takes two presses, as in Claude Code, so one stray key no longer ends a session. On an empty prompt, Ctrl-C or Ctrl-D shows "Press Ctrl-C again to exit" (or Ctrl-D) under the prompt, and the same key again within 1.5 seconds quits.

  • The TUI's prompt sits on the same band as your messages in the chat, edge to edge across the terminal with an empty row either side, instead of between two rules. The arrow and text stay where they were. The status line lines up with the arrow, with an empty row under it, and the working line reads "12s · esc to interrupt".

  • Keys the TUI suggests stand out: in hints, the help menu, the status line, approvals and questions, each key (esc, enter, y, ctrl-g, ?) is white instead of grey like the words around it.

Removed

  • krowk devices approve and the 32-hex device code. Pairing by a short
    code replaces them; krowk sync join takes no argument.

Fixed

  • A krowk session has one id. krowk sessions listed a krowk session
    under an id of krowk.db's own, which krowk sync host refused as no
    session. A session is now stored under its log's id, the one sync host
    and --resume take; sync host also takes the id an older store listed
    it under (krowk sessions rebuild relists those under their log's id).

  • krowk sync init warns about a skipped recovery kit once, where you
    press [s], rather than again in the line after it.

  • Sync reaches the hosted relay. krowk sync host and krowk sync attach could not dial a wss:// relay, so with KROWK_RELAY_URL set to
    wss://relay.krowk.com the host was never on it: a viewer replayed the
    history but saw nothing live, and its prompts stayed queued. They now dial
    it over TLS with the same trust as every other connection krowk makes, and
    say on stderr why, once, when the relay cannot be joined.

  • KROWK_RELAY_URL is no longer needed for krowk.com. Signed in to the
    production registry, krowk sync host and krowk sync attach dial
    wss://relay.krowk.com by default; a stand-in or custom KROWK_API_URL
    keeps the local relay, and KROWK_RELAY_URL still overrides both.

  • A host the relay let go joins again. When the relay dropped krowk sync host's link without the close reaching it, or the host was stopped
    or asleep longer than the relay keeps a silent link, the host stayed off
    the relay with no error while viewers showed it gone. It now joins again
    at once, and says why on stderr.

  • A prompt sent as the host went away runs when it is back. A viewer
    that sent a command the host never received, because its link was lost
    just then, now sends it again when the host returns. The host runs it
    once.

  • /name loads the skill on every agent, not only krowk's own. A skill
    picked from the TUI's slash menu reached Claude Code or Codex as a bare
    /implement, and a vendor that did not have that skill answered that there
    was no such skill. krowk now loads the skill's instructions itself and sends
    them ahead of your words, whichever agent runs the session.

  • Skills installed with npx skills are found. krowk reads
    ~/.agents/skills and .agents/skills from the repository's root down to
    the working directory, beside the .claude/skills it already read, so a
    skill in the shared directory is listed and can be used with /name.

  • A viewer that moves to the direct path mid-session keeps receiving
    the session.
    The direct listener replays from where the viewer was when
    it started looking for the direct path. The viewer had already opened
    those batches through the relay, so it never acked them, and once the
    listener's window filled, no further event reached the viewer, though
    its own prompts and their acks still went through. An approval request
    waiting while the viewer moved is now shown once, not once per path. A
    command still running when the viewer moved is answered on the path the
    viewer is on now. A host that reconnects to the relay no longer drops the
    viewers on its direct path.

  • krowk sync host no longer hangs on a session this machine does not
    have.
    It used to take the session's lease and then wait, silent, until
    interrupted; it now fails at once with no_session and how to start one,
    before writing anything to the registry. A bridge that stops by itself
    for any other reason also ends the command then, saying why.

  • A turn sent the moment the host daemon starts no longer stalls every
    other session.
    The daemon waited on its thread for the TLS setup a
    turn's first request needs, and read a new directory's configuration
    there too; both now happen off it, so streams and heartbeats keep going.
    A TLS setup that fails once is tried again, rather than failing every
    turn until the daemon restarts.

  • A turn caught up after its client fell behind no longer loses its
    answer.
    A terminal or phone that stopped reading, and was caught up
    from where it stood just as the turn ended, could get the typing and the
    end of the turn but not the finished answer. It now always gets it.

  • A terminal whose connection to the host daemon drops mid-turn picks the
    turn up where it left off.
    It reconnects and follows on from the last
    thing it had, with nothing shown twice and nothing skipped, instead of
    reporting the turn lost. A model switch caught up by another terminal
    also no longer shows up twice.

  • krowk host stop straight after a turn keeps that turn on disk. The
    daemon waits for the turn's log to be flushed before it exits, for up to
    ten seconds. Stopped by SIGTERM or Ctrl-C, it first interrupts the
    running turns, a turn still starting included, so each is logged as
    interrupted and flushed too. The waits add up: up to ten seconds for the
    turns to end, ten for each backend to close, and ten for the flush. A
    second SIGTERM or Ctrl-C exits at once. A prompt sent while it exits is
    refused rather than lost, an idle exit's included.

  • A terminal suspended while it follows a long session no longer grows
    the host daemon's memory.
    The daemon keeps a few megabytes for each
    client that stops reading, then drops what it held and, once the client
    reads again, sends what it missed from where it stopped — nothing twice,
    nothing lost. krowk host status --json counts the bytes waiting for
    clients and how often one was caught up.

  • A slow disk no longer stalls every session in the host daemon. Each
    event a turn logs, and its context record, is written off the thread all
    sessions and heartbeats share, and still before any client sees it.

  • Quitting the TUI is instant. On a Claude Code model it took most of a second, waiting on Claude Code to flush its telemetry as it exited; krowk now starts Claude Code with telemetry off (an instance can turn it back on by setting DISABLE_TELEMETRY in its env). The screen is also handed back before anything else is tidied up, and the session is saved into krowk sessions after each turn instead of on the way out, so leaving has nothing left to wait on. A quit now takes about 30 ms, down from 0.9 s.

  • A session resumed in the TUI shows what it has cost so far. On a model models.dev has no price for yet, such as a new Claude through Claude Code, the status bar showed $— until the next turn ran; it now counts what the backend reported for each past turn, the way the live figure does.

Install

# npm — the bare `krowk` name is blocked by npm's typosquat filter
npx @krowk/cli push screenshot.png

# The installer: the full build on a workstation, the lean one in CI
# and containers (or with --lean)
curl -fsSL https://krowk.com/install | bash

# Direct download — no runtime, no package manager. krowk_… is the
# full build (the agent, its TUI, sessions); krowk-lean_… the lean
# agent-container build.
curl -sSfL https://github.com/krowkcom/krowk/releases/download/v0.12.0/krowk_0.12.0_$(uname -s | tr A-Z a-z)_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz \
  | tar -xz -C /usr/local/bin krowk krowk-mcp

# From source
cargo install --locked --git https://github.com/krowkcom/krowk --tag v0.12.0 --features harness krowk

checksums.txt covers every archive on this release, and
checksums.txt.sigstore.json is its signature. To verify a
download (with cosign):

cosign verify-blob checksums.txt --bundle checksums.txt.sigstore.json \
  --certificate-identity https://github.com/krowkcom/krowk/.github/workflows/release.yml@refs/tags/v0.12.0 \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
sha256sum --check --ignore-missing checksums.txt

The .cdx.json files are CycloneDX SBOMs of the full and lean
builds, each signed the same way. SECURITY.md in the repository says
how to rebuild a release and compare the bytes; this release's
reproduce job already did it once for Linux x86-64.