Using QuickSnip from an agent or script
10 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.
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:
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"}}
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 (that one is schema: 1; this is schema: 2) — two different documents that happen to share a name.
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.
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.