A minimal TOTP authenticator for Linux. A background daemon keeps your secrets encrypted on disk and generates codes on demand. A CLI lets you import accounts and request clipboard copies. An optional Quickshell panel gives you a searchable visual interface with live countdown timers.
go build -o totp .Optional install location:
mkdir -p ~/.local/bin
cp ./totp ~/.local/bin/totpMake sure ~/.local/bin is in your PATH.
./totp daemonOn the very first run you will be asked once:
Set a passphrase for your master key (press Enter to generate one randomly):
- Press Enter — a random key is generated for you. Nothing to remember.
- Type a passphrase — the key is derived from your passphrase and stored. The passphrase itself is never saved and will never be asked again.
After this one-time setup the daemon starts silently on every subsequent run.
See Importing accounts below.
# list your accounts to find the ID
./totp list
# generate the code for an account and copy it to the clipboard
./totp copy <account-id>The daemon can copy codes directly to your clipboard. Because of that, it must run inside your active graphical session.
Do not start this daemon from default.target.
This is wrong:
[Install]
WantedBy=default.targetdefault.target starts too early. At that point your compositor may not have created the session environment yet, so variables like these may be missing:
WAYLAND_DISPLAY
DISPLAY
XDG_RUNTIME_DIR
DBUS_SESSION_BUS_ADDRESS
XDG_CURRENT_DESKTOP
XDG_SESSION_TYPE
If those variables are missing, clipboard copying will fail with an error like:
no clipboard backend available: WAYLAND_DISPLAY and DISPLAY are both unset
The correct setup is to start the daemon from a compositor/session-owned systemd user target.
This setup is compositor-agnostic. It works with Hyprland, Sway, River, Niri, Wayfire, Labwc, custom X11 sessions, or desktop environments, as long as you run the startup commands after the graphical session has started.
Create:
~/.config/systemd/user/totp-session.target
with:
[Unit]
Description=TOTP graphical sessionThis target represents the lifetime of the graphical session that owns the clipboard.
Create:
~/.config/systemd/user/totp.service
with:
[Unit]
Description=TOTP authenticator daemon
After=totp-session.target
PartOf=totp-session.target
[Service]
ExecStart=%h/.local/bin/totp daemon
Restart=on-failure
RestartSec=3
[Install]
WantedBy=totp-session.targetIf your binary is somewhere else, change:
ExecStart=%h/.local/bin/totp daemonto the correct absolute path.
systemctl --user daemon-reload
systemctl --user enable totp.serviceDo not use --now here.
This service should not start immediately. It should start only when totp-session.target starts.
Your compositor or desktop session needs to run these commands after the graphical session environment exists:
systemctl --user import-environment WAYLAND_DISPLAY DISPLAY XDG_RUNTIME_DIR XDG_CURRENT_DESKTOP XDG_SESSION_TYPE DBUS_SESSION_BUS_ADDRESS
dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY XDG_RUNTIME_DIR XDG_CURRENT_DESKTOP XDG_SESSION_TYPE DBUS_SESSION_BUS_ADDRESS
systemctl --user start totp-session.targetIf your compositor supports a shutdown/exit hook, also run this when the compositor exits:
systemctl --user stop totp-session.targetThat makes the daemon stop when the graphical session stops.
The startup sequence should be:
compositor starts
-> compositor creates WAYLAND_DISPLAY or DISPLAY
-> environment is imported into the user systemd manager
-> totp-session.target starts
-> totp.service starts
Add to ~/.config/hypr/hyprland.conf:
exec-once = systemctl --user import-environment WAYLAND_DISPLAY DISPLAY XDG_RUNTIME_DIR XDG_CURRENT_DESKTOP XDG_SESSION_TYPE DBUS_SESSION_BUS_ADDRESS
exec-once = dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY XDG_RUNTIME_DIR XDG_CURRENT_DESKTOP XDG_SESSION_TYPE DBUS_SESSION_BUS_ADDRESS
exec-once = systemctl --user start totp-session.target
exec-shutdown = systemctl --user stop totp-session.targetThen restart Hyprland. Reloading the config is not enough for exec-once.
Add to ~/.config/sway/config:
exec systemctl --user import-environment WAYLAND_DISPLAY DISPLAY XDG_RUNTIME_DIR XDG_CURRENT_DESKTOP XDG_SESSION_TYPE DBUS_SESSION_BUS_ADDRESS
exec dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY XDG_RUNTIME_DIR XDG_CURRENT_DESKTOP XDG_SESSION_TYPE DBUS_SESSION_BUS_ADDRESS
exec systemctl --user start totp-session.targetSway does not provide the same universal shutdown hook as Hyprland. If you need the daemon to stop strictly when Sway exits, start Sway from a wrapper script and stop the target after Sway exits.
Example wrapper:
#!/usr/bin/env bash
set -e
sway
systemctl --user stop totp-session.targetUse that wrapper instead of launching sway directly.
Put this in whatever autostart mechanism your compositor provides:
systemctl --user import-environment WAYLAND_DISPLAY DISPLAY XDG_RUNTIME_DIR XDG_CURRENT_DESKTOP XDG_SESSION_TYPE DBUS_SESSION_BUS_ADDRESS
dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY XDG_RUNTIME_DIR XDG_CURRENT_DESKTOP XDG_SESSION_TYPE DBUS_SESSION_BUS_ADDRESS
systemctl --user start totp-session.targetIf the compositor has an exit hook, run:
systemctl --user stop totp-session.targeton exit.
For X11, DISPLAY is the important variable.
Run this after the X11 session starts:
systemctl --user import-environment DISPLAY XDG_RUNTIME_DIR XDG_CURRENT_DESKTOP XDG_SESSION_TYPE DBUS_SESSION_BUS_ADDRESS
dbus-update-activation-environment --systemd DISPLAY XDG_RUNTIME_DIR XDG_CURRENT_DESKTOP XDG_SESSION_TYPE DBUS_SESSION_BUS_ADDRESS
systemctl --user start totp-session.targetIf your window manager has an exit hook, stop the target there:
systemctl --user stop totp-session.targetSome desktop environments already manage graphical-session.target. Some do not expose the exact behavior clearly to user services. The most reliable setup is still the explicit totp-session.target shown above.
Use your desktop environment's startup applications UI to run:
systemctl --user import-environment WAYLAND_DISPLAY DISPLAY XDG_RUNTIME_DIR XDG_CURRENT_DESKTOP XDG_SESSION_TYPE DBUS_SESSION_BUS_ADDRESS && dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY XDG_RUNTIME_DIR XDG_CURRENT_DESKTOP XDG_SESSION_TYPE DBUS_SESSION_BUS_ADDRESS && systemctl --user start totp-session.targetIf your desktop environment supports logout scripts, run:
systemctl --user stop totp-session.targeton logout.
After logging into your graphical session, check the imported environment:
systemctl --user show-environment | grep -E 'WAYLAND|DISPLAY|XDG|DBUS'On Wayland, you should see something like:
XDG_RUNTIME_DIR=/run/user/1000
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
WAYLAND_DISPLAY=wayland-1
XDG_CURRENT_DESKTOP=Hyprland
XDG_SESSION_TYPE=wayland
On X11, you should see something like:
DISPLAY=:0
XDG_SESSION_TYPE=x11
Check the target:
systemctl --user status totp-session.targetCheck the daemon:
systemctl --user status totp.serviceFollow daemon logs:
journalctl --user -u totp.service -fYou probably enabled it under default.target.
Check:
find ~/.config/systemd/user -type l -name 'totp.service' -o -name 'totp.path'If you see something like:
~/.config/systemd/user/default.target.wants/totp.service
disable it:
systemctl --user disable --now totp.serviceThen enable it again using the correct target:
systemctl --user enable totp.serviceExpected symlink:
~/.config/systemd/user/totp-session.target.wants/totp.service
Not:
~/.config/systemd/user/default.target.wants/totp.service
Your compositor has the variable, but systemd does not.
Run:
systemctl --user show-environment | grep WAYLAND_DISPLAYIf nothing appears, your autostart did not import the environment correctly.
Run this from inside the graphical session:
systemctl --user import-environment WAYLAND_DISPLAY DISPLAY XDG_RUNTIME_DIR XDG_CURRENT_DESKTOP XDG_SESSION_TYPE DBUS_SESSION_BUS_ADDRESS
dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY XDG_RUNTIME_DIR XDG_CURRENT_DESKTOP XDG_SESSION_TYPE DBUS_SESSION_BUS_ADDRESS
systemctl --user restart totp-session.targetThen check again:
systemctl --user show-environment | grep -E 'WAYLAND|DISPLAY|XDG|DBUS'That is normal on many standalone compositor setups.
You may see:
systemctl --user status graphical-session.targetreturn:
Active: inactive (dead)
Do not rely on it unless your desktop environment or session manager starts it correctly.
Also, do not try to start it manually. Some systems refuse manual start/stop of graphical-session.target.
Use totp-session.target instead.
Check that the clipboard tool exists.
For Wayland:
command -v wl-copyFor X11:
command -v xclipInstall the missing tool.
Arch Linux:
sudo pacman -S wl-clipboard xclipDebian / Ubuntu:
sudo apt install wl-clipboard xclipFedora:
sudo dnf install wl-clipboard xclipYour accounts and master key are stored in your home directory:
| File | Contents |
|---|---|
~/.local/share/totp/accounts.enc |
Encrypted account store |
~/.local/share/totp/master.key |
Master key readable only by your user |
Both paths respect XDG_DATA_HOME if set.
Your TOTP secrets are encrypted at rest and never leave the daemon.
./totp import-image ~/Downloads/qr.pngBoth Google Authenticator export QR codes and standard single-account QR codes are supported.
This requires zbarimg to be installed.
Arch Linux:
sudo pacman -S zbarDebian / Ubuntu:
sudo apt install zbar-toolsFedora:
sudo dnf install zbarmacOS with Homebrew:
brew install zbarIf you have an otpauth://totp/ URI, for example from a website's manual entry option, import it directly:
./totp import-text 'otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example'Importing is safe to run multiple times. Existing accounts are never deleted. If the same account is imported again, it is updated in place.
totp daemon # start the daemon in the foreground
totp status # check that the daemon is alive
totp list # list accounts as JSON
totp import-image <path> # import from a QR code image
totp import-text <uri> # import from an otpauth:// URI
totp copy <id> # generate a code and copy it to the clipboardtotp copy asks the daemon to generate a code and copy it to the clipboard.
The daemon chooses a clipboard backend from the environment it received when it started:
| Session detected | Tool used |
|---|---|
WAYLAND_DISPLAY is set |
wl-copy |
DISPLAY is set |
xclip |
| Neither variable is set | error |
Because clipboard access belongs to the graphical session, the daemon must be started after the compositor or desktop session has created the correct environment.
A visual panel is included in:
ui/shell.qml
It requires Quickshell.
qs -p /path/to/totp/uiThe panel opens on the right side of the screen and shows all your accounts with a live countdown arc. Type to filter by name, use arrow keys or j/k to navigate, and press Enter, Space, or click to copy a code.
The panel closes automatically after copying.
The Quickshell panel is optional. The daemon and CLI work fully on their own.
| Dependency | Required for |
|---|---|
| Go 1.21+ | building the project |
zbarimg from zbar |
importing QR code images |
wl-copy from wl-clipboard |
clipboard on Wayland |
xclip |
clipboard on X11 |
zenity |
file picker in the Quickshell panel, optional |
| Quickshell | the visual panel, optional |
