Push-to-talk dictation for Linux (Wayland or X11). Hold Right Ctrl, speak, release -- Scribe transcribes your speech with Groq's Whisper Large v3 Turbo, copies it to the clipboard, and auto-pastes into the focused window. No GUI or tray icon.
curl -fsSL https://raw.githubusercontent.com/cszach/scribe/main/install.sh | bashThis clones Scribe into ~/.local/share/scribe and sets everything up. To hack
on the code instead, see manual setup below.
- Linux with a working microphone
- Python 3.11+
wl-clipboardydotoolfor auto-paste (optional if you setSCRIBE_NO_AUTO_PASTE=1)- A Groq API key (free tier is sufficient for personal use)
# Debian/Ubuntu
sudo apt install wl-clipboard ydotool
# Fedora
sudo dnf install wl-clipboard ydotoolListening to a global hotkey under Wayland requires read access to
/dev/input/event*, which is gated on the input group:
sudo usermod -aG input $USERLog out and back in (or reboot) for the group change to take effect. Verify
with groups | grep input.
For auto-paste, start the ydotoold daemon so ydotool has a socket to talk
to:
sudo systemctl enable --now ydotoolFedora (and any distro shipping the upstream unit): the default
ydotool.service writes its socket to /tmp/.ydotool_socket owned by root, so
non-root ydotool calls fail with exit status 2. Change the socket ownership
(not the path โ /run/user/$UID/ doesn't exist at boot, so pointing the system
unit there makes ydotoold crash before you log in):
sudo mkdir -p /etc/systemd/system/ydotool.service.d
sudo tee /etc/systemd/system/ydotool.service.d/override.conf >/dev/null <<EOF
[Service]
ExecStart=
ExecStart=/usr/bin/ydotoold --socket-path=/tmp/.ydotool_socket --socket-own=$(id -u):$(id -g)
EOF
sudo systemctl daemon-reload
sudo systemctl restart ydotoolScribe sets YDOTOOL_SOCKET=/tmp/.ydotool_socket when invoking ydotool, so no
shell-rc export is needed.
ydotoold runs as root and injects synthetic keyboard events into
/dev/uinput. If you don't want this, set SCRIBE_NO_AUTO_PASTE=1 and Scribe
will only copy to clipboard (you'll paste manually).
Skip this section if you used the curl installer above โ it covers everything here. This is for people who want to clone the repo and hack on the code:
git clone https://github.com/cszach/scribe.git && cd scribe
./install.shinstall.sh is the single source of truth โ it creates the venv, installs
Python deps, writes .env, drops the scribe CLI in ~/.local/bin/, and sets
up the systemd user service. It's idempotent, so re-running it is the same as
scribe update (minus the git pull).
If evdev or sounddevice build from source (no prebuilt wheel for your Python
version), install the native deps first:
# Debian/Ubuntu
sudo apt install libevdev-dev portaudio19-dev
# Fedora
sudo dnf install libevdev-devel portaudio-develTo run Scribe ad-hoc without the systemd service:
source .venv/bin/activate
python scribe.py- Focus the window you want to dictate into.
- Hold Right Ctrl.
- Speak.
- Release. The transcript is pasted at your cursor.
Quick taps (under 300 ms) are ignored as accidental presses. Hold as long as you
like โ there's no time limit by default. To skip auto-paste and just copy to the
clipboard, run with SCRIBE_NO_AUTO_PASTE=1.
Different apps bind paste to different keystrokes. Set SCRIBE_PASTE_MODE to
pick which one Scribe synthesizes:
| Mode | Keystroke | Use it for | Avoid |
|---|---|---|---|
shortcut (default) |
Ctrl+Shift+V | Terminals (Claude Code, gnome-terminal, kitty), most Electron apps | Native GTK/GNOME apps (no-op) |
ctrl_v |
Ctrl+V | GTK/GNOME apps (gnome-text-editor), most browsers | Terminals (inserts literal ^V) |
type |
Types character-by-character | Anywhere the shortcut modes don't work | Long transcripts (~10ms/char) |
The text is on the clipboard regardless of mode, so a manual paste always works as a fallback.
| Symptom | Fix |
|---|---|
no keyboards with KEY_RIGHTCTRL found |
You're not in the input group, or haven't logged out since adding it. See One-time setup. |
permission denied on /dev/input/eventN |
Same as above. |
| Hotkey suddenly stops working after sleep/wake, dock unplug, or USB re-plug | Kernel re-enumerated the keyboard and scribe's evdev fd died. Confirm with journalctl --user -u scribe | grep disconnected โ look for device /dev/input/eventN disconnected. Recover with systemctl --user restart scribe (or kill <pid> and relaunch if running ad-hoc). Known limitation โ scribe scans devices once at startup and doesn't re-bind on hotplug. |
GROQ_API_KEY not set |
~/.config/scribe/.env is missing or empty. Run scribe update to set it. |
| Recording starts but transcript is empty | Mic isn't capturing โ check pavucontrol / wpctl status and confirm your input is unmuted. |
transcription failed: ... |
Network or API issue. Daemon stays alive; just press and try again. |
auto-paste failed: ...exit status 2 |
ydotoold is running but the socket isn't accessible to your user. Apply the Fedora override in One-time setup. |
auto-paste failed: ... |
ydotoold isn't running. sudo systemctl start ydotool, or set SCRIBE_NO_AUTO_PASTE=1. |
| Transcript copies but doesn't paste | Wrong SCRIBE_PASTE_MODE for the focused app. See Auto-paste modes. |
ydotool not installed |
Install it (see Prereqs) or set SCRIBE_NO_AUTO_PASTE=1. Clipboard still works regardless. |
Everything tunable lives in ~/.config/scribe/.env; see .env.example in the
repo for the full list with documented defaults. All variables are optional
except GROQ_API_KEY.
| Variable | Default | Description |
|---|---|---|
SCRIBE_HOTKEY |
KEY_RIGHTCTRL |
Push-to-talk key, any name from evdev.ecodes. |
SCRIBE_MIN_DURATION_S |
0.3 |
Holds shorter than this are dropped as accidental taps. |
SCRIBE_MAX_DURATION_S |
unset | If set, recording auto-stops at this point. Unset = no limit. |
SCRIBE_MODEL |
whisper-large-v3-turbo |
Any Groq Whisper variant. |
SCRIBE_PROMPT |
empty | Comma-separated custom vocabulary โ words that often get transcribed wrong. |
SCRIBE_PASTE_MODE |
shortcut |
Paste mechanism (see Auto-paste modes). |
SCRIBE_NO_AUTO_PASTE |
0 |
Set to 1 to skip auto-paste and use the clipboard only. |
After running install.sh, the scribe command controls Scribe โ starting and
stopping it, checking its status, and managing custom vocabulary.
| Command | What it does |
|---|---|
scribe start |
Start Scribe. |
scribe stop |
Stop Scribe. |
scribe restart |
Restart Scribe. |
scribe status |
Show whether Scribe is running, plus recent activity. |
scribe test |
Record a clip and show the transcript. |
scribe list |
Show your custom vocabulary. |
scribe add <word>... |
Add words to your custom vocabulary. |
scribe remove <word>... |
Remove words from your custom vocabulary. |
scribe update |
Update Scribe to the latest version. |
scribe uninstall |
Uninstall Scribe (asks before removing anything). |
Wrap multi-word phrases in quotes so the shell treats them as one:
scribe add "Claude Code" Anthropic OAuth KubernetesIf ~/.local/bin isn't on your PATH, install.sh will tell you to add it to
your shell rc.
install.sh sets this up for you if you answer Yes to the autostart prompt.
To do it manually, create ~/.config/systemd/user/scribe.service:
[Unit]
Description=Scribe push-to-talk dictation
After=graphical-session.target sound.target
[Service]
Type=simple
WorkingDirectory=%h/Code/scribe
ExecStart=%h/Code/scribe/.venv/bin/python %h/Code/scribe/scribe.py
Restart=on-failure
RestartSec=3
[Install]
WantedBy=default.targetReplace the paths with your own. WorkingDirectory doesn't need to point at the
repo โ scribe.py loads ~/.config/scribe/.env directly. Then run:
systemctl --user daemon-reload
systemctl --user enable --now scribe