Team-owned, self-hosted meeting intelligence. Captured locally, surfaced through Claude.
Clipwise records your meetings, transcribes them locally, and builds a persistent database of the moments that matter — decisions, commitments, objections, insights — that you can query across every meeting you've ever had. It's designed to live inside Claude via MCP, not to be another destination platform you have to log into.
Website: clipwiseapp.com · Join the waitlist
⚠️ Early development. The landing page is live; the recorder, server, and MCP are actively being built. Star the repo or join the waitlist to hear when the first release ships.
Meeting recorders today (Fathom, Gong, Grain, Otter) are destination platforms. You record → they store your data on their servers → you log into their UI to review summaries. The recording is a means to an end, and that end is their product.
Clipwise inverts that:
- Your recordings and transcripts never leave your infrastructure. No third-party capture SDK, no vendor cloud, no "AI notetaker" bot joining your calls.
- The interface is Claude, not a web app. Ask questions in natural language across every meeting you've ever had. "What did the ACME team commit to in Q2?" "Show me every objection we've heard about pricing." "Pull the clip where Sarah described the new architecture."
- A persistent extracted-moments database grows over time. Individual meeting summaries are commodity; the compounding value is a searchable, structured memory of every important thing anyone has said, across months and years.
┌────────────────────────────┐ ┌──────────────────────────────┐
│ Mac Desktop Recorder │ │ Self-Hosted Server │
│ ──────────────────────── │ │ ────────────────────────── │
│ • Shell: Electron │ uploads │ • Recording + transcript │
│ • Core Audio Taps │────────▶│ storage │
│ (native macOS capture) │ │ • Moment extraction │
│ • Local Parakeet │ │ • Moment DB (Postgres + │
│ transcription │ │ vector index) │
│ │ │ • MCP server interface │
└────────────────────────────┘ └──────────────┬───────────────┘
│ MCP
▼
┌──────────────────────────────┐
│ Claude (Desktop / Code / │
│ API) via Clipwise MCP │
│ ────────────────────────── │
│ Query across every meeting │
│ in natural language │
└──────────────────────────────┘
- Mac desktop recorder — an Electron menu bar app that spawns and manages the capture subprocesses. Capture is Clipwise's own Swift binaries using Core Audio Taps (macOS 14.2+) for system audio and Core Audio directly for the mic, recording both natively — no Recall.ai, no meeting-bot SDK, no third-party dependency in the capture path. Stopping a capture starts the rest on its own: the recorder spawns the capture pipeline, which on the Mac transcribes (with Parakeet through FluidAudio; whisper.cpp stays selectable), classifies the capture and separates voices. When it exits, the recorder uploads the capture to the server, which ingests it and extracts moments, without anyone driving it. From the menu bar, "Save transcript" writes the most recent meeting's transcript, with speaker names applied, to a text file; a voice that was never named is labelled "Unnamed voice N", and a line no voice clearly owns "Unattributed". "Recent meetings…" lists the past week's captures — title, who was on it, when, how long — to save any one's transcript or fix its speaker names; it is for picking a meeting, not reading one.
- Self-hosted server — A long-running Node/TypeScript container that stores raw recordings, transcripts, and the moments database, and serves it through a REST API and an MCP endpoint (below). It's a single Docker artifact from day one: the hosted deployment (Fly.io / Railway / Render) and any user-run self-hosted deployment run the same image. Persistence is standard Postgres (with
pgvector); Neon is the currently-used managed provider, but the connection is plainpgso any Postgres works. It has a capture endpoint,POST /captures, that takes one finished capture: the transcript's segments, the capture's provenance, and the identity answer if there is one. It needs a capture token, which is separate from the MCP token (its audience is<server>/capture, its scopeclipwise:capture) and is issued only to the recorder's OAuth client; an MCP token on it is a 403. The account and the owner come from the token, and the capture is stored as a private recording. It is idempotent on the capture id: the same capture sent again is a 200 withcreated:false, different segments under the same id, or another member's capture id, are a 409, and an identity answer that arrives on a repeat is applied once. Apart from the auth routes (signing in, refreshing its token and signing out, under/api/auth/oauth2/, and the/.well-known/oauth-protected-resourcelookup), the recorder uses only the capture routes under/captures, never the rest of the REST API. It runs the database-free capture pipeline (capture-cli) on the Mac — from inside the app bundle in a built app, from the checkout when run from source — and uploads each capture toPOST /captures; ingest and extraction run on the server. - Clipwise MCP — The server's own MCP endpoint, at
/mcp, over Streamable HTTP with OAuth sign-in. Claude connects to it directly; there is no separate bridge to install or run. Two tools today:search_moments, which searches the extracted-moments database by text, by meaning, by moment kind, by recording, or by who was on the call; andget_transcript, which returns one recording's transcript with per-segment timestamps and speaker labels.
| Clipwise | Fathom / Gong / Grain | |
|---|---|---|
| Where data lives | Your infrastructure | Vendor cloud |
| Capture path | Native Core Audio Taps | Bot joins the call, or SDK |
| Transcription | Local Parakeet, on-device | Vendor cloud |
| Interface | Claude via MCP | Vendor web UI |
| Cross-meeting queries | Persistent moment DB, ask Claude | Per-meeting summaries |
| License | Apache 2.0, open source | Proprietary, closed |
| Credentials / recordings | Never leave your infra | Uploaded to vendor |
If you want your meeting intelligence to live on infrastructure you control rather than a vendor's cloud, Clipwise is built for you.
Tracked in Linear, not here. What is built, what is being built and why each call was made lives on the issues; a list in this file is a second copy that goes stale the moment the first one moves.
Clipwise needs an Apple silicon Mac with macOS 14.2 or later. It doesn't open on an Intel Mac.
Open the Clipwise .dmg from the link you were sent, drag Clipwise to Applications, and open it from there.
Covers the Mac recorder only; the server, which serves MCP itself, has its own setup.
Prerequisites
- macOS 14.2+ (Core Audio Taps floor)
- Swift and Node toolchains
mesonandninja—brew install meson ninja. Build tools only, forrecorder/aec(the echo canceller, below); nothing is installed on a user's machine.ffmpegonPATH—brew install ffmpeg, for running from a checkout only. There it is not optional, but not for capture:transcribe.pyuses it to downsample both tracks to the 16 kHz the transcription engines read. The built app carries its own LGPL-only ffmpeg (recorder/ffmpeg/build.sh) and never reads one fromPATH. The mic is captured by Clipwise's ownmiccapbinary, which replaced the ffmpeg mic path.
Build the binaries. All of these are gitignored build outputs, so a fresh clone has none of them.
(cd recorder && swiftc -O audiodevs.swift -o audiodevs)
(cd recorder/systemtap && swift build -c release)
(cd recorder/miccap && swift build -c release)
recorder/aec/build.shInstall the app shell.
(cd recorder/app && npm install && npx install-electron)npx install-electron is not optional. Electron 43's package manifest has no
scripts key at all, so nothing fetches the runtime binary during npm install
— the install exits clean and reports no vulnerabilities while leaving an app
that cannot launch. Verified from the installed package on disk 2026-08-09, with
ignore-scripts confirmed false.
Package it as an app. recorder/app/build-app.sh does everything above and
produces recorder/app/dist/Clipwise.app — a menu bar app you can double-click
and add to Login Items in System Settings. It signs with the Developer ID
identity; override with CLIPWISE_SIGN_IDENTITY if you have your own.
(cd recorder/app && ./build-app.sh)It is signed with the hardened runtime but not notarized, and there is no disk
image: notarize.sh does both, after this. The app registers itself for launch
at login the first time it runs from Applications with setup finished; the
tray's "Open at login" turns that off.
The bundle carries the capture pipeline whole: its tools (systemtap, miccap,
audiodevs, micwatch, diarize, parakeet, aec and ffmpeg), their models, its own
Python, and the pipeline compiled to JavaScript. It reads nothing from the
checkout or PATH. It uploads each capture to the server named in
capture-server.json, which makes the moments.
The developer keys (the database URL and the API keys) are not in the checkout:
they are in ~/.config/clipwise/dev.env, which only server/scripts/scratch-db.sh
and the schema push (server/drizzle.config.ts) load, by name. Nothing the app
runs reads it.
A packaged run has no terminal to print to, so everything it would have written
to stderr goes to ~/Library/Application Support/clipwise/recorder.log. That is
where notification delivery, child spawn failures and pipeline errors are.
Capture output. Each capture writes to
~/Library/Application Support/clipwise/recordings/, sharing one timestamp
stem: the two audio tracks, a log per child, and manifest-<stem>.json.
The manifest is written at capture start, before any child spawns, and is what
identifies a recording. Its recording_id is a generated UUID — the stem stays
the human-readable handle, but it is not an identifier: timestamps collide and
depend on a correct clock. Consumers key on recording_id (it is what the
normalized transcript contract carries as source_recording_id).
It also records the device and sample rate per track, which nothing else
preserves — a headset can put the mic at 24 kHz against a 48 kHz tap, and the
children only mention their rates in stderr logs. Both rates come from one
CoreAudio query at start rather than from the children, which report their
formats milliseconds later; sample_rate_source on each track names that
provenance, so a manifest recovered from a capture's own artifacts is
distinguishable from one written live.
Transcription — needed by anyone who records, not only by anyone calling
transcribe.py by hand: stopping a capture spawns the pipeline, and its first
step is transcription.
-
Default engine: Parakeet TDT 0.6B v2 through FluidAudio. English only, and Apple Silicon only — on an Intel Mac, set
CLIPWISE_TRANSCRIBER=whisper. Build the binary and fetch the model:(cd recorder/parakeet && swift build -c release && ./fetch-models.sh)
The fetch is 443 MB, checksum-verified, and gitignored;
recorder/app/build-app.shruns both steps. Nothing touches the network at run time.build-app.shcopies the models into the app'sResources/models, and the built app reads them from there. Run from a checkout instead, the pipeline reads the checkout's copy, inrecorder/parakeet/modelsandrecorder/diarize/models. The first transcription on a machine is slower while Core ML compiles the model (about 12 s extra on the evaluation harness); later ones are not. Lines are built from word timings, breaking on pauses of 0.6 s or more, and end at the last word's end time. -
Whisper, selectable with
CLIPWISE_TRANSCRIBER=whisper:brew install whisper-cppfor thewhisper-clibinary, and the GGML model —transcribe.pyprints the download command in its error message. Whisper output keeps its Silero VAD gating and its measured segment ends. -
Echo removal: when the capture's output device was the built-in speakers, the mic also records the call, so other people's speech would be stored as the user's. After downsampling and before transcription,
recorder/aec(WebRTC AEC3, echo cancellation only, the tap as the reference) writesmic-<stem>.16k.aec.wavand both engines transcribe that. The rawmic-<stem>.16k.wavis kept. The rule is one manifest field: the system track'sdevice_uidisBuiltInSpeakerDevice. Any other output device, a missing manifest, orCLIPWISE_AEC=offleaves the mic untouched. The transcript'saecblock records whether it ran and why or why not, and ingest stores it inrecordings.metadata.aec. Build it withrecorder/aec/build.sh(build-app.shruns it); if a capture needs it and it isn't built, transcription stops rather than transcribe the echo. The device is the one the manifest recorded at capture start, so an output switch mid-call isn't seen. -
A selected engine that isn't installed is an error, not a fallback to the other one. The capture's audio is kept and re-transcribes once the engine is present. Each transcript records which engine wrote it in its
enginekey, and ingest stores that as the transcript'sprovider.
The commands that built, notarized and installed the app from a fresh worktree on 2026-10-25, run from the repo root.
Prerequisites. The app shell's packages and Electron runtime, and the server's packages:
build-app.sh compiles the capture pipeline with the server's own tsc
(server/node_modules/.bin/tsc), and fails without them.
(cd recorder/app && npm install && npx install-electron)
(cd server && npm ci)Build and sign. Produces recorder/app/dist/Clipwise.app, and ends with built … naming the
commit.
(cd recorder/app && ./build-app.sh)Notarize. notarize.sh needs CLIPWISE_NOTARY_PROFILE, the notarytool keychain profile name,
exported from ~/.zprofile; the command below runs under a login shell so it picks that up. It
notarizes and staples the app, then packs, signs, notarizes and staples
recorder/app/dist/Clipwise.dmg. Both submissions print status=Accepted, and Gatekeeper prints
source=Notarized Developer ID for each.
zsh -l -c 'cd recorder/app && ./notarize.sh'Install from the disk image. Quit the running app and confirm it has quit (pgrep prints
nothing), move the installed copy aside, copy the app out of the disk image, then launch it.
osascript -e 'quit app "Clipwise"'
pgrep -fl Clipwise
mv /Applications/Clipwise.app <backup folder>/Clipwise.app.prev
hdiutil attach -nobrowse -readonly -mountpoint <mount folder> recorder/app/dist/Clipwise.dmg
ditto <mount folder>/Clipwise.app /Applications/Clipwise.app
hdiutil detach <mount folder>
xcrun stapler validate /Applications/Clipwise.app
spctl --assess --type execute --verbose=2 /Applications/Clipwise.app
open /Applications/Clipwise.appThe server connects as its own login, which can read and write rows in every table in schema
public and do nothing else: no creating, altering, dropping or truncating tables, and no creating
logins or databases. Everything that changes the schema keeps using the owner.
-
As the owner, create the login with SQL (on Neon, in the SQL editor, not the console's role page, which makes roles administrators):
CREATE ROLE clipwise_server LOGIN PASSWORD '<a long generated password>'; -
With
DATABASE_URLset to the owner, runnpm run db:grant-server-login -- clipwise_serverinserver/(-- clipwise_server --dryprints the statements without running them). It grantsSELECT, INSERT, UPDATE, DELETEon every table inpublic,USAGEonpublic, and the same row rights on tables the owner creates later. It refuses a login that doesn't exist or holds more than a login should, never creates one, never touches a password, and can be run again. -
Point the server's
DATABASE_URL(on Fly,fly secrets set) at the new login.
db:push, db:sync, db:provision, db:checks, auth:migrate and the auth CLI keep using
the owner.
From server/, on a clean main that has been pushed (git status --porcelain prints nothing):
fly deploy --label org.opencontainers.image.revision=$(git rev-parse HEAD)fly.toml supplies the app, the Dockerfile and its prod target. The label is passed by hand
because the Dockerfile doesn't set it yet; without it, nothing on Fly says which commit an image
was built from.
After the deploy:
fly releases: the new version is at the top,complete.fly image show:LABELSreadsorg.opencontainers.image.revision=<the full hash of HEAD>.curl https://clipwise-looprite.fly.dev/liveanswers{"status":"ok"}(the Fly health check; it never touches the database).curl https://clipwise-looprite.fly.dev/healthanswers{"status":"ok","db":"ok"}.
To roll back, find the previous release's image with fly releases --image and deploy it as it
is, without building:
fly deploy --image registry.fly.io/clipwise-looprite:<that release's deployment tag>Then run the same checks; fly image show reads the old image's own revision label, or none if
it was deployed without one. A rollback swaps the image only: it does not undo a schema push to
the database.