---
title: "Docs — Krowk CLI and upload API"
canonical_url: "https://krowk.com/docs"
last_updated: "2026-09-02T08:07:52.754Z"
meta:
  description: "Install the Krowk CLI, push screenshots and recordings from AI agents like Claude Code and Cursor, and get links that render as cards in GitHub, Slack, Basecamp and Linear. Full command reference and the raw upload API."
  "og:description": "Install the Krowk CLI, push screenshots and recordings from AI agents like Claude Code and Cursor, and get links that render as cards in GitHub, Slack, Basecamp and Linear. Full command reference and the raw upload API."
  "og:title": "Docs — Krowk CLI and upload API"
---

**Docs**

# **The CLI and the API, on one page. **

Everything an agent, or the person wiring one up, needs to push a file and get back a link that renders as a card. The CLI is MIT, at [~~github.com/krowkcom/cli~~](https://github.com/krowkcom/cli).

## Install

One script puts the CLI on the path:

terminal

```
$ curl -fsSL https://krowk.com/install | bash
```

Or skip installing anything: `npx @krowk/cli` runs the same binary, and Go users can `go install github.com/krowkcom/cli/cmd/krowk@latest`. It is one static binary with no runtime, because agent containers rarely have Node and the upload step should never fail for boring reasons.

## First push, no account

agent shell

```
$ npx @krowk/cli push screenshot.png

✓ uploaded  screenshot.png  412 KB
  https://krowk.com/a/art_00000000000000000000demo
  expires in 24h
  keep it:  krowk claim art_00000000000000000000demo krowk_claim_...
```

A keyless push lands in the anonymous workspace: the link works immediately and expires 24 hours later. The result also carries a **one-shot claim token** (`krowk_claim_…`). It is shown once and never again, so an agent that wants the link kept should surface it. One file is one artifact is one link; pushing three files creates three artifacts with three URLs.

## Keeping a link

Links in a claimed workspace do not expire. To move an anonymous upload into one:

1. [Create a workspace](https://app.krowk.com/registration/new) at app.krowk.com and generate an API key ( `krowk_sk_…`) in the dashboard.
2. Log the CLI in. The key is verified against the registry before it is stored:terminal

   ```
   $ krowk auth login --token krowk_sk_...
   ```


3. Spend the claim token the push printed:terminal

   ```
   $ krowk claim art_00000000000000000000demo krowk_claim_...
   ```



Claiming works for 30 days after a link expires, and the URL comes back exactly as it was. With a key configured, every push lands in your workspace directly and there is nothing to claim.

## Commands

| Command | What it does |
| --- | --- |
| `krowk push <file...>` | Upload files, get a link for each |
| `krowk uploads create <file...>` | The same thing, spelled out |
| `krowk uploads list` | List the workspace's uploads, newest first (--limit, --before) |
| `krowk uploads show <artifact>` | Read one artifact back |
| `krowk uploads attach <artifact> --run <run>` | Put an upload under a run after it was uploaded |
| `krowk runs start` | Open a run to group later uploads under |
| `krowk runs finish <run>` | Close a run |
| `krowk claim <artifact> <claim-token>` | Keep an anonymous upload past its expiry (--run groups it while claiming) |
| `krowk auth login --token <token>` | Verify a key against the registry, then store it in \~/.config/krowk/credentials.json |
| `krowk auth token` | Print the stored token, for scripts |
| `krowk auth verify` | Ask the registry which key this is, and the workspace it acts in |
| `krowk doctor` | Report version, API reachability, auth and detected run context |

Upload flags: `--run`, `--pull-request`, `--reference` (repeatable), `--session`, `--title`, plus `--repo`, `--commit` and `--agent` to override what the CLI detects from the environment. Global flags: `--format human|json|markdown|url`, `--json`, `--quiet`, `--dev`. Output is human on a TTY and JSON when piped, so an agent that captures stdout gets structured data without asking for it.

## Pasting the result

There is no single paste-ready string, because the destinations disagree about what they render:

| Destination | Use | Why |
| --- | --- | --- |
| GitHub, Linear, Notion | `--format markdown` | They build preview cards only for their own resources. The inline image embed is what actually renders the artifact in a PR comment. |
| Slack, Basecamp | `--format url` | Both unfurl a bare URL into a card of their own. Slack renders no markdown image embeds at all. |

`--json` carries both forms under `paste`, one line per artifact, with link labels escaped for CommonMark so a filename like `frame[0].png` pastes as a working link.

## Runs: where the metadata lives

An artifact is just the file. The repo, commit, branch, agent, pull request and session live on a **run**, and artifacts attach to it. With a key, every push not given `--run` opens and closes its own run; `krowk runs start` opens one to group a whole session under. Keyless uploads have no run: metadata named by flag is not recorded, and the result says so rather than dropping it silently.

## The API, with curl

The CLI speaks a documented REST API at `api.krowk.com/v1`, so you can upload with nothing installed. Bytes never pass through the registry. An upload is three steps:

1. **Declare.** `POST /v1/artifacts` with the filename, content type, byte size and hex-encoded SHA-256 checksum. The response carries the artifact's slug and a presigned `upload.url` with the exact headers to send. Size and digest are declared up front because they are signed into the URL, which is how storage refuses an oversized or corrupted body at the edge.
2. **Upload.** `PUT` the bytes straight to that URL.
3. **Finalize.**`PUT /v1/artifacts/:slug/finalization` confirms what landed. Idempotent: a retry gets the same success.

terminal

```
$ curl -s https://api.krowk.com/v1/artifacts \
  -H "Content-Type: application/json" \
  -d '{
    "filename": "screenshot.png",
    "content_type": "image/png",
    "byte_size": 421888,
    "checksum": "<hex sha-256 of the file>"
  }'
```

Authenticate with `Authorization: Bearer krowk_sk_…` to land the artifact in your workspace; without it the upload is anonymous and the declare response includes the claim token. Every failure has one shape, `{ "error": { "code", "message", "details" } }`, with a machine-readable code and the actual offending value, so a client (or an LLM) can act on it without parsing prose.

## Limits

|  | Free | Pro | Business |
| --- | --- | --- | --- |
| Link lifetime | 24 hours | Never expires | Never expires |
| Uploads | 100 a day | 10,000 a month included | 30,000 a month included |
| Artifact size | 100 MB | 2 GB | 2 GB |
| Storage | — | 100 GB included | 100 GB included |
| Recovery window | 30 days after expiry | — | — |
| Visibility | Public | Public | Private and shared |
| Access logs | — | — | Per-fetch |

Rates past the included amounts are published in full on the [pricing section](https://krowk.com/#pricing). Separately from those, every key, and every address pushing without one, has a per-minute ceiling on new uploads, on by default and not switchable off: past it the API answers `429` with `Retry-After`, and nothing new lands until you wait the seconds it names. A hard spend cap is coming, but not yet.

## Where to go next

This page is the reference. For the same material as a walkthrough — getting a screenshot from an agent into a pull request, or out of a CI run — see [the guides](https://krowk.com/guides). For why the card carries a commit at all, there is [how it works](https://krowk.com/how-it-works) and [the argument for provenance](https://krowk.com/blog/agent-output-provenance). The refusals the CLI and the MCP server make, with the reasoning, are on [the security page](https://krowk.com/security).