scrybe 2.0.0

Local-first meeting recording, transcription, and notes for macOS.
scrybe-2.0.0 is not a library.
Visit the last successful build: scrybe-0.1.0

scrybe

Crates.io License: Apache-2.0

Local-first meeting transcription and notes. No bot, no account, no vendor cloud by default.

scrybe is an open-source meeting transcription tool built around one constraint: the meeting artifacts belong on the user's machine as ordinary files. It captures audio locally, transcribes it with either local Whisper or a user-configured OpenAI-compatible provider, generates Markdown notes, and writes everything under ~/scrybe/.

Current release: v1.6.0.

What Works Today

The supported user path today is macOS:

  • scrybe init writes the default local macOS profile on macOS.
  • scrybe init --profile default writes the hermetic synthetic profile used by CI and cross-platform smoke tests.
  • scrybe record creates a session folder with source-separated audio.opus, centered playback.opus, transcript.md, notes.md, and meta.toml.
  • --source synthetic runs the hermetic smoke path used by CI.
  • --source mic records Core Audio's default microphone resolved once at session start; --input-device <uid> or [record].input_device pins an exact macOS Core Audio device UID.
  • scrybe devices lists macOS input-device UIDs and identifies the current default.
  • --source mic+system records microphone plus macOS system audio through ScreenCaptureKit when built with mic-capture,system-capture-mac on macOS 13+. It requires the broader Screen & System Audio Recording permission.
  • --whisper-model <PATH> enables local whisper.cpp transcription when built with whisper-local.
  • --llm openai-compat enables real notes through Ollama, vLLM, OpenAI, Groq, Together, or any compatible /chat/completions endpoint when built with llm-openai-compat.
  • scrybe record TITLE --shell enables the native macOS recording shell: a five-bar 4 Hz waveform cycling between the current macOS appearance foreground and recording red, an optional Scrybe image mark, a compact floating elapsed-time pill, and one shared Stop & save path for the tray, pill, and global hotkey.
  • scrybe list, scrybe show <id>, scrybe doctor, scrybe repair <session>, scrybe notes <session>, and scrybe bench are available in the CLI. A session is addressed by its folder name or an unambiguous session-ID prefix, never by a path: every session resolves beneath the configured storage root, and --root is the only way to point a command at a different one.
  • scrybe bench stt --corpus <MANIFEST> --whisper-model <FILE> --sherpa-model <DIR> compares both local providers on a checksum-validated English paired corpus when built with whisper-local,stt-sherpa and an explicitly provisioned native runtime. Manual acquisition and measurement scope. Whisper remains the default; the historical multilingual corpus is Whisper-only.

Linux, Windows, and Android crates are present in the workspace as adapter surfaces and scaffolds. They are not the polished end-user install path yet. The project keeps those adapters in-tree so the trait contracts, config, tests, and packaging work stay cross-platform from the start.

Install

Install the full macOS application from crates.io:

cargo install scrybe
scrybe doctor

This builds Scrybe locally with microphone capture, ScreenCaptureKit system audio, Whisper, Opus, and OpenAI-compatible notes enabled. It requires Rust 1.95 and Xcode Command Line Tools. Use cargo install scrybe --locked only when reproducing the exact dependency graph qualified for a release or troubleshooting a registry install; releases qualify both forms.

For a faster prebuilt installation:

curl --proto '=https' --tlsv1.2 -LsSf \
  https://github.com/Mathews-Tom/scrybe/releases/latest/download/scrybe-installer.sh | sh
scrybe doctor

The GitHub installer downloads the matching macOS tarball, verifies the release checksum manifest, and installs scrybe on PATH. Both installation paths provide the same production capabilities. On an interactive terminal, scrybe doctor reads the configured capture mode, explains the required macOS permission, and offers the applicable live probe. It does not modify the system or create signing identities without an explicit confirmation. Manual tarball installation, release verification, and source builds are documented in INSTALL.md.

First Local Recording Setup

In the application

Open Scrybe. An installation that cannot record yet opens on guided setup, which walks through four steps and can be left at any point:

  1. Welcome — where recordings are kept, that no meeting bot joins a call, and that the transcription-model download is the only network request setup makes.
  2. Recording — the microphone, and what the Microphone and Screen & System Audio Recording permissions are for. macOS raises its own dialog the first time a recording needs one; if you have already refused a capability, each has a button that opens the System Settings pane where it is granted.
  3. Transcription and notes — the transcription model, shown with its source, upstream revision, licence, exact byte count, SHA-256, destination, and disk requirement before anything is requested. Nothing is fetched until you confirm it; the download is cancellable, and it is promoted to its final name only after its size and digest both match exactly. Local notes are checked separately, and setup can be finished without them.
  4. Ready — capture, transcription, notes, storage, and privacy reported separately. Capture is reported as not checked: Scrybe does not read whether macOS has granted microphone and system-audio recording, because no way to read it without prompting exists in this release. macOS asks the first time a recording needs the grant; if you have already refused it, macOS will not ask again, and step 2 above is where it is granted through System Settings instead.

Setup never asks for an account or an API key. Recording remains unavailable while transcription or storage is blocked; notes are optional and visibly so.

Settings carries the same model storage, the same readiness report, and a diagnostics list whose repairs run only when you choose one. Open advanced configuration opens config.toml for the settings no form models.

Reading what is already recorded

Sessions lists what is under the storage root, grouped by the day each meeting was recorded and newest first. A session that never finished, and one that can still be recovered, are marked as such in words as well as by the edge beside them; a session that never wrote a start time is filed under Undated rather than dropped. Only the first page is read, and the list says how many there are when there are more.

Search matches folder names and titles for every session, and notes and transcript text for completed ones. Searching again abandons the search still running, so a slower answer to an earlier question can never replace a newer one.

Opening a session shows what is actually on disk for it: its state, identity, times and length, how it was captured, which providers produced its text, and which of the five artifacts are present and which are not. Notes open first. The transcript is one button away and is read fifty lines at a time — the whole document never crosses into the window, so a long meeting costs the same to show as a short one.

Five actions, each offered only when the session accepts it and each saying why beside itself when it does not:

  • Regenerate notes — replaces notes.md from the durable transcript, through the provider config.toml names. Offered only for a completed session that has a transcript.
  • Repair recording — completes a recording that was interrupted. Offered only when durable state survives for it to recover.
  • Reveal in Finder — opens the session's folder. The folder is resolved in Rust beneath the storage root; the window never handles a path.
  • Copy notes and Copy transcript — read the document and put it on the clipboard, both in Rust. The transcript is never assembled in the window, for the same reason it is never read there whole.

A session with a playback.opus gets a player, using the platform's own controls. One without gets no player at all rather than a control that does nothing: playback.opus is written only for a two-channel capture, so a mono session is complete, has audio, and has nothing to play. The audio reaches the window through a scheme that serves that one file and no other — a URL on it names a session and the word playback, and anything else is refused before any file is opened.

Nothing on these screens edits a note or a transcript, and nothing deletes, archives, or moves a session. The files on disk stay the source of truth; the window is a way to read them.

From the terminal

mkdir -p ~/Library/Application\ Support/dev.scrybe.scrybe/models
curl -L -o ~/Library/Application\ Support/dev.scrybe.scrybe/models/ggml-small.en.bin \
  https://huggingface.co/ggerganov/whisper.cpp/resolve/5359861c739e955e79d9a303bcbc70fb988958b1/ggml-small.en.bin
shasum -a 256 ~/Library/Application\ Support/dev.scrybe.scrybe/models/ggml-small.en.bin
# expect c6138d6d58ecc8322097e0f987c32f1be8bb0a18532a3f88f734d1bbf9c41e5d

ollama pull gemma4:latest
scrybe init
scrybe doctor

The URL is pinned to an immutable upstream revision, and the digest above is the one the application's own catalog (scrybe-application/models.toml) carries — so a file fetched by hand and one installed through the application are the same 487,614,201 bytes. A resolve/main URL names whatever that branch points at today and no checked-in digest can describe it; do not substitute one.

scrybe doctor reports whether the model is present and whether a configured local notes endpoint is answering. A missing model is reported before a recording starts rather than when transcription does.

On macOS, bare scrybe init writes the local recording profile:

  • [record].source = "mic+system"
  • [record].input_device = "<Core Audio UID from scrybe devices>" (pins the meeting microphone instead of following later OS default changes)
  • [record].llm = "openai-compat"
  • [record].system_backend = "sck" (ScreenCaptureKit; use "tap" only for the macOS 14.4+ legacy Core Audio Tap recovery path)
  • [stt].model = "small.en" and [stt].language = "en"
  • [llm].model = "gemma4:latest"
  • [shell].indicators = ["menu-bar-waveform", "menu-bar-label", "floating-window"] (the default when [shell] is omitted)

The macOS platform data path is ~/Library/Application Support/dev.scrybe.scrybe/. Other platforms resolve the model path through their native data directory convention. Pass --profile default for the synthetic smoke-test profile, or override local model choices with --whisper-model <PATH> and --llm-model <MODEL>.

If a config file already exists, scrybe init refuses to overwrite it; pass --force only when you intentionally want to replace the existing config with fresh profile defaults.

Record with the native macOS shell:

scrybe record "client-call" --shell
# Stop from the menu bar, floating panel, global hotkey, or Ctrl-C.
scrybe list
scrybe show <session-id>

The ergonomic scrybe record TITLE resolves capture source, system-audio backend, Whisper model, and LLM kind from your config and platform probes. Add --shell to construct the configured indicators before capture starts; without it, recording remains headless. ScreenCaptureKit runs directly from the invoking terminal; the legacy tap backend auto-launches through the .app bundle so its Audio Capture TCC grant binds correctly. Run scrybe doctor after configuration to inspect the selected backend and accept or decline its live permission probe. Use scrybe rec --title TITLE --source … --system-backend … --whisper-model … --llm … --shell when you need explicit flag control (CI, debugging, alternate hardware setups).

Select a microphone explicitly:

scrybe devices
scrybe rec --title "client-call" --source mic --input-device <uid>

The terminal prints each accepted transcript chunk while recording, then reports transcript flush, audio encoding, notes generation, and metadata-writing progress after the first stop request. During a shell recording, the menu and panel change to Saving, freeze the elapsed time, and disable Stop & save until finalization returns. A second Ctrl-C aborts finalization immediately. Run scrybe repair <session-folder> to recover unfinished audio or reconstruct missing metadata, then scrybe notes <session-folder> to regenerate missing notes.

For cloud or hosted-compatible LLMs, configure [llm] with a base URL, model, and an environment-variable name for the API key. Secrets stay in the environment, not in config.toml.

Storage Model

Every session is a directory:

~/scrybe/
└── 2026-05-02-1430-acme-discovery-01HXY7K9RZ/
    ├── audio.opus
    ├── playback.opus
    ├── transcript.md
    ├── notes.md
    ├── meta.toml
    ├── pid.lock
    ├── transcript.partial.jsonl
    └── .stignore

The filesystem is the database. meta.toml, notes.md, audio.opus, and playback.opus use atomic replace. transcript.md is append-only. Audio is treated as the source of truth so failed or improved transcription can be regenerated later.

For --source mic+system, audio.opus preserves the source master with the user's microphone on the left channel and system audio on the right. playback.opus contains the same meeting as a centered stereo listening mix, preventing either speaker from playing in only one ear. The exact source layout remains recorded in meta.toml under [audio].layout as stereo:mic-l,system-r; downstream transcription and archival tools must use audio.opus, not the convenience playback mix. Mono sessions produce only audio.opus.

Architecture

scrybe is a Rust workspace with a small core and platform adapters:

Package Role
scrybe Published application and scrybe binary (scrybe-cli/)
scrybe-meeting-core Published session, storage, config, provider, hook, diarization, and pipeline library
scrybe-meeting-application Published session, configuration, diagnostics, and recording services shared by every frontend (scrybe-application/)
scrybe-meeting-capture-mac Published macOS microphone and system-audio adapter
scrybe-meeting-capture-mic Published microphone adapter via cpal
scrybe-capture-linux Private PipeWire/Pulse adapter surface
scrybe-capture-win Private WASAPI adapter surface
scrybe-android Private Android FFI adapter surface

The important public seams are:

  • AudioCapture for platform audio.
  • ContextProvider for meeting metadata.
  • SttProvider and LlmProvider for transcription and notes backends.
  • Diarizer for speaker attribution.
  • Hook for post-session actions.

The Tier-1 stability contract is documented in docs/system-design.md. In short: AudioCapture, MeetingContext, LifecycleEvent, ConsentAttestation, the meta.toml schema, storage invariants, and the Apache-2.0 license are frozen for the v1 series.

Privacy and Network Posture

  • The published application includes OpenAI-compatible provider support, but no network provider runs unless selected in configuration.
  • API keys are read from named environment variables.
  • There is no account system, sync service, telemetry, hosted backend, or bot that joins calls.
  • Courtesy notification is part of the recording flow and is recorded in meta.toml.
  • scrybe mcp (feature agent-access, off by default) is a read-only local-agent surface over ~/scrybe/: it exposes list_recent_meetings, search_meetings, get_meeting, get_meeting_notes, and get_meeting_transcript as MCP tools over stdio JSON-RPC. It is handed a read-only view of the shared session services whose trait surface has no method that repairs, regenerates, writes, or deletes anything, so there is no mutation a tool handler could reach. There is no network listener either — stdio only, reachable only by spawning it as a child process. The server refuses to start unless [agent_access].enabled = true is set explicitly in config.toml.

Run the egress audit locally:

cargo build -p scrybe --release --no-default-features
python3 scripts/check-egress-baseline.py

Current Limitations

  • macOS is the only polished binary distribution target today.
  • --source mic+system defaults to ScreenCaptureKit on macOS 13+ and requires Screen & System Audio Recording. This privacy permission covers screen recording in addition to system audio; deny it if that scope is unacceptable. ScreenCaptureKit runs from the invoking terminal and does not require an application bundle or signing identity.
  • The macOS 14.4+ Core Audio Tap backend remains available as [record].system_backend = "tap" for recovery. It requires the narrower Audio Capture permission and a signed .app bundle. scrybe doctor reports bundle state and offers repair with the project self-signed identity; it never creates or auto-selects a signing identity. A failed or silent Tap switches once to ScreenCaptureKit after a 1.5 s startup window, so a quiet desktop can switch before external audio begins.
  • The application reads sessions; it does not record one yet. Recording remains a terminal command.
  • Reading a session offers no paging control: the first twenty are shown and the count says how many there are. Search is submitted rather than run per keystroke.
  • The native recording shell requires the default cli-shell feature and explicit --shell; headless recording remains available. The menu-bar-label indicator is the Scrybe branding slot, and the status item never includes elapsed time. Configured indicators contain only recording state, elapsed time, and the stop control—never captured or generated meeting content.
  • crates.io installation supports the polished macOS application. Linux and Windows recording remain parked until hardware-qualified release paths exist.
  • Native macOS notarization and Windows Authenticode signing are out of scope for the v1 line. Release artifacts are verified with checksums and cosign provenance instead.

Development

Required toolchain is pinned by the repository:

cargo fmt --all --check
cargo clippy --workspace --all-targets --no-default-features -- -D warnings
cargo check --workspace --all-targets --no-default-features
cargo test --workspace --all-targets --no-default-features

Additional gates used by CI include cargo audit, cargo deny check, coverage, LoC budget, egress audit, release planning, and advisory reproducibility checks.

The desktop application is qualified against the built bundle rather than against a test harness, which needs a real macOS login session and so runs locally rather than in CI:

python3 scripts/qualify-desktop-app.py --hermetic --scenario lifecycle
python3 scripts/qualify-desktop-app.py --hermetic --scenario setup
python3 scripts/qualify-desktop-app.py --hermetic --scenario library
python3 scripts/qualify-desktop-app.py --hermetic --scenario recording
python3 scripts/qualify-desktop-app.py --hermetic --scenario installed

Each drives the shipped shape of the application against a disposable storage root and configuration, and reports what it observed rather than what it assumed. library is the one that answers a question no unit test can: whether the webview may load a scrybe-audio:// URL at all. That is decided by the content security policy, and the policy check in that script compares the built artifact against a constant the same file declares — so it passes for any string written in both places. Driving the real webview at a real URL and reading back what the protocol handler served is what tells a correctly-admitted scheme from one admitted under a directive that does nothing.

recording answers a different one. Every surface a recording is stopped from is a platform event, and the dispatch behind each is reachable from a unit test — but that the built application wires them to it is not. A tray item built disabled, a command missing from a capability file, a permission absent from the generated manifest: each leaves the unit tests passing and the application inert. That scenario records a real session through the synthetic source and the stub providers, so it needs no device, no permission grant and no network, and asserts on the session the pipeline actually wrote.

installed is the only one that does not pass, and it is meant not to. It copies the bundle out of the build tree and drives the copy, checks that the process it is driving really came out of that copy rather than some other registered one, records a session and then asks the reader for that session — the handoff from recorder to reader that neither recording nor library covers, because one asserts what was written and the other reads sessions this harness seeded. It then reads the tray's accessible names out of the running application's accessibility tree, and stops at the wall: no Developer ID Application certificate and no notarization credential exist here, so the copy is ad-hoc signed and cannot be qualified as installable by anyone else. That last check fails rather than skipping. A qualification reporting an installed application as qualified, having driven an artifact nobody else could install, would be worse than no qualification — so a green installed on a machine with no distribution credential means a check has stopped being able to fail.

Project docs:

License

Apache-2.0. See LICENSE.