| title | Privacy Statement |
|---|---|
| description | What YazSes does and does not do with your voice, text, and data. Offline by design — nothing leaves your machine unless you explicitly turn on the SSH remote path. |
Last updated: 2026-07-10
YazSes is designed from the ground up to keep your voice and text on your device. By default, no audio, transcripts, editor context, or usage data ever leave your machine. There is no cloud dependency, no telemetry, and no account. Everything below describes local behaviour; the only outbound path is the SSH remote feature you turn on yourself, and even then only the final typed text is sent.
Audio is captured from your microphone only while you hold the hold-to-talk key (or squeeze a connected EMG device). It is held in a short in-memory buffer, fed directly to the on-device transcription model, and then discarded. Because YazSes is push-to-talk, nothing is recorded while the key is up. Audio is never written to disk, never logged, and never transmitted anywhere — unless you explicitly opt in to the learning corpus (below) with audio capture enabled.
Transcription runs entirely on your device using
faster-whisper (CPU, int8; no GPU
required). The model weights are downloaded once into the shared Hugging Face cache —
~/.cache/huggingface/hub/ on Linux and macOS, %LOCALAPPDATA%\huggingface\hub\ on
Windows — and run offline thereafter. That cache is shared with any other Hugging Face
tool you have, which is why it survives uninstalling YazSes and has
its own step on the uninstall page. The resulting transcript is held in memory
only long enough to inject the text (and, if commands are enabled, to check whether
you spoke a command). At the default log level it is not written to any file, and it
is never transmitted anywhere.
The diagnostic log (yazses logs) records metadata only — timing and state,
never your dictated text — at the default [general] log_level = "INFO".
!!! warning "log_level = "DEBUG" is the one exception"
Setting [general] log_level = "DEBUG" in config.toml also writes each transcript
into daemon.log, which is what makes a hard-to-reproduce dictation bug diagnosable.
Nothing is uploaded either way — but that file then holds what you dictated, so treat
it as you would the text itself. yazses report knows this and omits every DEBUG
line from the bundle it builds for a bug report, stating how many it dropped. The
same bundle also reports the daemon's staged buffer — text you have dictated but
not yet committed — by length rather than content, for the same reason. To
clear it: set the level back to "INFO" and delete the file at
yazses logs --path.
YazSes can improve its accuracy for your voice over time, but only if you opt in.
The [learning] feature is disabled by default; when it is off, nothing is
captured.
When you enable it (yazses features enable learning), the daemon records one event
per hold-release into a machine-bound, encrypted local corpus at
~/.local/share/yazses/corpus.db (with audio clips under clips/). The transcript
text and any stored audio are encrypted with AES-256-GCM using a key derived from
your machine — there is no passphrase to remember and no cloud backup. Only coarse
metadata is kept in the clear. Capture happens on a background thread and nothing is
ever uploaded.
You stay in control of the corpus:
| Action | How |
|---|---|
| Never capture anything | Leave [learning] disabled (the default) |
| Turn capture off again | yazses features disable learning |
| Store text but not audio | Set capture_audio = false under [learning] |
| Auto-expire old events | retention_days / max_corpus_mb under [learning] |
| Scrub patterns from stored text | redact_patterns under [learning] |
| Inspect what is stored | yazses corpus status |
| Erase everything | yazses corpus destroy (or delete corpus.db) |
redact_patterns are regular expressions removed from every transcript column before it is
encrypted — but a redaction cannot reach into a waveform. With capture_audio on (the
default), the clip still holds you saying the thing the pattern removes. If that matters,
set capture_audio = false; yazses doctor warns when patterns are set and the audio is
being kept anyway. An invalid pattern stops capture entirely rather than being skipped, so
a redaction you asked for is never silently dropped.
retention_days and max_corpus_mb are applied in sweeps — when the daemon starts, and
then every 200 captures — rather than on every write. So an event older than
retention_days can still be on disk until the next sweep, and yazses corpus status can
report a size above max_corpus_mb. Both are limits the corpus is pulled back to, not
ceilings it is prevented from crossing. If you need something gone now, use
yazses corpus forget -m N or yazses corpus destroy, which act immediately.
A sweep will not empty the corpus to satisfy the size limit: it trims the oldest events as far as reclaiming disk actually helps and then stops, leaving the rest in place.
Deleting an event — by forget, by retention, or by the size sweep — zeroes its stored
bytes rather than only unlinking the row, so the transcript is not left readable in a
freed page of corpus.db. That matters because the key is deliberately not kept
elsewhere: corpus.key is machine-bound and sits beside the database, so anything left
in the file is readable by anyone who has the machine. forget additionally compacts
the database, which clears residue left by earlier versions.
What that does not cover is the filesystem underneath: the blocks of an unlinked audio clip, or of a deleted journal, may survive on the disk until they are overwritten. If that matters for your threat model, full-disk encryption is the control for it.
Spoken Recall ("what did I say about X") queries this same encrypted corpus. Ambient
Scratch does not: a spoken note-to-self is appended to scratch.jsonl as plain
JSON, because a note you dictated in order to read it back later is a note, not a
capture. Neither leaves the machine, and both are off by default.
yazses meeting records a whole meeting rather than a hold-to-talk burst, so it captures
other people's voices as well as your own. It is disabled by default and does nothing
until you set [meeting] enabled = true and start a meeting explicitly.
Everything it produces stays in meetings/<id>/ under your data directory, in plain text:
the transcript, the live rolling transcript kept for crash recovery, and — if you turned
notes on — the generated minutes. The recording itself is deleted after the post-pass
unless you set [meeting] retain_audio = true; it is kept until then so that a crash
during transcription cannot lose the meeting. Nothing is uploaded, and transcription and
minutes both run on-device.
Naming speakers is separate and explicit. Diarization alone labels people Speaker 1,
Speaker 2; a name appears only if you pass one, or if you have deliberately enrolled that
person with yazses meeting enroll. An enrolled participant is stored as an encrypted
voiceprint under participants/ and never auto-created from a recording. Recording other
people may need their consent where you are — YazSes does not and cannot judge that for you.
Most of what YazSes persists lives in one directory — ~/.local/share/yazses/ on Linux,
~/Library/Application Support/yazses/ on macOS, %LOCALAPPDATA%\yazses\ on Windows — and
nothing in it is uploaded anywhere.
Most, not all, and the difference matters if you are deleting things: YazSes follows the platform's directory conventions, so on Linux and macOS it writes under four roots, not one. The other three are listed below; the uninstall page removes all of them.
| Path | Holds | Encrypted |
|---|---|---|
corpus.db |
the learning corpus: metadata in the clear, every transcript column encrypted | transcripts yes |
clips/ |
source audio for corpus events, when capture_audio is on |
yes |
corpus.key |
the machine-bound key for both of the above, mode 0600 |
n/a — it is the key |
scratch.jsonl |
spoken notes-to-self (Ambient Scratch, off by default) | no — plain JSON |
few_shots.toml |
utterances you approved in yazses tune as command-router examples |
no — plain text |
meetings/ |
meeting transcripts, live transcripts, minutes, and retained recordings | no — plain text |
participants/ |
voiceprints of people you enrolled by name for meetings | yes |
voiceprint.enc |
your own enrolled voiceprint | yes |
diarization/, tts/ |
downloaded model files — no content of yours | n/a |
daemon.lock, tray.lock, tray-stderr.log |
single-instance locks and the tray's stderr | n/a |
yazses-report.json |
the last yazses report bundle you generated, already redacted |
n/a |
The three marked no are the ones worth knowing about, and each is deliberate: a note you dictated in order to read it back, examples you explicitly approved, and a meeting transcript whose whole purpose is to be opened and edited. They are ordinary files with your user's permissions. If you want them at rest under a key, that is what full-disk encryption is for.
| What | Linux | macOS | Holds |
|---|---|---|---|
| Settings | ~/.config/yazses/ |
~/Library/Application Support/yazses/ |
config.toml, vocabulary.txt |
| Model cache | ~/.cache/yazses/ |
~/Library/Caches/yazses/ |
downloaded model files — the gaze landmarker, and any GGUF the optional intent router uses (up to 2.2 GB) |
| Diagnostic log | ~/.local/state/yazses/log/ |
~/Library/Logs/yazses/ |
daemon.log and its rotations |
On Windows all four are nested inside %LOCALAPPDATA%\yazses\, so there is only one
folder to think about.
Two of these are worth a second look. The model cache holds no content of yours, but
it is the largest thing YazSes leaves behind and it is not where people look. The
diagnostic log holds metadata only at the default log level — but log_level = "DEBUG" puts every transcript in it, as the warning above says, and it does not live in
the data directory that the rest of this page is about.
YazSes reads vscode-context.json from the model-cache directory if it is there, to
pick up your editor cursor position. Nothing in YazSes writes it, and the companion VS
Code extension that was designed to write it has never been published — so in practice
the file is absent. Removing the cache directory removes it.
YazSes reads a TOML configuration file at startup:
| Platform | Path |
|---|---|
| Linux | ~/.config/yazses/config.toml |
| macOS | ~/Library/Application Support/yazses/config.toml |
| Windows | %LOCALAPPDATA%\yazses\config.toml |
It holds your preferences (hotkey, microphone device, model selection, optional EMG port, feature toggles, and so on). It is read locally and never transmitted anywhere.
The daemon communicates with the CLI and tray through a local, host-only channel: a Unix domain socket on Linux/macOS, a named pipe on Windows. YazSes does not open a network port or listen on any external interface for this.
Correction (2026-08-17): this section previously described the daemon reading
your active file path, language and cursor line into every transcription prompt,
controlled by lsp_enabled. That never happened. The daemon does not construct
the editor bridge, and lsp_enabled is read by nothing — so no editor context has
ever reached the transcription prompt. The statement overstated what YazSes
collects, and the mitigation it offered protected against nothing.
What is true: the editor bridge is contacted only when you run a command that
needs it — yazses jump — which asks your editor for symbols and cursor position
to move the caret, on demand, for that one invocation. Nothing is stored, and
nothing leaves your device.
Two optional features use a local language model. There is no cloud LLM and no OpenAI/Azure/Anthropic backend in either:
- SLM intent router (
[commands] slm_model_path) — a small local model, loaded from a model file on your disk viallama-cpp-python, that resolves a spoken command when the fast regex grammar is unsure. In-process only; it opens no socket. - Offline dictation cleanup (
[filters.disfluency] llm_enabled) — lightly reformats dictation. Off by default. It has two backends: a local GGUF file viallama-cpp-python(llm_model), or — if no model file is set — an HTTP POST to Ollama atllm_endpoint, which defaults tohttp://localhost:11434.
If you do not configure a model path, neither feature is active and no model runs.
Dictation cleanup's Ollama backend is the only path in YazSes that sends transcribed
text over a socket. That is worth stating plainly rather than rounding down to "nothing
leaves your machine", because it is an HTTP request and llm_endpoint is a string you can
edit.
YazSes refuses to send it anywhere but this machine. Before any request is made, the
endpoint is checked to be a loopback address — localhost, anything in 127.0.0.0/8, or
::1. Point it at a LAN box, a VPS, or a hosted API and cleanup switches itself off and
logs why, rather than quietly posting your dictation to it.
A hostname that merely resolves to 127.0.0.1 is not accepted either. DNS is not a
security boundary: the answer is controlled by whoever owns the zone and can change between
the check and the connection, so a guard that trusted resolution would depend on the network
it exists to avoid.
If sending dictated text off this machine is genuinely what you want, it takes a second, separate, deliberate setting:
[filters.disfluency]
llm_endpoint = "http://ollama.my-lan-box:11434"
llm_allow_remote_endpoint = true # off by default; warns on every daemon startTwo edits, not one, and never the default. Audio is never involved in any case — only the already-transcribed text, and only when you have turned cleanup on.
Remote mode lets you dictate into an application on another machine you control.
Your speech is still captured and transcribed locally — only the final typed
text is forwarded over an SSH tunnel to the remote host, where yazses-agent
injects it. Your audio never leaves your machine. The target host is always specified
by you on the command line; YazSes never connects anywhere automatically. Ensure you
trust and control the remote host before using this feature.
| Data | Default (local-only) | Remote mode (yazses remote) |
|---|---|---|
| Audio | Stays on device | Stays on device (never forwarded) |
| Transcript / typed text | Stays on device | Final text forwarded over SSH to your host |
| Editor context | Stays on device | Not forwarded |
| Learning corpus | Stays on device | Not forwarded |
| Telemetry / usage stats | Never collected | Never collected |
In the default configuration, nothing leaves your device.
Two features can change that, and both are off until you turn them on and neither ever
moves audio: yazses remote <host> forwards the final text to a host you name, and
dictation cleanup can POST transcribed text to an Ollama endpoint — which YazSes holds
to loopback unless you also set llm_allow_remote_endpoint = true. There is no third path,
and the table above is what the test suite and the --network none check below verify.
A privacy policy is a promise. This one is testable, and you should test it rather than trust it. The strongest check takes one flag: transcribe with networking switched off entirely and confirm it still works.
No Docker, or you would rather test the app you actually installed? The offline-inference challenge walks through the same demonstration on a normal desktop in about ten minutes, and there is a report template if you want to publish what you saw. It is careful about one distinction that is easy to blur and would make the result meaningless: installing and downloading a model needs the network; transcribing does not.
# Once the model is cached, this container has no network at all.
docker run --rm --network none \
-v yazses-models:/home/yazses/.cache \
-v "$PWD/data/librispeech-sample:/data:ro" -v /tmp:/out \
yazses transcribe jfk.wav -o /out/jfk-heard.txtThat transcribes correctly with no route to the internet, which is only possible if the speech recognition is genuinely running on your own machine. Full instructions.
On a normal install you can watch the same thing directly:
# Show every connection the daemon has open. After the model download, expect none.
ss -tunp 2>/dev/null | grep -i yazses || echo "no network connections"Or cut it off at the source and keep dictating:
sudo ip link set <your-interface> down # or just pull the Wi-FiThe one time YazSes needs the network in its default configuration is the first run, to download the speech model from Hugging Face. After that, dictation never needs it again.
That default is most of the product but it is not all of it, and the distinction is worth stating exactly rather than rounding off. Every optional feature that runs a model of its own downloads that model once, when you enable it — a different speech engine, speaker diarization, read-back, gaze targeting, offline LLM cleanup. Each of those features is off until you turn it on; each fetch is a one-time download of weights; and none of them sends anything — not audio, not a transcript, not an identifier.
One deserves naming on its own. The optional pyannote diarization backend uses a gated
model, so that download carries your Hugging Face token — the host requires it to check you
accepted the licence. It is still a download and it still carries nothing you said, but it
is the only fetch in the product that tells anyone who is asking, which is not the same
disclosure as an anonymous one.
yazses update is the only other outbound action, and only when you run it yourself —
unless you set [general] update_check = true, which is off by default and, when on,
fetches a version string in the background and nothing else.
All of it is enumerated — module, destination, direction and trigger — in the project's egress inventory, and a test fails the build when any module gains a connection that is not on that list.
YazSes collects no telemetry, no usage analytics, and no crash reports, and makes no
automatic outbound connections in its default configuration. Update checks are a manual,
explicit action (yazses update); YazSes does not phone home on its own. The one thing
that can become automatic is the release watcher, [general] update_check, which is off by
default and which — when you switch it on — reads a version string and announces a new
release once. It carries nothing about you or your machine, and a firewall makes it a
silent no-op rather than a delay.
"Prepare a bug report" is not an exception to this, and it is worth being exact
about why. When YazSes cannot identify a failure, its notification offers a button. That
button assembles the same redacted bundle yazses report writes and opens your
browser at GitHub's issue form with the text already filled in. You read it there,
signed in as yourself, and press submit — or close the tab. YazSes never makes the
request; your browser does, if you decide to. Nothing is transmitted by YazSes at any
point, and the project's egress inventory
— which a test enforces on every build — is unchanged by this feature.
YazSes builds on open-source libraries — faster-whisper, sounddevice, and (optionally)
llama-cpp-python, among others — all of which run on your device. You can audit them
through the project's dependency manifest (pyproject.toml).
A machine-readable inventory ships with the project, for anyone whose organisation requires one before software may be installed:
sbom.cdx.json— a CycloneDX 1.5 SBOM listing every resolved dependency with its version, package URL and, where the lock file records one, the SHA-256 of its source distribution.
Every component carries a CycloneDX scope, so the inventory distinguishes what you
receive from what only the maintainer builds with:
scope |
Count | What it means for you |
|---|---|---|
required |
57 | Installed by pip install yazses, across every supported platform |
optional |
167 | Installed only if you enable the matching feature (yazses features enable …) |
excluded |
54 | Test, type-check, benchmark and docs tooling. Never installed by a user |
That distinction matters if you feed the SBOM to a vulnerability scanner: without it, every advisory against a test or documentation package would be reported against your installation, and there would be no way to tell which findings apply to you.
It is generated from uv.lock rather than from whatever happens to be installed on a
build machine, so it describes what you will actually resolve to, and a test fails if
it drifts out of step with the lock file. Regenerate it yourself with
python scripts/gen-sbom.py.
| Action | How |
|---|---|
| Disable the learning corpus | Leave [learning] off (default), or yazses features disable learning |
| Erase all captured data | yazses corpus destroy (or delete corpus.db) |
| Disable editor context | Nothing to disable — the daemon never reads your editor; only yazses jump contacts it, when you run it |
| Disable the offline cleanup LLM | Set llm_enabled = false under [filters.disfluency] (the default) |
| Inspect stored data | yazses corpus status |
Because all data is stored locally and no account is required, there is no server-side data to request deletion of. You have full control over every file YazSes writes.
YazSes is an open-source project. If you find a privacy concern or a discrepancy between this statement and the software's actual behaviour, please open an issue on the project's GitHub repository so it can be investigated and corrected.