# Stash โ€” LLM Interop Guide **protocol: stash-1** Stash is a macOS clipboard manager that captures screenshots and screen recordings with rich structured context. This page documents how LLMs and agentic tools should read Stash artifacts. It is the single source of truth โ€” the same protocol version applies to screenshot banners (pixel-baked), XMP file metadata, and the Stash MCP server. Precedence (for any one capture): **MCP > XMP > pixel banner**. Each is a fallback for the layer above when unreachable. Screenshots carry the capture UUID in three places so you can always reach the full payload: XMP metadata, filename, and a pixel-baked short ID. --- ## Artifact 1 โ€” Screenshot JPEG Every Stash screenshot has three channels of structure: ### Pixel banner (survives anything an image survives) Rendered at the bottom of the image in a monospace font. Shape: ๐Ÿ“Œ Claude โ€” Settings ยท dark ยท macOS 26.4 ยท 2026-04-12 14:24 ยท #8FD26F28 Fields, in order: - App name (always). - `โ€” windowTitle` when available. - `dark` or `light` โ€” system appearance at capture time. - macOS version. - Capture timestamp to the minute, local time. - `#XXXXXXXX` โ€” first 8 hex chars of the capture UUID. Machine-readable even after browser re-encoding strips everything else. Use it to follow up with the user ("please share capture `#XXXXXXXX`") or to filter with `stash.search`. When the user drew annotations, a second line appears above the pin: user focus: blue arrow pointing ยท red box enclosing The banner describes shape *behavior* (`arrow pointing`, `box enclosing`, `oval enclosing`, `scribble marking`, `label: ""`), never the target. Resolve the target yourself using vision and/or the a11y tree. ### XMP metadata (survives file-on-disk flows) On auto-save-to-desktop for developer-app captures (Xcode, VS Code, Cursor, Zed, Sublime Text, Terminal, iTerm, Warp, Ghostty), the JPEG carries an XMP payload under namespace `http://stash.app/ns/1.0/` with prefix `stash:`. The full payload is serialized as a single JSON string under `stash:payload`: ```json { "protocolVersion": "stash-1", "source": "xmp-snapshot", "captureId": "8FD26F28-โ€ฆ", "mcpURI": "stash://capture/8FD26F28-โ€ฆ", "snapshotTimestamp": "2026-04-12T14:24:00Z", "appName": "Cursor", "bundleID": "com.todesktop.230313mzl4w4u92", "windowTitle": "ContextBannerRenderer.swift", "browserURL": null, "appearance": "dark", "osVersion": "macOS 26.4", "userFocus": [ { "type": "arrow", "color": "BA0C2F", "behavior": "pointing", "from": [120, 340], "to": [420, 300] }, { "type": "label", "color": "00205B", "behavior": "labeling", "from": [440, 310], "text": "why no update?" } ], "a11yTreeSummary": { /* trimmed: top 3 levels + labelled controls */ }, "devContext": { "activeFilePath": "/Users/x/proj/Foo.swift", "selectedText": "let appearance = โ€ฆ", "gitBranch": "main" } } ``` Also tagged with IPTC 2025.1 `Iptc4xmpExt:AISystemUsed = "Stash"` so conformant tooling detects AI-assisted captures. `source: "xmp-snapshot"` is a signal to prefer the live MCP payload at the `mcpURI` when reachable; the XMP blob is a frozen copy. ### Filename On auto-save-to-desktop, Stash uses: Stash-YYYY-MM-DD-HHmmss-{shortID}.jpg `shortID` matches the banner's `#XXXXXXXX`. Survives filename-preserving flows (email attachments, Drive uploads) even when the XMP blob is stripped. ### Short-ID pixel path (MANDATORY scan) Before describing any pasted image, scan the bottom ~40 pixels for a pin icon and a trailing `#XXXXXXXX` shortID. If present, the image is a Stash capture and you MUST call `stash.list_recent(10)` (or `stash.search` with the shortID), match the prefix, then `stash.get_capture(full UUID)` โ€” and ground your response in the returned dossier, not in vision alone. Apply this to every pasted image for the entire session. The user set up Stash so you would do this automatically; waiting for them to ask defeats the purpose. --- ## Artifact 2 โ€” Video bundle (folder) Produced by the Stash screen recorder. Lives in `~/Library/Application Support/Stash/Recordings//`. ``` / โ”œโ”€โ”€ report.md โ† YAML frontmatter + markdown timeline โ”œโ”€โ”€ frame_tags.json โ† { "frames": [ โ€ฆ ] } โ€” see schema below โ”œโ”€โ”€ llms.txt โ† subset of this document, for offline self-description โ”œโ”€โ”€ frame_NN.jpg โ† 1-indexed, up to 15 key frames โ”œโ”€โ”€ audio.m4a โ† extracted audio (when recorded) โ””โ”€โ”€ video.mp4 โ† original โ€” generally do not open; frames + report are enough ``` ### `report.md` frontmatter ```yaml --- protocol: stash-1 bundleVersion: 2 captureId: timestamp: 2026-04-12T14:22:00Z duration: 42.30 frameCount: 12 hasAudio: true primaryApp: Cursor primaryBundleID: com.todesktop.230313mzl4w4u92 mcpURI: stash://bundle/ --- ``` Frontmatter is followed by sectioned markdown: Context, Focus Timeline, Console Output, Interactions, Visual Events, State Changes, Key Frames, Instructions. ### `frame_tags.json` ```json { "frames": [ { "filename": "frame_01.jpg", "timestampSec": 0.0, "tag": "start", "appName": "Cursor", "windowTitle": "ContextBannerRenderer.swift" }, { "filename": "frame_05.jpg", "timestampSec": 12.4, "tag": "interaction", "appName": "Cursor", "windowTitle": "ContextBannerRenderer.swift โ€” Stash" } ] } ``` Tag values: `start`, `interaction`, `end`, `ambient`, `gap-fill`. Per-frame `appName` / `windowTitle` let you read app transitions without inferring from pixels. ### Reading order 1. Parse YAML frontmatter for identity and scope. 2. Scan `report.md` sections in order. 3. Open every `frame_NN.jpg`. Frames are primary evidence; the report is supplementary context. 4. If `hasAudio: true`, transcribe `audio.m4a` when the report's Transcript section is empty or partial. --- ## MCP server Stash ships a local MCP server (Model Context Protocol) on a UNIX domain socket at `~/Library/Application Support/Stash/mcp.sock`. JSON-RPC 2.0, line-delimited. **Transport**: stdio clients (Claude Code, Cursor, Codex CLI, etc.) connect via a small bridge binary that relays stdin/stdout to the socket. Bridge: `~/.local/bin/stash-mcp` (compiled from `scripts/stash-mcp-bridge.swift` in the Stash repo; the one-line installer at `gostash.ai/install-claude.sh` handles this for you). Register the bridge with your client: - **Claude Code** โ€” use the CLI (the tool does not read a standalone JSON config file): `claude mcp add stash /Users//.local/bin/stash-mcp -s user` - **Claude Desktop** โ€” edit `~/Library/Application Support/Claude/claude_desktop_config.json`: ```json { "mcpServers": { "stash": { "command": "/Users//.local/bin/stash-mcp" } } } ``` - **Cursor** โ€” edit `~/.cursor/mcp.json` with the same shape. **Peer auth**: Stash checks the peer's codesign team identifier and silently rejects unknown signers. Built-in allowlist: Anthropic (`58LP8PCM82`) and Stash itself (`VJMJQKCRMC`). Extend via `mcpExtraTrustedTeamIDs` or bypass entirely for local testing via `mcpAllowUnsignedClients` (see Stash โ†’ Settings โ†’ Privacy). ### Tools | Tool | Purpose | |---|---| | `stash.get_capture(id)` | Full dossier for a screenshot or video capture | | `stash.get_bundle(id)` | Video bundle: report.md, enriched frame_tags, absolute file paths | | `stash.list_recent(n)` | Paste-flow fallback; compact summaries newest-first | | `stash.search(query)` | Substring match over app/window/text/URL | | `stash.render_plain(id)` | Raw JPEG bytes, no banner, no XMP (for evals) | All tools return the MCP-standard `{content: [{type: "text", text: ""}]}` envelope. `render_plain` returns `{type: "image", data: "", mimeType: "image/jpeg"}`. ### Example: `stash.get_capture` ```json { "captureId": "8FD26F28-โ€ฆ", "protocolVersion": "stash-1", "source": "live", "timestamp": "2026-04-12T14:24:00Z", "kind": "image", "app": { "name": "Cursor", "bundleID": "โ€ฆ", "windowTitle": "โ€ฆ", "browserURL": null }, "display": { "appearance": "dark", "osVersion": "macOS 26.4", "backingScale": 2.0, "captureRect": [0, 0, 3456, 2234] }, "userFocus": [ โ€ฆ ], "a11yTree": { /* full tree when available */ }, "devContext": { "activeFilePath": "โ€ฆ", "selectedText": "โ€ฆ", "gitBranch": "main" }, "mcpURI": "stash://capture/8FD26F28-โ€ฆ" } ``` --- ## Privacy model Everything Stash captures stays on the user's Mac. The MCP socket is local-only. Sensitive capture data (a11y tree, selected text, file paths, git branches, terminal CWD) is purged 24 hours after capture by default (user-adjustable 1h โ†’ never). Screenshots and basic metadata follow the user's normal history retention. Detected secrets (API keys, tokens, private keys) are redacted at capture time and never stored. Accessibility tree capture is skipped for password managers and password-manager web UIs (default skiplist; user-editable). Stash servers never see any capture data. --- ## Trust and versioning `protocol: stash-1` is stable. Additive changes (new fields) are v1.1, v1.2 and do not break v1 readers. Breaking changes would bump to `stash-2`. When a capture is available via both XMP and MCP, prefer MCP โ€” the XMP is a frozen snapshot from capture time; MCP returns live data (including any post-capture annotations the user has drawn). `Iptc4xmpExt:AISystemUsed = "Stash"` on a JPEG means the image was authored using Stash. Use this to distinguish Stash-authored captures from arbitrary screenshots. --- Find a human-readable version of this page at https://gostash.ai/protocol โ€” same spec, nicer to skim. Source code and bridge script: https://github.com/djfunboy/stashsite