Smart clipboard / watch-later for Windows. A hotkey captures anything, Claude Code turns it into structured notes in your Obsidian vault, and a local dashboard shows it all.
One Tauri binary. No cloud, no account, no telemetry.
The Clipper dashboard has been redesigned from the ground up according to DESIGN.md under the Impeccable design specification (Operate mode). It embodies a calm, editorial, content-forward personal library aesthetic that feels like a quiet reading room:
- Typography & Theme: Built with Spectral (display serif), Inter (body), and JetBrains Mono (code). Includes "Warm Paper" light mode and "Lamplit Room" dark mode with zero raw hex in components.
- 8 Hand-Tuned Type Icons: Distinct inline SVG glyphs for articles, videos, books, repositories, code errors, photos, events, and papers.
- Today Landing Page:
- Time-aware personal greeting and localized date.
- This Week metric cards (saved, read, focus time).
- For You Today ranked cards with 6px top accent bar in the clip type's color and "Why" explanations.
- Stale & Forgotten triage drawer for neglected items.
- Pure SVG 90-day activity sparkline (
720×60, 90 bars, zero chart libraries). - Topics You Lean On horizontal bars & Decay Distribution meters.
- Contextual right rail with user focus and today's digest.
- Library Page:
- Horizontal chip rail filtering by all 8 types and custom tag pills.
- Grid view (320px cards with decay indicators, reading time, and lift hover) and List view (56px dense rows with decay dots).
- Inbox Page:
- 64px triage rows with circular checkboxes, hover actions (Archive, Tag, Obsidian), and keyboard navigation (
j/k,a,t,Enter). - Floating bulk action bar for multi-selection.
- 64px triage rows with circular checkboxes, hover actions (Archive, Tag, Obsidian), and keyboard navigation (
- Detail Slide-Over:
- 520px smooth slide-over from the right with backdrop blur.
- Rendered markdown body with syntax highlighting, pull quotes, and code blocks.
- Quick action toolbar, related clip recommendations, and expandable inline chat.
- Ask Chat Page:
- Left rail with conversation history grouped by date.
- Essay-style assistant responses with collapsible source citation cards.
- Streaming SSE output via
ReadableStream.getReader()with subtle 1px accent cursor. - Sticky composer with auto-grow textarea and streaming abort control.
- Skills Workbench:
- Card list of extraction prompts and triggers with usage frequency stats.
- Split-view slide-over editor: YAML & markdown form on the left, live sanitized preview (DOMPurify) on the right.
- Interactive URL extraction test runner.
- Settings Page:
- 6 dedicated tabs: Soul (persona/voice), Memory (user.md/memory.md), Self (learned patterns), Providers (CLI health & config.toml), Pairing (LAN QR code & TTL countdown), and Devices (paired mobile access with revocation).
- Sticky save button with dirty-state tracking.
- Universal States & Accessibility:
- Full-page and row-level skeleton pulse animations (
1.5s ease-in-out). prefers-reduced-motion: reduceoverrides for instantaneous transitions.- Accessible focus rings (
2px solid var(--accent)), skip links, and ARIA live regions.
- Full-page and row-level skeleton pulse animations (
/api/items/{id}now returns the full markdown body (content+body_md), parsed frontmatter (frontmattermap), and raw frontmatter (raw_frontmatter). The slide-over renders the real notes, not a stub.- 365-day contribution calendar on Today above the sparkline. Color buckets by save count, hover for date + count, click to filter the day.
- Tag color map — deterministic 12-color palette (
rust/amber/forest/sea/berry/plum/indigo/teal/olive/ochre/crimson/slate) with WCAG AA tokens. Applied to Library, Inbox, slideover, For-You. Cmd+Kcommand palette — fuzzy search across pages, recent items, tags; quick-save URL mode.- Inline hover preview on cards (600ms hover → floating panel with title + summary + hero image).
- All 11
--<n>-smokegates green, 22smoke.ps1checks green.
First Impeccable redesign. Headless Tauri daemon, 6 pages, 12 dashboard commits by Pam, 9 a11y fix commits, 0 raw hex outside token definitions.
- Clipboard URL capture — press
Ctrl+Shift+Cin any app. A URL is deduped (SHA-256 of the URL, first 16 hex chars, checked againstarchive.jsonl) and queued. An already-saved URL reportsAlready saved: <host>and is skipped. - Clipboard image capture — press
Ctrl+Shift+X(useWin+Shift+Sfirst). The image is written to~/.clipper/staging/img_<unix_ts>.pngand queued astype: image. - Smart classification via the Claude Code CLI — a background drainer batches queued
items, builds a prompt from
~/.clipper/prompts/extract.md, and runsclaude -p. It gets back one markdown path per item carryingtype,title, a one-sentencesummary, and up to fivetags. - Your Obsidian vault is the source of truth — notes land as
<vault>/Inbox/YYYY-MM-DD_HHMM_<source>_<id>.mdwith YAML frontmatter. Clipper auto-detects the vault; if you have none it creates~/Documents/ClipperVault. - Local dashboard —
http://127.0.0.1:7137/with Today / Inbox / Library / Ask / Skills / Settings panes and full-text search, backed by a SQLite + FTS5 index that mirrors the vault. - Headless desktop process — the tray daemon has no window and no webview. Tray → Show dashboard opens the dashboard in your default browser.
- Phone PWA pair over LAN — tray → Show QR, scan it, and the phone gets a no-build web app that posts links to the desktop.
- Zero Build Step Frontend — Pure Vanilla JS + Alpine.js + Pico.css with standalone mock API fallback.
- Local only — no cloud, no account, no telemetry. A JSONL queue, one SQLite file, and markdown in your vault.
- MIT licensed.
Design Reference: See
DESIGN.mdfor the complete design system specification.
-
Windows 10/11 with the WebView2 runtime. Included on Windows 11; on Windows 10 install it from Microsoft.
-
Rust 1.99+ with MinGW-w64 (GCC 16+) and the
x86_64-pc-windows-gnutarget. Clipper builds with the GNU toolchain, so no Visual Studio C++ build tools are needed:winget install --id Rustlang.Rustup -e winget install --id BrechtSanders.WinLibs.POSIX.UCRT -e # MinGW-w64: gcc, ld, ar rustup toolchain install stable-x86_64-pc-windows-gnu rustup default stable-x86_64-pc-windows-gnu
Then point cargo at the MinGW linker —
~/.cargo/config.toml:[target.x86_64-pc-windows-gnu] linker = "x86_64-w64-mingw32-gcc.exe" ar = "ar.exe"
winget id note:
BrechtSanders.WinLibs.POSIX.UCRTis the package that works. TheniXman.mingw-w64-gccid does not exist in the winget repository. -
Node.js 20+ — only to run the Tauri CLI. Not a runtime dependency of the built app.
-
Claude Code CLI on
PATH(claude --versionworks). This is what performs the extraction; without it the queue will deadletter.
git clone <repo>
cd SmartClipboard
npm install
npx tauri buildProduces target\release\clipper.exe, plus installers in target\release\bundle\:
an .msi and an NSIS -setup.exe.
- Launch
clipper.exe. The tray icon appears; no window opens (by design). - Clipper auto-detects your Obsidian vault, or creates
~/Documents/ClipperVault.~/.clipper/config.tomlis written on this first run and then left alone. - Tray → Show dashboard opens http://127.0.0.1:7137/ in your default browser. There is no app window: the desktop process is headless and the browser is the UI.
| Hotkey | Does |
|---|---|
Ctrl+Shift+C |
queue the URL currently on the clipboard |
Ctrl+Shift+X |
queue the image currently on the clipboard (Win+Shift+S first) |
- Show dashboard — opens the dashboard in your default browser
- Show QR — opens the phone-pairing page in your browser
- Quit
Launching Clipper a second time also opens the dashboard — that is what the single-instance guard does now that there is no window to focus.
Tray → Show QR. Scan the code with your phone; the browser opens the Clipper PWA. Type a URL, press Save. It takes exactly the same path as a desktop capture — same queue, same drainer, same vault. The PWA is plain HTML/JS with no build step, and keeps working if the CDN is unreachable (the form falls back to plain DOM).
The QR encodes http://<lan-ip>:<port>/pair?token=<session-token>. The token is 32
random characters and rotates every 24 hours; POST /save rejects anything without it.
<vault>/
└── Inbox/
└── 2026-10-15_1225_clipboard_<uuid>.md # one structured note per capture
Each note opens with YAML frontmatter:
---
type: article # article | video | repo | book | product | note | other | image
source: example.com
url: https://example.com/post
saved_at: 2026-10-15T12:25:00Z
title: How sockets work
summary: One sentence, at most 25 words.
tags: [networking, tcp]
---Editing a note is safe: the index is rebuilt from the vault, keyed by the id in the filename, and re-indexing never overwrites a status you set in the dashboard.
~/.clipper/config.toml is written on first run and left alone afterwards. Every path
accepts a leading ~.
| Key | Default | Meaning |
|---|---|---|
vault.path |
auto-detected | your Obsidian vault |
vault.inbox_subdir |
Inbox |
subfolder notes are filed into |
provider.claude.command / .args |
claude / ["-p"] |
the extractor to spawn |
provider.claude.stdin |
prompt-file |
prompt-file pipes the prompt into the child's stdin |
provider.image.backend / .prompt_hint |
claude-via-mcp / MiniMax hint |
how images are described |
queue.dir |
~/.clipper/queue |
inbox.jsonl, archive.jsonl, deadletter.jsonl |
queue.staging_dir |
~/.clipper/staging |
batch prompts, generated notes, screenshots |
queue.batch_size |
5 |
items per provider run |
queue.poll_interval_ms |
5000 |
drainer poll interval (it also wakes on file change) |
queue.timeout_s |
120 |
per-batch provider timeout |
server.bind / .port |
127.0.0.1 / 7137 |
dashboard + API |
prompts.extract |
~/.clipper/prompts/extract.md |
the extraction prompt |
skills.dir |
~/.clipper/skills |
where skill markdown files live |
memory.dir |
~/.clipper/memory |
soul.md, user.md, memory.md |
Three markdown files are read at startup and sent ahead of every Claude prompt, so the persona and your context come before the task:
~/.clipper/memory/
├── soul.md # persona and tone
├── user.md # your expertise, style, dislikes
└── memory.md # active work, recent focus, today's plan
They are created with defaults on first run (seeded from src-tauri/seed/). Edit them
and restart Clipper for the change to take effect; a live editor is planned.
Each prompt is assembled as <soul><user><memory>, then a --- fenced copy of
extract.md, then the batch instructions.
Setting CLIPPER_HOME replaces ~/.clipper entirely — that is how
scripts/smoke.ps1 runs a whole session without touching your real state, and it also
makes a portable install possible.
Bound to [server] bind/port (127.0.0.1:7137). GET / serves the dashboard.
| Route | Returns |
|---|---|
GET /healthz |
200 ok |
GET /api/items?limit=50&offset=0&type=&tag=&q= |
{items, total}; q is an FTS5 phrase match, type/tag are exact |
GET /api/items/{id} |
one item, or 404 |
POST /api/items/{id}/status {status} |
{ok:true}; status must be inbox/reading/done/archived/deleted |
POST /api/items/{id}/tags {tags:[…]} |
{ok:true, tags} |
POST /api/clips {url, type?} |
{id, status:"queued"}; appends to inbox.jsonl |
GET /api/today |
{items}, newest 3 with status inbox |
GET /api/stats |
{total, by_type, by_status, stale_30d} |
GET /pair |
the phone PWA page |
GET /pair.png |
QR code for the pairing URL, as image/png |
GET /qr |
the small page shown by the tray's Show QR item |
GET /pwa/app.js, GET /pwa/manifest.json |
PWA assets |
POST /save {url} |
same as /api/clips; requires the pairing token in ?token= or X-Clipper-Token, else 401 |
GET /api/skills |
{skills, total} from ~/.clipper/skills/ |
GET /api/skills/{name} |
one skill plus html (markdown rendered, embedded raw HTML stripped) |
PUT /api/skills/{name} {body_md, source?} |
writes the file and re-indexes it |
DELETE /api/skills/{name} |
removes the file and its row |
POST /api/skills/rescan |
rebuilds the skill index from the directory |
GET /api/memory |
{parts} — each with bytes, mtime, etag |
GET /api/memory/{part} |
{part, content, mtime, etag}; part is soul/user/memory |
PUT /api/memory/{part} {content, if_match?} |
{ok, mtime, etag}; 412 when if_match is stale |
GET /api/soul |
the soul plus settings parsed from its frontmatter |
PUT /api/soul {content, if_match?} |
same rules as /api/memory/soul |
GET /api/events |
Server-Sent Events stream (text/event-stream) |
POST /api/events {kind, payload?} |
records a user_event |
GET /api/self/summary |
{topics, patterns, recent_events, event_count} |
POST /api/self/recompute |
rebuilds topics + patterns from the index |
GET /api/providers |
the configured providers (read-only in this release) |
POST /api/providers/{name}/test |
runs the provider once with a trivial prompt; reports exit code + first stdout line |
GET /api/pairing |
{lan_ip, port, pair_url, qr_path, token_length} — never the token itself |
POST /api/pairing/regenerate |
rotates the session token and emits token_rotated |
POST /api/items/{id}/chat {message} |
SSE: event: chunk frames, then event: done |
GET /api/chat/history?item_id=&limit= |
{turns, total} — session-scoped |
Chat spawns the configured provider once per question with the full prompt template
(soul + user + memory, skill context, the clip and its body, then the question) and
normalises both plain-text and NDJSON stdout into chunk frames. A bounded channel
between the reader thread and the stream is the backpressure: if the client stalls, the
reader blocks instead of buffering the model's output without limit.
Provider stdout is parsed tolerantly. The strict shape (exactly one existing staging path
per input) is tried first; if the count does not match, Clipper rescans for .md paths
under the staging directory and recovers the batch, logging when it does. Only files
under staging are ever accepted, so a provider cannot talk Clipper into moving a file
from somewhere else.
The pairing token is never returned by the JSON API. It exists only inside the QR
image served at /pair.png, so nothing but a scan of that image can read it.
Optimistic concurrency uses If-Match against the part's etag — a SHA-256 content
hash, not the mtime. Filesystems that only tick mtime once per second would let two
edits in the same tick silently clobber each other; a hash cannot. Send the etag you
last read, or omit if_match to force the write.
items.saved_at is Unix seconds as a JSON number — the dashboard multiplies by 1000
before formatting.
Skill names are slugs ([A-Za-z0-9._-], no ..), so a name can never escape the skills
directory. Skill markdown is rendered with pulldown-cmark and the embedded raw-HTML
events are dropped server-side; the client runs DOMPurify as well.
CORS: only http://127.0.0.1:7137, http://localhost:7137 and the app's own webview
origins get CORS headers; everything else is default-deny.
If the port is already in use Clipper logs the address it could not bind and exits non-zero — it never silently picks another port.
One Tauri 2 binary: Rust backend, vanilla HTML/JS frontend, no framework and no bundler.
A capture appends a line to a JSONL queue. A background drainer batches those lines,
writes a prompt file, runs the Claude Code CLI with a timeout, and files each returned
markdown document into the Obsidian vault — which stays the source of truth. A SQLite
index with an FTS5 virtual table mirrors the vault for search, kept in step by a file
watcher. An axum server on 127.0.0.1:7137 serves the dashboard, the REST API, and the
pairing endpoints; the dashboard is embedded in the binary and served from /, so its
relative /api/... fetches stay same-origin.
Failures are loud and bounded: a bad provider exit, a timeout, or unparseable stdout
moves the batch to deadletter.jsonl with a reason instead of retrying forever, and if
the port is taken Clipper reports it and exits rather than silently relocating.
See SPEC.md for the data model and the build slices.
pwsh -File scripts/smoke.ps1 # or: powershell -File scripts\smoke.ps1Builds the release binary if needed, then runs it fully isolated under a temporary
CLIPPER_HOME with a faked provider and asserts the whole pipeline: config + vault
auto-detect, /healthz, the items/today API, /pair, /pair.png, /save returning
401 without a token, then queue → drainer → vault → index for three URLs. It ends with
SMOKE OK (19 checks) and leaves the temp vault behind for inspection.
The release binary also carries flag-driven self-checks, one per slice:
clipper.exe --hag3-smoke # vault, queue, drainer, writer
clipper.exe --hag4-smoke # SQLite, FTS5, every REST route over real HTTP
clipper.exe --hag6-smoke # image capture through the drainer
clipper.exe --hag7-smoke # pairing: /pair, /pair.png, token-gated /save
clipper.exe --hag10-smoke # soul/user/memory seeding + injection into the Claude prompt
clipper.exe --integration-smoke # cross-cutting API contract gates (also tests/integration_api.rs)These live behind flags rather than in
tests/because a Rust test-harness binary linking the Tauri stack cannot be loaded on the windows-gnu toolchain (STATUS_ENTRYPOINT_NOT_FOUND), whileclipper.exeitself loads fine. This way the checks exercise the code that actually ships.
| Symptom | Fix |
|---|---|
claude: command not found |
Install the Claude Code CLI and make sure it is on PATH. |
Clipper: cannot bind 127.0.0.1:7137 … |
Something else owns 7137. Clipper exits rather than quietly moving to another port — kill the other process, or change [server] port. |
no .obsidian folder found |
Expected if you have no vault yet: Clipper creates ~/Documents/ClipperVault. Move it wherever you like and update vault.path. |
warning: .rsrc merge failure: multiple non-default manifests during the build |
Known and harmless — MinGW ld and tauri-winres both supply a manifest. Linking succeeds. |
| A hotkey does nothing | Another app may already own Ctrl+Shift+C / Ctrl+Shift+X. Both are constants at the top of src-tauri/src/lib.rs. |
| Notes never appear in the vault | Check ~/.clipper/queue/deadletter.jsonl — a note there carries the reason (bad-stdout, timeout, exit-<code>). |
| The dashboard shows sample clips that are not yours (e.g. "Tauri 2.0 Architecture…") | That is the dashboard's built-in offline fallback: it could not reach the API, so it rendered mock fixtures. Make sure Clipper is running, and open the dashboard from the tray. |
MIT — see LICENSE.