Using QuickSnip from an agent or script
15 min read
Overview
quicksnip is the command-line client for QuickSnip. Agents and scripts use it two ways: write — upload a recording or screenshot and get a share link — and read — fetch that recording back as a machine-readable JSON manifest, no video decoding required. This page covers both, in the order first-time setup actually goes: install, log in, wire up your coding agent, upload into the right organization, then read the result back.
Install
Requires Node 20 or later.
npm i -g quicksnip
quicksnip login
login opens your browser once and stores an API key locally — every script and agent on this machine authenticates from that one key afterward. Confirm it worked:
quicksnip auth status
Authenticated to https://quicksnip.com (source: file).
Set up your coding agent
If this machine runs an AI coding agent (Claude Code, Cursor, Codex) rather than just a script, let the CLI write its own instructions instead of copying anything from this page by hand:
quicksnip agents install
It looks for ~/.claude, ./.cursor, or ./AGENTS.md and picks the matching target. Preview first with --dry-run — it reports what it would do and writes nothing:
quicksnip agents install --dry-run
--target not given; using claude (found ~/.claude).
Dry run — no files were written.
would update: ~/.claude/skills/quicksnip/SKILL.md
Pass --target claude|cursor|codex|stdout to choose explicitly — stdout just prints the block instead of writing anywhere. Each target writes somewhere different:
| Target | Writes to |
|---|---|
claude | ~/.claude/skills/quicksnip/SKILL.md |
cursor | ./.cursor/rules/quicksnip.mdc |
codex | ./AGENTS.md |
If more than one surface is present, it asks instead of guessing:
--target not given and multiple agent surfaces detected: claude (~/.claude), cursor (./.cursor). Pass --target to choose one.
Running agents install again against a file it already wrote updates that file in place. If the target file exists but wasn't written by this command — an unmarked SKILL.md you wrote yourself, say — it refuses and exits 1; pass --force to overwrite it anyway (it backs up the original first).
Synced or symlinked config
If ~/.claude, .cursor, your project directory, or the target file itself is a symlink — a Dropbox- or dotfiles-synced setup is the common case, including AGENTS.md -> ~/dotfiles/AGENTS.md — agents install follows it and writes at the real location, leaving the link a link. Whenever that resolved destination differs from the path you'd expect, it says so on its own line:
created: ~/.claude/skills/quicksnip/SKILL.md
→ /Users/alex/Library/CloudStorage/Dropbox/Prefs/Claude/skills/quicksnip/SKILL.md
--json carries the same information as an additive resolvedPath field. It only refuses when the resolved destination escapes your own space entirely — a system directory such as /etc or /usr, a path owned by someone else, or one that any other account can write to — or when the link is broken, in which case it names the link so you can fix it. An ordinary sync setup hits none of those.
What actually gets installed is short, and points back at itself so it can't go stale:
## QuickSnip
Use when asked to inspect, summarize, or share the contents of a screen recording or screenshot — QuickSnip uploads it and returns a fetchable JSON manifest (transcript, chapters, frame images) instead of an unreadable video file.
Upload: `quicksnip upload <file>` — prints a share link. Add `--json` for a machine-readable result with `urls.agent`, the fetchable manifest URL.
Bulk: `quicksnip upload <directory>` uploads every supported file in the folder, paced so each video processes reliably. It journals progress in the folder, so a stopped run continues with `--resume` instead of starting over, and `--json` returns one batch document with a per-file result list.
Read back what you uploaded: fetch `urls.agent` (a plain `.json` URL, no auth needed) — it self-describes its own contract, including an `instructions` array telling you what to fetch next.
For a video, add `--wait` to `quicksnip upload` to block until it finishes processing before printing the result.
Run `quicksnip guide --json` for the full, current command/flag contract — this file can go stale; that command cannot.
Headless environments (CI)
No browser available? Set QUICKSNIP_API_KEY in the environment — the CLI reads it and takes precedence over the stored key, so a CI job never needs to run quicksnip login.
Upload a recording (the write half)
Works on a video or an image, the same way. From CLI 0.4.0 it works on any other file too (a PDF, a log, a markdown doc, a zip), which reads back as type: "file" (see Files below, and Uploading files for what differs on the way in):
quicksnip upload ./bug-repro.mp4
Uploading bug-repro.mp4...
Video uploaded to QuickSnip and is processing.
Uploaded to Acme Corp.
https://quicksnip.com/s/q09...
Choose the organization
Omit --org and it uploads to your account's default team — the same one the desktop app would use. See your options:
quicksnip orgs
org_9f24a1 Acme Corp
org_02b7cd Acme Corp — Marketing
Then pass the id explicitly:
quicksnip upload ./bug-repro.mp4 --org org_9f24a1
Pipelines should pass --org explicitly. "Default team" is default for your account — a teammate running the identical script gets their own default, which may not be the team you meant. The id is checked against your account's real organizations before anything uploads; get it wrong and nothing uploads:
Organization "wrong-id" was not found for this account. Valid organizations: org_9f24a1 (Acme Corp), org_02b7cd (Acme Corp — Marketing).
That's exit code 4.
Wait for processing
Without --wait, upload returns as soon as the bytes land — open the link immediately after and a video may still say "Preparing." Pipelines should use --wait: it blocks, polling the manifest until the recording is ready or partial, up to --wait-timeout <seconds> (default 300):
quicksnip upload ./bug-repro.mp4 --org org_9f24a1 --wait
With --wait, the command exits 0 for ready or partial, and 1 for failed, unavailable, or timeout — so a pipeline can trust the exit code without re-parsing text.
The --json output
Add --json and stdout becomes exactly one JSON document — everything else ("Uploading…", progress) goes to stderr instead:
quicksnip upload ./bug-repro.mp4 --org org_9f24a1 --wait --json
{"schema":1,"id":"q09xJ2kLp8wRt3z","type":"video","status":"partial","urls":{"share":"https://quicksnip.com/s/q09xJ2kLp8wRt3z","agent":"https://quicksnip.com/s/q09xJ2kLp8wRt3z.json","direct":null},"readiness":{"state":"partial","detail":{"media":"ready","transcript":"unavailable","summary":"unavailable","chapters":"unavailable"},"reasons":{"transcript":"no_audio","summary":"no_audio","chapters":"no_audio"}},"org":{"id":"org_9f24a1","name":"Acme Corp"}}
(That capture came from a silent clip, which is why reasons says no_audio — more on that below.) Worth reading directly: urls.share (send this to a human), urls.agent (an agent fetches this — it's the same URL with .json appended), org (which team it landed in), and readiness (the next section).
For an image there's no processing step, so status is "uploaded" and urls.direct — a direct link to the image bytes — is already populated:
{"schema":1,"id":"a1B2c3D4e5F6g7H","type":"image","status":"uploaded","urls":{"share":"https://quicksnip.com/s/a1B2c3D4e5F6g7H","agent":"https://quicksnip.com/s/a1B2c3D4e5F6g7H.json","direct":"https://quicksnip.com/s/a1B2c3D4e5F6g7H+"},"readiness":{"state":"ready"},"org":{"id":"org_9f24a1","name":"Acme Corp"}}
Uploading files
A file goes up the same way a video does:
quicksnip upload ./release-notes.md --org org_9f24a1 --json
{"schema":2,"id":"f7Gp2QxT4mR9vLc","type":"file","status":"uploaded","urls":{"share":"https://quicksnip.com/s/f7Gp2QxT4mR9vLc","agent":"https://quicksnip.com/s/f7Gp2QxT4mR9vLc.json","direct":null},"readiness":{"state":"ready"},"org":{"id":"org_9f24a1","name":"Acme Corp"}}
Five things differ from a video or an image, and a pipeline should know all five.
It carries schema: 2. Image and video results stay schema: 1, exactly as earlier CLI versions published them. The rule for a consumer is simple. schema: 1 means the result is an image or a video and nothing else. schema: 2 means new type values are possible, so a schema or a type you do not recognize is a result to ignore rather than one to guess at.
It needs a Pro plan. A free account is refused before any bytes move, at exit code 3.
Uploading a file that is not an image or a video needs a Pro plan. Upgrade at quicksnip.com/pricing.
Images and videos are unaffected by this. A server that does not offer file uploads at all exits 7 with a plain sentence saying so, rather than a raw HTTP error, so a pipeline can tell "this server is too old" apart from "this upload was rejected".
urls.direct is always null. There is no permanent unauthenticated byte URL for a file. Fetch urls.agent instead, which gives you the name, the size, and the text of a text file inline.
status can be "verifying". QuickSnip accounts a file's real size after the bytes land, and urls.agent returns 404 until that finishes. Without --wait you get status: "verifying" and a readiness.retryAfterSeconds to come back on. With --wait the CLI blocks until the file is stored and then confirms the manifest, which is what a pipeline wants. Verification waits on the upload's write lease, so budget for it: roughly 3 minutes for a small file, and over 10 for a large one that uploaded in parts. --wait-timeout defaults to 300 seconds, so raise it for anything big.
--space and --folder are applied in a second step. QuickSnip files an image or a video into a space or a folder as it creates it. From CLI 0.4.0 a file lands there too, filed by a follow-up call once its bytes are in, and the result says whether that worked:
{"placement":{"requestedSpace":"spc_1","requestedFolder":"fld_1","filed":true}}
filed: false with a reason means the file is in QuickSnip but not where you asked — the server is too old to file an uploaded file (file_placement_unsupported), or it refused the target (folder_not_in_space, wrong_org, space_not_found, folder_not_found). Either way the upload still succeeds and still exits 0, so move it from the dashboard. In a directory upload the affected items carry "unfiled": true with the same unfiledReason, and the summary counts them.
Note
A directory upload now includes every top-level file, not only the images and videos. A refusal that applies only to files, such as a free account or a server without the route, fails those items and leaves the rest of the batch alone, so the images and videos in the same directory still upload and the batch exits 6.
Read a recording back (the read half)
Append .json to any QuickSnip share URL — not just ones the CLI printed — and you get the agent manifest: no login, no API key, just a GET. The share page has the same URL one click away too: open the share-link menu and Copy agent link sits right next to Copy link.
curl https://quicksnip.com/s/CdU5nkL1CogC3oh.json
{
"schema": 2,
"id": "CdU5nkL1CogC3oh",
"type": "video",
"title": "QuickSnip Recording - 13 August 2026",
"createdAt": "2026-08-13T10:14:44.000Z",
"durationMs": 18885.5,
"width": 2120,
"height": 1440,
"urls": {
"share": "https://quicksnip.com/s/CdU5nkL1CogC3oh",
"agent": "https://quicksnip.com/s/CdU5nkL1CogC3oh.json",
"video": "https://quicksnip.com/api/playlist?videoId=CdU5nkL1CogC3oh",
"thumbnail": "https://quicksnip.com/api/thumbnail?videoId=CdU5nkL1CogC3oh"
},
"readiness": {
"state": "partial",
"detail": {
"media": "ready",
"transcript": "unavailable",
"summary": "unavailable",
"chapters": "unavailable"
},
"reasons": {
"transcript": "no_audio",
"summary": "no_audio",
"chapters": "no_audio"
}
},
"frames": [
{ "atMs": 1000, "label": "Opening", "reason": "fixed", "timestamp": "0:01", "url": "https://quicksnip.com/a/CdU5nkL1CogC3oh/frame/1" },
{ "atMs": 4000, "label": "25%", "reason": "fixed", "timestamp": "0:04", "url": "https://quicksnip.com/a/CdU5nkL1CogC3oh/frame/4" },
{ "atMs": 9000, "label": "50%", "reason": "fixed", "timestamp": "0:09", "url": "https://quicksnip.com/a/CdU5nkL1CogC3oh/frame/9" },
{ "atMs": 14000, "label": "75%", "reason": "fixed", "timestamp": "0:14", "url": "https://quicksnip.com/a/CdU5nkL1CogC3oh/frame/14" }
],
"guide": "https://quicksnip.com/docs/agents",
"instructions": [
"You can see pixels, not just read text — fetch frames[].url for the moment you need.",
"If you can't process images, transcript/summary cover the same content."
]
}
That's a real, live recording — fetch it yourself and you'll get this back right now. schema here versions the manifest independently of the CLI's own --json output above (a video upload prints schema: 1; this manifest is schema: 2), two different documents that happen to share a field name and version on their own clocks.
Beyond readiness, worth knowing: durationMs/width/height; frames — a handful of stills already picked at meaningful timestamps, each independently fetchable; guide — this page's own URL, always; and instructions — a short array telling the reader what to fetch next. Fetch frames[].url if you can see images; read transcript.text if you can't.
Readiness, and why "no audio" is normal
readiness.state | Meaning |
|---|---|
preparing | Still processing. Retry after retryAfterSeconds, or use --wait next time. |
ready | Media, transcript, summary, and chapters are all available. |
partial | The media plays, but one or more of transcript/summary/chapters permanently isn't — check readiness.reasons. |
failed | Processing failed. It will never become readable — stop polling. |
When a dimension is unavailable, readiness.reasons says why:
reasons value | Means |
|---|---|
no_audio | No speech was detected. |
disabled | The owner turned captions/transcript off for this recording. |
plan_gated | The owner's plan doesn't include AI summaries or chapters. |
transcription_failed | Transcription itself errored. |
generation_failed | Transcript succeeded, but summary/chapter generation errored. |
Note
Most screen recordings have no narration. no_audio means transcription ran and found nothing to transcribe — it is not a bug, and not a missing feature. Read frames or the video itself instead of waiting for a transcript that will never appear.
The live example above is partial for exactly that reason — pull just the readiness out of it:
curl https://quicksnip.com/s/CdU5nkL1CogC3oh.json | jq '.readiness'
{
"state": "partial",
"detail": { "media": "ready", "transcript": "unavailable", "summary": "unavailable", "chapters": "unavailable" },
"reasons": { "transcript": "no_audio", "summary": "no_audio", "chapters": "no_audio" }
}
Images vs. videos
An image's manifest looks like this — note urls.direct and its own, shorter instructions:
{
"schema": 2,
"id": "a1B2c3D4e5F6g7H",
"type": "image",
"title": "Product screenshot",
"createdAt": "2026-08-13T13:07:56.000Z",
"width": 800,
"height": 600,
"urls": {
"share": "https://quicksnip.com/s/a1B2c3D4e5F6g7H",
"agent": "https://quicksnip.com/s/a1B2c3D4e5F6g7H.json",
"direct": "https://quicksnip.com/s/a1B2c3D4e5F6g7H+"
},
"readiness": { "state": "ready" },
"instructions": [
"This is a still image, not a video.",
"Fetch urls.direct for the raw bytes."
]
}
urls.direct is the share URL with a + on the end (https://quicksnip.com/s/<id>+) — images only; that same suffix 404s on a video. It serves the raw file directly, right content-type and no HTML wrapper, which is what you want for embedding.
The share page offers the same thing without the CLI: Copy image link copies that URL, and Copy as Markdown copies  — same bytes, .png instead of +, empty alt text. Paste that straight into a GitHub issue or PR comment and the image renders inline.
Video doesn't get any of this, on either side. GitHub (like Loom) won't render an external video player inside a comment, so there's no markdown trick to reach for — a pasted video share link is just a link, and a human has to click it to watch. That split is the actual pitch, not a limitation to work around: send the share link for a human to click, and let the agent fetch the .json sitting right beside it.
Files
Anything that isn't a video or an image — a PDF, a log file, a markdown doc, a zip — uploads and reads back as type: "file". Its manifest looks like this for a plain-text or markdown file under the preview size cap:
{
"schema": 2,
"id": "f7Gp2QxT4mR9vLc",
"type": "file",
"title": "release-notes.md",
"createdAt": "2026-08-20T09:00:00.000Z",
"urls": {
"share": "https://quicksnip.com/s/f7Gp2QxT4mR9vLc",
"agent": "https://quicksnip.com/s/f7Gp2QxT4mR9vLc.json"
},
"file": {
"name": "release-notes.md",
"bytes": 2048,
"text": "# 2.5.1\n\n- pause actually stops the recording\n..."
},
"readiness": { "state": "ready" },
"instructions": [
"This is a text file. Its contents are included inline in file.text."
]
}
There's no urls.direct for files — that suffix is images-only. urls.video/urls.thumbnail never appear either, and neither does guide: a file has no frames or transcript to chase, so there's nothing further to point at.
file.text is only present when the file classified as previewable text or markdown under a 256 KiB cap — the same bytes the share page itself renders, decoded but never markdown-rendered here. A binary file (an image format QuickSnip doesn't otherwise recognize, a zip, a compiled binary) gets file.bytes with no file.text, plus an instructions entry pointing you at urls.share to download it instead. A file too large to preview inline, or one whose size couldn't be verified, gets the same download-it-yourself instruction without ever claiming it's binary — the exact wording differs so you can tell which case you're in.
A 0-byte file is a legitimate, deliberately-supported upload, not an error: it gets "file": { "name": "...", "bytes": 0, "text": "" } and "instructions": ["This file is empty."] — never the binary instruction.
Unlike videos and images, a file manifest never returns a preparing readiness for "still uploading" — request it before the upload finishes verifying and you get a plain 404, identical to a nonexistent recording; retry the same way --wait already tolerates a 404. A preparing state can still appear, rarely, if a transient storage read failed for an otherwise-stored file — retry after retryAfterSeconds in that case, same as any other preparing response.
readiness.state | Meaning |
|---|---|
ready | The file is confirmed available at urls.share. file.bytes is present unless the size itself couldn't be verified (see instructions for that case); file.text is present only for previewable text/markdown, or as "" for a 0-byte file — every other ready file has neither. |
preparing | A transient read failure — retry after retryAfterSeconds. Never returned for an upload still in progress; that 404s instead. |
Note
There is no failed readiness for files either. A file that fails verification is hidden behind the same 404 as one that never existed, until someone on our side clears it — polling past a 404 for a file will never resolve on its own the way a preparing video eventually does.
Who can read a manifest
A .json fetch needs all three of: the recording's link set to Public link: Anyone with the link can watch, no password on it, and the owning team's Access email domain (Settings → Team) left empty. If any of those isn't true, the fetch 404s — plainly, with no JSON and no sign-in page, for anyone: an anonymous curl, a signed-in teammate, even the recording's own owner authenticated with an API key. There is no authenticated path around this gate; a manifest is public or it doesn't exist.
Warning
The email-domain restriction is a team-wide setting, not a per-recording one. If a team has an access domain configured, none of that team's recordings are agent-readable from outside it — the fix is in Settings → Team, not anything on the CLI side.
The full contract
quicksnip guide --json
prints every command, flag, exit code, and JSON shape the installed CLI actually supports — including ones not covered here. It's what quicksnip agents install tells an agent to run for exactly this reason: when this page and the CLI disagree, trust the command.
Can't Find What You Need?
Reach out to our support team and we'll get back to you within 24 hours.