# v2.36.0 — the failures that reported success

**Released 2026-08-30.** Upgrade:

```sh
sudo snap refresh yazses     # snap
pipx upgrade yazses          # or: uv tool upgrade yazses
scoop update yazses          # Windows
```

This is a large release, and it has one shape running through it. Almost nothing here
crashed. A config file was discarded and the daemon started anyway. A secret was written
to disk by the setting that exists to remove it. A guard was skipped by the one code path
that executes what it guards. A CI job ran 48 tests out of 13,800 and went green. In every
case the failing thing returned normally, which is why these lasted as long as they did.

The full list is in the [CHANGELOG](https://github.com/MSKazemi/yazses/blob/main/CHANGELOG.md).
What follows is what actually affects you.

## If you have ever edited `config.toml` on Windows

**A UTF-8 byte-order mark threw away every setting in the file, not one line.**

`tomllib` rejects a leading BOM — TOML has no place for one — and a TOML document parses
as a whole, so the failure is total. The daemon fell back to defaults for *everything*:
model, hotkey, VAD threshold, injector. The error it reported was

```
could not be read (Invalid statement (at line 1, column 1))
```

about a line that looks perfectly correct on screen, because the offending bytes are
invisible.

This is not an exotic input. **Windows PowerShell 5.1 — the default shell on Windows —
writes UTF-8 *with* a BOM** from `Set-Content` and `Out-File`, and "UTF-8 with BOM" is
still an offered encoding in Notepad. Every hand-edited TOML this project reads was
affected: `config.toml`, the macros file, the style-rules file, and the copy `yazses
report` puts in a diagnostic bundle — so the bundle you would send to ask for help said
your config was unreadable, pointing every reader at the wrong problem.

The same mark cost the **vocabulary file** its first entry, which is the word you added
first and care about most. `﻿Kubernetes` renders identically to `Kubernetes` in
`yazses vocab list`, is not matched by `yazses vocab remove Kubernetes`, and reaches
Whisper as a token the model has never seen — so priming that one word silently stopped
working.

Both now read `utf-8-sig`. Writing any setting also *repairs* a file that already had a
BOM, rather than carrying it forward — otherwise the one command you would run to fix
your config would keep re-emitting the thing that broke it. A real syntax error is still
reported as one.

## A one-letter typo in `[redaction] mode` shipped the secret it was set to hide

`[redaction] mode` accepted a misspelling and fell through to *not redacting*. The setting
that exists to keep a value out of a file was, when misspelled, the setting that put it
there — and nothing said so, because an unrecognised mode is not a parse error.

If you use redaction, check the spelling of that key once after upgrading.

## A destructive command skipped its safety gate in the one mode that runs it

`cmdsafety` holds a dictated `rm -rf`, `mkfs`, `dd of=`, `curl | sh` or force-push until
you confirm it out loud. It was wired into the dictation path and **not** into command
mode — the one path that actually presses Return. Holding the command key says *"this is
a command"*, not *"and I accept this one"*, and a misheard command is exactly as dangerous
as an unintended one.

Both branches are now gated.

## Dictation

- **A large vocabulary erased the one word the prompt exists to prime.** faster-whisper
  keeps only the **last** 223 tokens of `initial_prompt` and drops the front in silence,
  so putting a term first meant deleting it first. Priming is now appended, not prepended.
- **The disfluency filter lost the first half of a sentence** in which a filler phrase is
  doing real work. The self-correction guard looks at the single word before a trigger to
  decide whether a rollback is genuine, and a pronoun is not enough to tell the two apart:
  "they never mind the noise from the street" came out as "the noise from the street".
  Android lost the same sentences, because only one of the guard's two lists had been
  ported to Kotlin.
- **`yazses mic-level` calibrated to an empty room and called it a recommendation.** One
  recording cannot tell speech from room tone, so a quiet room produced a confident
  threshold *below that room's own noise* — the state where ambient noise clears the gate
  and the decoder answers it with an invented word. It now records twice, places the
  threshold between the two, and **refuses** when they are closer than 3× rather than
  inventing a number.
- **The microphone-change watcher could not fire on the setup most Linux users have.** It
  compared the default input's *name* between polls, and on ALSA/PipeWire that name is
  `default` — a route, not a microphone. It compared `default` with `default` forever, so
  unplugging a headset mid-session went unannounced. It now reads through the alias where
  the desktop lets it, and stays quiet rather than guessing where it cannot.

## Meeting Mode

- Minutes could **leave out part of a meeting without saying so**: the reduce step never
  ran on a meeting long enough to need it, a summariser window could fail by *returning*
  rather than raising and nothing counted it, and the `INCOMPLETE` disclosure was left for
  the summariser itself to keep.
- A pyannote pipeline that returns no annotation failed mid-loop instead of degrading.

## Install channels

- **For two years, every release told Ubuntu users to run
  `sudo add-apt-repository ppa:mskazemi/yazses`.** Launchpad answers 404 for that archive
  *and* for the account it would belong to — the PPA was never created, and the workflow
  meant to publish it had been dead since v1.0.0, triggering on `v0.*` tags only. The
  instruction is gone, and a test now refuses to let the release notes advertise a channel
  that nothing in this repository publishes.
- **On FreeBSD, every spoken command crashed.** Resolving a key name imported `evdev`, a
  Linux-only C extension, so every command, every backspace correction and the clipboard
  paste raised `ModuleNotFoundError`. The keycode table is now committed — safe precisely
  because `input-event-codes.h` is a kernel ABI. The FreeBSD CI job that should have caught
  it was running **48 tests out of 13,800**.
- **Flathub was still building the release before last.** The one line in the generated
  Flatpak manifest that has to move with every release — the `yazses` wheel pin — did not
  move when 2.35.0 was cut, so Flathub would have built 2.34.0 while the metadata beside
  it advertised the current version. A test now fails the build when the pin falls more
  than one release behind.
- A re-run of the snap publish uploaded a duplicate revision every time; ~200 store
  rejection emails came from a job that uploaded without asking the store first.

## Security

- **The one advisory in this tree that is actually reachable had no assessment.** The other
  two had Dependabot alerts — a UI, an email, a warning on push. This one existed only as
  an id inside a Scorecard check detail with no summary, so attention had followed
  *notification surface*, which is uncorrelated with severity. It is now assessed in
  `SECURITY.md` with its bounds stated and guarded by tests.
- **A `setuptools<81` pin inside one opt-in extra held every install below a patched
  release** — including both shipped bundles, because `uv.lock` is a universal resolution
  and a pin in an extra is not confined to it.
- **The program that writes the Flatpak pins was itself unpinned.** `uv` and `req2flatpak`
  generate the 45 URL-and-hash pins Flathub builds from, and were installed with
  `pip install --upgrade`. The hash is the one field a reviewer cannot check by reading.
- **The only executable binary in the tree was checked by nothing.** `gradle-wrapper.jar`
  is 43 KB that `./gradlew` executes, and a swapped wrapper survives review because a
  reviewer sees a filename in the diff stat and cannot read the contents. It is now
  validated against Gradle's published checksums on every commit.
- Three workflows granted a write token at the top level, where every job added to the file
  afterwards would inherit it.

## Added

- **The text pipeline is now fuzzed.** Two `atheris` harnesses cover the two surfaces that
  take input nobody wrote: the config parser and the transcript post-processing chain.
- `yazses doctor` reports the update watcher, which was both off by default and invisible.
- The `windowctl` layout grammar, which shipped with no caller, is wired.

## For maintainers

Several fixes here are about checks that were not checking. Twenty guards reported findings
about a repository they never read; a monthly Dependabot job was red on a bump that cannot
exist; a contract-only change reported the previous run's pass; and a weekly job written to
prove the two heavy extras work had, since it was added, produced no run at all — its pin
was undone by the next line, and the gate underneath demanded something no single
environment could give it.

## Upgrading

Your config, models and learning corpus are untouched. If you edit `config.toml` on
Windows, this release is the one that makes that safe.
