The Stash protocol.
How LLMs and agentic tools read Stash captures. This page is the human-readable companion to
/llms.txt — the same spec, same precedence rules, just nicer to skim.
stash-1.
Three channels, one capture
Every Stash screenshot carries structure in three places so a capture can always be resolved — even after the user pastes it into a web chat and every byte of metadata is stripped.
| Channel | Survives | What it carries |
|---|---|---|
| Pixel banner | Anything an image survives | App, window title, appearance, OS version, timestamp, shortID |
| XMP metadata | File-on-disk flows (Drive, email) | Full structured payload: annotations, a11y tree summary, dev context |
| MCP server | Local RPC (same machine) | Live dossier including full a11y tree and un-summarized fields |
Screenshot banner
Rendered at the bottom of every Stash screenshot in a monospace font:
📌 Claude — Settings · dark · macOS 26.4 · 2026-04-12 14:24 · #8FD26F28
- App name (always).
— windowTitlewhen available.dark/light— system appearance at capture time.- macOS version.
- Capture timestamp to the minute, local time.
#XXXXXXXX— first 8 hex chars of the capture UUID. Call it out in a follow-up prompt or filter withstash.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 — never the target. Resolve the target yourself using vision and/or the a11y tree.
XMP payload
On auto-save-to-desktop for developer apps, the JPEG carries an XMP payload under namespace
http://stash.app/ns/1.0/. Serialized as a single JSON string under stash:payload:
{
"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",
"appearance": "dark",
"osVersion": "macOS 26.4",
"userFocus": [
{ "type": "arrow", "color": "BA0C2F", "behavior": "pointing",
"from": [120, 340], "to": [420, 300] }
],
"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 can detect AI-assisted captures. Filename convention on save-to-desktop:
Stash-YYYY-MM-DD-HHmmss-{shortID}.jpg.
Video bundles
Produced by the Stash screen recorder. A self-describing folder, indexable as one unit:
Recordings/<uuid>/
├── report.md ← YAML frontmatter + markdown timeline
├── frame_tags.json ← { "frames": [ … ] } — per-frame app/window/tag
├── llms.txt ← offline self-description
├── frame_NN.jpg ← 1-indexed, up to 15 key frames
├── audio.m4a ← extracted audio when present
└── video.mp4 ← original; generally skip
The report.md opens with machine-readable YAML frontmatter:
---
protocol: stash-1
bundleVersion: 2
captureId: <UUID>
duration: 42.30
frameCount: 12
hasAudio: true
primaryApp: Cursor
mcpURI: stash://bundle/<UUID>
---
MCP server
Stash ships a local Model Context Protocol server on a UNIX domain socket at
~/Library/Application Support/Stash/mcp.sock — line-delimited JSON-RPC 2.0.
Local-only by design; the socket is not exposed to the network.
Transport
Stdio MCP clients (Claude Code, Cursor, Codex CLI, etc.) connect via a small bridge binary
at ~/.local/bin/stash-mcp that relays stdin/stdout to the socket. The
one-line installer at gostash.ai/claude compiles it, signs it, and
registers it with every Claude client it finds.
Manual registration by client:
- Claude Code — uses its own CLI, not a JSON file:
claude mcp add stash /Users/<you>/.local/bin/stash-mcp -s user - Claude Desktop / Cursor — edit the client's MCP JSON config:
{ "mcpServers": { "stash": { "command": "/Users/<you>/.local/bin/stash-mcp" } } }
Peer auth
Stash reads the peer's codesign team identifier on connect and silently rejects unknown
signers. Built-in allowlist: Anthropic (58LP8PCM82) and Stash itself
(VJMJQKCRMC). Extend via
Stash → Settings → Privacy → Additional trusted team IDs, or toggle
Allow unsigned MCP clients for local experiments.
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: "<json>"}]} envelope.
render_plain returns {type: "image", data: "<base64>", mimeType: "image/jpeg"}.
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 1 hour → 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 their web UIs.
- Stash servers never see any capture data.
Versioning
stash-1 is stable. Additive changes (new fields, new tools) land as v1.1, v1.2
and do not break v1 readers. A breaking change bumps to stash-2. Prefer live
MCP data over a frozen XMP snapshot when both are available.
gostash.ai/llms.txt. Point your agent's config there
for a one-shot sync; it's the same spec as above, optimized for plain-text consumption.