A secret manager with a GTK4 overlay UI and a full CLI. Secrets are encrypted locally with your GPG key and stored on a remote gRPC server. The server sees only encrypted blobs. Your private key never leaves your machine.
Decrypted values are protected in memory using Go 1.26's runtime/secret.Do, which zeroes registers, stack, and heap after use.
go install -tags runtimesecret github.com/bnema/sekeve/cmd/sekeve@latestOr build from source:
make build
make installThe GTK4 overlay (omnibox and PIN prompt) requires libgtk-4 and gtk4-layer-shell. Install via your package manager:
# Arch
sudo pacman -S gtk4 gtk4-layer-shell
# Debian / Ubuntu
sudo apt install libgtk-4-1 libgtk4-layer-shell0When gtk4-layer-shell is installed, sekeve automatically sets LD_PRELOAD and re-execs itself — no wrapper scripts needed. Without it, the overlay opens as a regular window. The CLI works either way.
Warning
The gRPC server uses plaintext HTTP/2 by default and does not terminate TLS. Do not expose it directly to the internet. Place it behind a reverse proxy that handles TLS, such as Caddy, nginx with a certificate, or Cloudflare proxy. All data on the wire is GPG-encrypted, but connection metadata and the auth handshake are not protected without TLS.
docker compose up -dOn first run, initialize the server with your GPG public key:
docker compose run -it sekeve-server server init --data /data/sekeve.dbThis opens an interactive prompt where you paste your armored GPG public key. The key is stored in the database; no file remains on disk.
The server stores the registered public key. If PIN unlock is enabled, it also stores the PIN hash and salt. It never stores the plaintext PIN.
You can also provide the key non-interactively:
# Via file
docker compose run sekeve-server server init --data /data/sekeve.db --pubkey-file /path/to/key.asc
# Via environment variable
SEKEVE_PUBKEY="$(gpg --export --armor you@example.com)" docker compose run sekeve-server server init --data /data/sekeve.db
# Via stdin
gpg --export --armor you@example.com | docker compose run -T sekeve-server server init --data /data/sekeve.dbsekeve server init --pubkey-file ./key.asc --data ./sekeve.db
sekeve server start --data ./sekeve.db --addr :50051Create a .env file (gitignored) to map the host port:
SEKEVE_SERVER_PORT=50053The container always listens on port 50051 internally.
Run the interactive setup:
sekeve initThis prompts for the server address and GPG key ID, writes $XDG_CONFIG_HOME/sekeve/config.toml (defaults to ~/.config/sekeve/config.toml), and verifies the server connection.
Both fields can be overridden with --server, --gpg-key flags or SEKEVE_SERVER_ADDR, SEKEVE_GPG_KEY_ID environment variables.
| Variable | Description | Default |
|---|---|---|
SEKEVE_LOG_LEVEL |
Log level: trace, debug, info, warn, error |
info |
SEKEVE_LOG_FORMAT |
Log output format: console, json |
console |
SEKEVE_SERVER_ADDR |
gRPC server address | localhost:50051 |
SEKEVE_GPG_KEY_ID |
GPG key ID for encryption/auth | (none) |
SEKEVE_PUBKEY |
Armored GPG public key for non-interactive server init | (none) |
# Add entries
sekeve add login github --site github.com --username user
sekeve add secret stripe-key sk_live_abc123
echo "some notes" | sekeve add note meeting-notes
# Retrieve and decrypt
sekeve get github
sekeve get stripe-key --json
# List and search
sekeve list
sekeve list --type login --json
sekeve search git
# Edit and delete
sekeve edit github
sekeve rm stripe-keysekeve omnibox opens a GTK4 overlay for searching, viewing, adding, and editing entries — all from one keyboard-driven window. It renders as a Wayland layer-shell surface, so it floats above your desktop without a title bar or window decorations.
Bind it to a hotkey in your compositor:
// niri
Mod+Ctrl+P hotkey-overlay-title="Sekeve" { spawn "sekeve" "omnibox"; }# sway / hyprland
bindsym $mod+Ctrl+p exec sekeve omniboxIf no session exists, a PIN prompt appears first. Press Escape on empty search to close the overlay.
List all entries in a picker-friendly format, then copy the selected value to clipboard:
sel=$(sekeve dmenu --list | fuzzel --dmenu --with-nth=1 --accept-nth=2) && sekeve dmenu --copy "$sel"When PIN is configured, use --ensure-session to authenticate before the picker opens (see PIN unlock below).
Logins copy the password, secrets copy the value, notes copy the full content.
Export your vault from Bitwarden as unencrypted JSON (bw export --format json), then import:
sekeve import bitwarden ~/bw-export.jsonLogins are imported with the username appended to the name (e.g., GitHub (alice@work.com)). URIs are normalized to strip paths while preserving subdomains. Secure notes are imported as-is. Cards, identities, and SSH keys are skipped.
Delete the export file after import - it contains plaintext credentials.
PIN unlock adds a second step to the GPG challenge-response flow. The client asks for a PIN only when the server requires one.
sekeve reuses a cached session token first. If none exists, it authenticates with GPG. If the server still requires a PIN, it prompts in the TTY or in the GTK overlay when no TTY is available.
The server stores the PIN as an argon2id hash and salt, requires the current PIN to change it, issues one-time unlock tickets, backs off after failed attempts, and invalidates all sessions when the PIN changes.
sekeve pin set # first-time setup; 4-6 digits
sekeve pin change # change an existing PINIn a terminal, the PIN is prompted interactively. When launched from a hotkey with no TTY (for example, a niri or sway keybinding), a GTK4 overlay prompt appears automatically via layer-shell.
For the dmenu workflow, use --ensure-session so the PIN prompt appears before fuzzel:
sekeve dmenu --ensure-session && sel=$(sekeve dmenu --list | fuzzel --dmenu) && sekeve dmenu --copy "$sel"Without --ensure-session, fuzzel opens simultaneously with the PIN prompt and steals focus. The omnibox handles this automatically.
GPG challenge-response establishes the session: the server encrypts a nonce with your public key, and the client decrypts it to prove identity. The client caches the session token locally for one hour.
make proto # regenerate protobuf
make test # run all tests
make lint # golangci-lint
make mock # regenerate mocks
make build # build binary
make wipe # delete all vault entries (requires jq)
make install # install to $GOBINRequires Go 1.26+, buf, mockery, golangci-lint, and gpg.