<p align="center">
<img src="assets/banner.png" alt="Scrybe banner" width="100%">
</p>
# scrybe
[](https://crates.io/crates/scrybe)
[](LICENSE)
> 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: `v2.2.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](INSTALL.md#optional-streaming-zipformer-and-english-paired-stt-benchmark). 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 native macOS application from the DMG attached to the [latest GitHub release](https://github.com/Mathews-Tom/scrybe/releases/latest). Choose the Apple-silicon (`aarch64`) or Intel (`x86_64`) image, verify it against the signed checksum manifest, open the image, and drag `Scrybe.app` to `/Applications`.
The application uses Scrybe's stable self-signed community certificate. It is not signed with Apple Developer ID and is not notarized, so Gatekeeper rejects the first launch. Verify the release and certificate fingerprint, then use **System Settings → Privacy & Security → Open Anyway**. Do not disable Gatekeeper globally or remove quarantine metadata. Exact verification and installation steps are in [`INSTALL.md`](INSTALL.md#macos-desktop-application--dmg).
Install the command-line application from crates.io:
```sh
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. The prebuilt CLI installer remains available:
```sh
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/Mathews-Tom/scrybe/releases/latest/download/scrybe-installer.sh | sh
scrybe doctor
```
The native app, CLI archive verification, source builds, and community trust limitations are documented in [`INSTALL.md`](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
```sh
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:
```sh
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:
```sh
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:
```text
~/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`](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:
```sh
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.
- 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.
- Native application releases are self-signed with the stable Scrybe community certificate and are not Apple-notarized. Gatekeeper rejection is expected until the user chooses Open Anyway after verifying the release. Apple Developer Program enrollment remains deferred.
- Linux and Windows recording remain parked until hardware-qualified release paths exist.
## Development
Required toolchain is pinned by the repository:
```sh
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:
```sh
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 model-download-live
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` copies the community-signed bundle out of the build tree and drives that copy. It verifies the running process came from the copy, records a session and reads the same session through the reader, reads both the Scrybe status item's name and its actions from the native accessibility tree, and checks the explicit community trust contract: stable certificate, expected Gatekeeper rejection, and no notarization ticket. The native accessibility observation requires a human-granted TCC permission for the invoking terminal. `model-download-live` is the release-only proof that the checked-in production catalog can complete its pinned Hugging Face transfer and promote the exact expected bytes inside a disposable model root.
Project docs:
- [`docs/pitch.md`](docs/pitch.md) — product framing and market position.
- [`docs/system-overview.md`](docs/system-overview.md) — user-facing system explanation.
- [`docs/system-design.md`](docs/system-design.md) — engineering contract and stability tiers.
- [`docs/desktop-recording.md`](docs/desktop-recording.md) — how a recording starts and stops from the desktop application, and what each way of failing leaves behind.
- [`INSTALL.md`](INSTALL.md) — installation, source builds, and release verification.
- [`MAINTENANCE.md`](MAINTENANCE.md) — v1 maintenance commitments.
- [`CHANGELOG.md`](CHANGELOG.md) — release history.
## License
Apache-2.0. See [`LICENSE`](LICENSE).