A GTK4 SSH-askpass in Zig

Hacker News Top Tools

Summary

A developer writes a lightweight GTK4-based ssh-askpass utility in Zig to avoid pulling in X11 or KDE dependencies on their hardened Gentoo system, using hand-written GTK4 bindings.

No content available
Original Article
View Cached Full Text

Cached at: 07/31/26, 04:59 PM

# A GTK4 ssh-askpass in Zig Source: [https://xn--gckvb8fzb.com/a-gtk4-ssh-askpass-in-zig/](https://xn--gckvb8fzb.com/a-gtk4-ssh-askpass-in-zig/) Every ssh\-askpass in the*Gentoo*tree pulls in*X11*or a full*KDE*stack, so I wrote my own with*Zig*0\.16 and*GTK4*, with hand\-written bindings that keep`X`out of the build\. I run[hardened*Gentoo*](https://xn--gckvb8fzb.com/hardened-gentoo-with-full-disk-encryption-on-the-star-labs-starbook-mk-vi/)on my laptop, and most of the time I never touch`ssh\-askpass`because I’m using`\-sk`keys for most of the systems\. There is one class of situation where I do need it, though, which is when a program wants an SSH key passphrase for a regular ED25519 key, but has no terminal to read it from\. The usual case is`go get`, or the`go`toolchain in general, fetching a private module over SSH during a build that runs without a TTY\.*OpenSSH*can’t prompt on a pipe, so it runs whatever`SSH\_ASKPASS`points at and puts the passphrase prompt in a window instead\. For years I had nothing installed for that and had to work around these scenarios\. The main reason for that is what*Gentoo*’s*Portage*offers: ```  ~ emerge -s ssh-askpass [ Results for search key : ssh-askpass ] Searching... * kde-plasma/ksshaskpass Latest version available: 6.6.6 Latest version installed: [ Not Installed ] Size of files: 32 KiB Homepage: https://kde.org/plasma-desktop https://invent.kde.org/plasma/ksshaskpass Description: Implementation of ssh-askpass with KDE Wallet integration License: GPL-2 * lxqt-base/lxqt-openssh-askpass Latest version available: 2.4.0 Latest version installed: [ Not Installed ] Size of files: 19 KiB Homepage: https://lxqt-project.org/ https://github.com/lxqt/lxqt-openssh-askpass/ Description: LXQt OpenSSH user password prompt tool License: LGPL-2.1 LGPL-2.1+ * net-misc/gnome-ssh-askpass Latest version available: 10.0_p1 Latest version installed: [ Not Installed ] Size of files: 1,927 KiB Homepage: https://www.openssh.com/ Description: GTK-based passphrase dialog for use with OpenSSH License: BSD GPL-2 * net-misc/ssh-askpass-fullscreen Latest version available: 1.2 Latest version installed: [ Not Installed ] Size of files: 81 KiB Homepage: https://github.com/atj/ssh-askpass-fullscreen Description: A small SSH Askpass replacement written with GTK2 License: GPL-2+ * net-misc/x11-ssh-askpass Latest version available: 1.2.4.1-r2 Latest version installed: [ Not Installed ] Size of files: 30 KiB Homepage: https://github.com/sigmavirus24/x11-ssh-askpass Description: X11-based passphrase dialog for use with OpenSSH License: HPND [ Applications found : 5 ] ``` Each of these has at least one inconvenience I didn’t feel like putting up with\. My system runs with the global`\-X`USE flag, so anything that needs*X11*is out before I look any further\. Of the five,`kde\-plasma/ksshaskpass`is the only one with**no***X11*dependency whatsoever, which should have made it the obvious pick, but the trouble is everything else that comes with it\. As a[Sway user](https://xn--gckvb8fzb.com/minimalist-and-functional-desktop-environment-without-xorg/), I did not want a full*KDE*stack on the machine just to type the occasional passphrase, and that is what a`ksshaskpass`install pulls in: ``` ~ # emerge -a kde-plasma/ksshaskpass These are the packages that would be merged, in order: Calculating dependencies... done! Dependency resolution took 1.18 s (backtrack: 0/20). [ebuild N ] kde-frameworks/kf-env-6 [ebuild N ] dev-libs/plasma-wayland-protocols-1.21.0 USE="-test" [ebuild N ] dev-libs/qtkeychain-0.16.0 USE="-keyring -test" [ebuild N ] kde-frameworks/ksecretd-services-6.27.0 USE="(-systemd)" [ebuild N ] kde-frameworks/breeze-icons-6.27.0 USE="-test" [ebuild N ] kde-frameworks/ki18n-6.27.0 USE="-debug -test" PYTHON_SINGLE_TARGET="python3_14 -python3_12 -python3_13" [ebuild N ] kde-frameworks/kconfig-6.27.0 USE="dbus -debug -qml -test" [ebuild N ] kde-frameworks/kcoreaddons-6.27.0 USE="dbus -debug -test" [ebuild N ] kde-frameworks/kwidgetsaddons-6.27.0 USE="-debug -designer -test" [ebuild N ] kde-frameworks/kdbusaddons-6.27.0 USE="-X -debug -test" [ebuild N ] kde-frameworks/kwindowsystem-6.27.0 USE="wayland -X -debug -test" [ebuild N ] app-crypt/qca-2.3.10-r1 USE="ssl -botan -debug -doc -examples -gcrypt -gpg -logger -nss -pkcs11 -sasl -softstore -test" [ebuild N ] kde-frameworks/karchive-6.27.0 USE="crypt zstd -debug -test" [ebuild N ] kde-frameworks/kguiaddons-6.27.0 USE="dbus wayland -X -debug -test" [ebuild N ] kde-frameworks/kdoctools-6.27.0 USE="nls -debug -test" [ebuild N ] kde-frameworks/kcolorscheme-6.27.0 USE="-debug" [ebuild N ] kde-frameworks/kcrash-6.27.0 USE="-X -debug -test" [ebuild N ] kde-frameworks/knotifications-6.27.0 USE="-debug" [ebuild N ] kde-frameworks/kservice-6.27.0 USE="man -debug -test" [ebuild N ] kde-frameworks/kwallet-6.27.0 USE="-debug -minimal -test" [ebuild N ] kde-frameworks/kwallet-runtime-6.27.0 USE="(keyring) (legacy-kwallet) man -X -debug -gpg -test" [ebuild N ] kde-plasma/ksshaskpass-6.6.6 USE="-debug" Would you like to merge these packages? [Yes/No] ``` `lxqt\-base/lxqt\-openssh\-askpass`is next, and it needs`X`outright\. On top of that it pulls in a few*KDE*framework packages and a Qt built with`X`support, which collides with the`dev\-qt/qtbase`already on my system that was compiled`\-X`, so Portage stops on a slot conflict: ``` ~ # emerge -a lxqt-base/lxqt-openssh-askpass These are the packages that would be merged, in order: Calculating dependencies... done! Dependency resolution took 3.10 s (backtrack: 0/20). [ebuild R ] x11-libs/libxkbcommon-1.13.2 USE="X*" [ebuild N ] dev-util/lxqt-build-tools-2.4.0 [ebuild N ] x11-libs/xcb-util-renderutil-0.3.10 ABI_X86="(64) -32 (-x32)" [ebuild N ] x11-libs/libXScrnSaver-1.2.5 USE="-doc" ABI_X86="(64) -32 (-x32)" [ebuild N ] dev-libs/plasma-wayland-protocols-1.21.0 USE="-test" [ebuild N ] x11-libs/xcb-util-wm-0.4.2 ABI_X86="(64) -32 (-x32)" [ebuild N ] x11-libs/xcb-util-0.4.1 ABI_X86="(64) -32 (-x32)" [ebuild N ] x11-libs/xcb-util-image-0.4.1 ABI_X86="(64) -32 (-x32)" [ebuild N ] x11-libs/xcb-util-cursor-0.1.6 ABI_X86="(64) -32 (-x32)" [ebuild R ] dev-qt/qtbase-6.11.1 USE="X*" [ebuild N ] sys-auth/polkit-qt-0.201.1 [ebuild N ] dev-libs/libqtxdg-4.4.0 USE="-test" [ebuild N ] kde-frameworks/kf-env-6 [ebuild N ] kde-frameworks/kwindowsystem-6.27.0 USE="X wayland -debug -test" [ebuild N ] lxqt-base/liblxqt-2.4.0 USE="backlight" [ebuild N ] lxqt-base/lxqt-openssh-askpass-2.4.0 !!! Multiple package instances within a single package slot have been pulled !!! into the dependency graph, resulting in a slot conflict: dev-qt/qtbase:6 (dev-qt/qtbase-6.11.1:6/6.11.1::gentoo, installed) USE="concurrent dbus gui icu libinput libproxy network nls opengl sql sqlite ssl syslog udev vulkan wayland widgets xml (zstd) -X -accessibility -brotli -cups -custom-cflags -eglfs -evdev -gles2-only -gssapi -gtk -io-uring (-journald) -mysql -oci8 -odbc -postgres -renderdoc -sctp -test -tslib" ABI_X86="(64)" pulled in by ~dev-qt/qtbase-6.11.1:6[X=,concurrent,eglfs=] required by (dev-qt/qtmultimedia-6.11.1:6/6.11.1::gentoo, installed) USE="alsa dbus ffmpeg gstreamer opengl pipewire qml v4l vaapi vulkan wayland -X -custom-cflags -eglfs -pulseaudio -test" ABI_X86="(64)" ^^ (dev-qt/qtbase-6.11.1:6/6.11.1::gentoo, ebuild scheduled for merge) USE="X concurrent dbus gui icu libinput libproxy network nls opengl sql sqlite ssl syslog udev vulkan wayland widgets xml (zstd) -accessibility -brotli -cups -custom-cflags -eglfs -evdev -gles2-only -gssapi -gtk -io-uring (-journald) -mysql -oci8 -odbc -postgres -renderdoc -sctp -test -tslib" ABI_X86="(64)" pulled in by >=dev-qt/qtbase-6.10.1:6=[gui,X] required by (kde-frameworks/kwindowsystem-6.27.0:6/6.27::gentoo, ebuild scheduled for merge) USE="X wayland -debug -test" ABI_X86="(64)" ^ It may be possible to solve this problem by using package.mask to prevent one of those packages from being selected. However, it is also possible that conflicting dependencies exist such that they are impossible to satisfy simultaneously. If such a conflict exists in the dependencies of two different packages, then those packages can not be installed simultaneously. For more information, see MASKED PACKAGES section in the emerge man page or refer to the Gentoo Handbook. The following USE changes are necessary to proceed: (see "package.use" in the portage(5) man page for more details) # required by lxqt-base/liblxqt-2.4.0::gentoo # required by lxqt-base/lxqt-openssh-askpass-2.4.0::gentoo # required by lxqt-base/lxqt-openssh-askpass (argument) >=kde-frameworks/kwindowsystem-6.27.0 X # required by kde-frameworks/kwindowsystem-6.27.0::gentoo[X] # required by lxqt-base/liblxqt-2.4.0::gentoo # required by lxqt-base/lxqt-openssh-askpass-2.4.0::gentoo # required by lxqt-base/lxqt-openssh-askpass (argument) >=dev-qt/qtbase-6.11.1 X # required by dev-qt/qtbase-6.11.1::gentoo[gui] # required by kde-frameworks/kwindowsystem-6.27.0::gentoo[X] # required by lxqt-base/liblxqt-2.4.0::gentoo # required by lxqt-base/lxqt-openssh-askpass-2.4.0::gentoo # required by lxqt-base/lxqt-openssh-askpass (argument) >=x11-libs/libxkbcommon-1.13.2 X Would you like to add these changes to your config files? [Yes/No] ``` `net\-misc/ssh\-askpass\-fullscreen`needs`X`as well, this time by way of*GTK2*and a Cairo built with`X`support: ``` ~ # emerge -a net-misc/ssh-askpass-fullscreen These are the packages that would be merged, in order: Calculating dependencies... done! Dependency resolution took 2.98 s (backtrack: 0/20). [ebuild R ] x11-libs/cairo-1.18.4-r1 USE="X*" [ebuild N ] x11-libs/libXcomposite-0.4.7 USE="-doc" ABI_X86="(64) -32 (-x32)" [ebuild N ] x11-libs/libXcursor-1.2.3 USE="-doc" ABI_X86="(64) -32 (-x32)" [ebuild N ] x11-libs/libXdamage-1.1.7 ABI_X86="(64) -32 (-x32)" [ebuild N ] x11-libs/libXi-1.8.3 USE="-doc" ABI_X86="(64) -32 (-x32)" [ebuild N ] x11-themes/gnome-themes-standard-3.28-r1 [ebuild NS ] x11-libs/gtk+-2.24.33-r3 [3.24.52] [ebuild N ] x11-themes/gtk-engines-adwaita-3.28-r1 ABI_X86="(64) -32 (-x32)" [ebuild N ] net-misc/ssh-askpass-fullscreen-1.2 The following USE changes are necessary to proceed: (see "package.use" in the portage(5) man page for more details) # required by x11-libs/gtk+-2.24.33-r3::gentoo # required by x11-themes/gtk-engines-adwaita-3.28-r1::gentoo >=x11-libs/cairo-1.18.4-r1 X Would you like to add these changes to your config files? [Yes/No] ``` `net\-misc/x11\-ssh\-askpass`is*X11*by name, so no surprise there, and it also needs the old imake build system, namely`x11\-misc/xorg\-cf\-files`and`x11\-misc/imake`, to compile at all: ``` ~ # emerge -a net-misc/x11-ssh-askpass These are the packages that would be merged, in order: Calculating dependencies... done! Dependency resolution took 1.00 s (backtrack: 0/20). [ebuild N ] app-text/rman-3.2-r2 [ebuild N ] x11-misc/xorg-cf-files-1.0.9 [ebuild N ] x11-misc/imake-1.0.10 [ebuild N ] net-misc/x11-ssh-askpass-1.2.4.1-r2 Would you like to merge these packages? [Yes/No] ``` That left`net\-misc/gnome\-ssh\-askpass`\. At first glance it looked like the one option that needed no`X`at all, but that turned out to be wrong\. It does need*X11*, and the ebuild appears to be broken about it\. The build calls`pkg\-config \-\-libs gtk\+\-3\.0 x11`and the source includes`gdk/gdkx\.h`, an`X`\-only GDK header, so on a system compiled without`X`it fails to build: ``` ~ # emerge -a net-misc/gnome-ssh-askpass These are the packages that would be merged, in order: Calculating dependencies... done! Dependency resolution took 0.89 s (backtrack: 0/20). [ebuild N ~] net-misc/gnome-ssh-askpass-10.0_p1 USE="-verify-sig" Would you like to merge these packages? [Yes/No] Yes >>> Verifying ebuild manifests >>> Emerging (1 of 1) net-misc/gnome-ssh-askpass-10.0_p1::guru >>> Failed to emerge net-misc/gnome-ssh-askpass-10.0_p1, Log file: >>> '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/temp/build.log' >>> Jobs: 0 of 1 complete, 1 failed Load avg: 0.79, 0.57, 0.61 * Package: net-misc/gnome-ssh-askpass-10.0_p1:0 * Repository: guru * USE: abi_x86_64 amd64 elibc_glibc kernel_linux * FEATURES: network-sandbox preserve-libs sandbox selinux sesandbox userpriv usersandbox >>> Unpacking source... >>> Unpacking 'openssh-10.0p1.tar.gz' to /var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work >>> Source unpacked in /var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work >>> Preparing source in /var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work/openssh-10.0p1/contrib ... >>> Source prepared. >>> Configuring source in /var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work/openssh-10.0p1/contrib ... >>> Source configured. >>> Compiling source in /var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work/openssh-10.0p1/contrib ... make -j17 -l15.95 gnome-ssh-askpass3 x86_64-pc-linux-gnu-gcc -O2 -pipe -march=native `pkg-config --cflags gtk+-3.0` \ gnome-ssh-askpass3.c -o gnome-ssh-askpass3 \ `pkg-config --libs gtk+-3.0 x11` gnome-ssh-askpass3.c:62:10: fatal error: gdk/gdkx.h: No such file or directory 62 | #include <gdk/gdkx.h> | ^~~~~~~~~~~~ compilation terminated. make: *** [Makefile:17: gnome-ssh-askpass3] Error 1 * ERROR: net-misc/gnome-ssh-askpass-10.0_p1::guru failed (compile phase): * emake failed * * If you need support, post the output of `emerge --info '=net-misc/gnome-ssh-askpass-10.0_p1::guru'`, * the complete build log and the output of `emerge -pqv '=net-misc/gnome-ssh-askpass-10.0_p1::guru'`. * The complete build log is located at '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/temp/build.log'. * The ebuild environment file is located at '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/temp/environment'. * Working directory: '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work/openssh-10.0p1/contrib' * S: '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work/openssh-10.0p1/contrib' * Messages for package net-misc/gnome-ssh-askpass-10.0_p1: * ERROR: net-misc/gnome-ssh-askpass-10.0_p1::guru failed (compile phase): * emake failed * If you need support, post the output of `emerge --info '=net-misc/gnome-ssh-askpass-10.0_p1::guru'`, * the complete build log and the output of `emerge -pqv '=net-misc/gnome-ssh-askpass-10.0_p1::guru'`. * The complete build log is located at '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/temp/build.log'. * The ebuild environment file is located at '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/temp/environment'. * Working directory: '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work/openssh-10.0p1/contrib' * S: '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/work/openssh-10.0p1/contrib' * * The following package has failed to build, install, or execute postinst: * * (net-misc/gnome-ssh-askpass-10.0_p1:0/0::guru, ebuild scheduled for merge), Log file: * '/var/tmp/portage/net-misc/gnome-ssh-askpass-10.0_p1/temp/build.log' * ``` This is where I gave up on the packaged options\. Even setting the*X11*question aside, every one of these uses*GTK2*or*GTK3*at most\. However, it just so happened that I had wanted to build something with*GTK4*for a long time, so instead of patching one of the existing implementations, which are mostly C anyway, I wrote my own with*Zig*0\.16 and*GTK4*, and called it[ssh\-askpass\-zigtk](https://tty.fail/mrus/ssh-askpass-zigtk)\. ### Avoiding*X11* The reason the*GTK*helpers break on my system is the headers\. The standard way of calling*GTK*includes the*GTK4*headers, which pull in*GDK*, and*GDK*still ships`gdk/gdkx\.h`on most installs, so an*X11*header comes in whether you want it or not\.*Zig*’s`@cImport`, the obvious way to call a C library, would do the same, because it pulls in exactly those headers\. So`ssh\-askpass\-zigtk`doesn’t`@cImport`anything\.`src/gtk\.zig`declares the thirty\-odd*GTK*and*GLib*functions the program calls by hand, as plain`extern`prototypes: ``` pub const Widget = opaque {}; pub extern fn gtk_window_new() *Widget; pub extern fn gtk_password_entry_new() *Widget; pub extern fn gtk_editable_get_text(editable: *Widget) [*:0]const u8; pub extern fn gtk_button_new_with_label(label: [*:0]const u8) *Widget; ``` Nothing in that file names a symbol from`gdk/gdkx\.h`or`X11/Xlib\.h`, so the compiler never sees an`X`header, and the binary builds and runs against a*GTK4*that was compiled without*X11*\. The one`X`\-adjacent value it needs, the*Escape*keysym, is hardcoded as`0xff1b`rather than pulled from`gdk/gdkkeysyms\.h`\. *GTK*is built on*GObject*, which does single inheritance by putting the parent struct as the first member of the child, so a window, a box, a label, a password entry and a button are all layout\-compatible with a`GtkWidget \*`at the ABI boundary\. On the*Zig*side one`opaque \{\}`type stands in for all of them, and every widget function takes and returns the same`\*Widget`, without a hierarchy of wrapper types to model something the C ABI already flattens\. The parts that don’t touch*GTK*, the mapping of`SSH\_ASKPASS\_PROMPT`to a dialog type and the parsing of the`GNOME\_SSH\_ASKPASS\_\*\_COLOR`variables, are in`src/root\.zig`with unit tests, so they run under`zig build test`with no display and no*GTK*at all\. Recoloring goes through a small CSS provider, since*GTK4*removed`gtk\_widget\_modify\_fg`and`\_bg`\. ### Cross\-compiling Because the bindings are hand\-written externs and no*GTK*headers enter the build,*Zig*can cross\-compile the binary for any Linux architecture without a*GTK4*toolchain for that target\. The only thing missing at link time is the*GTK4*shared library itself, and`\-Dgtk\-stub`covers that, as it builds a tiny stub`libgtk\-4\.so\.1`whose exported symbols are all no\-ops, links the executable against that, and lets the target’s real*GTK4*resolve at runtime instead\. The release workflow uses this to produce binaries for`x86\_64`,`aarch64`,`armv7`,`riscv64`,`powerpc64le`,`i386`,`loongarch64`and`s390x`from one machine, none of which has*GTK4*installed for the other seven\. **Note:**`ssh\-askpass\-zigtk`doesn’t grab the keyboard as other*askpass*implementations normally would\. The*GTK3*helper calls`gdk\_seat\_grab`so another client can’t read the passphrase as you type it, but from what I see,*GTK4*dropped that interface and I believe that Wayland doesn’t let a client grab the keyboard at all, so there is no portable way to do it without*X11*\. Hence the`GNOME\_SSH\_ASKPASS\_GRAB\_SERVER`and`GNOME\_SSH\_ASKPASS\_GRAB\_POINTER`variables also have no effect\. The code is on[tty\.fail](https://tty.fail/mrus/ssh-askpass-zigtk)and mirrored to[GitHub](https://github.com/mrusme/ssh-askpass-zigtk), where each tagged release ships prebuilt Linux binaries per architecture\. To use it, put the binary somewhere on your`PATH`and point`SSH\_ASKPASS`at it\. For a terminal that means two lines in`~/\.profile`or your shell’s startup file \(e\.g\.`~/\.zshrc`for my[fellow Zsh users](https://tty.fail/mrus/dotfiles)\): ``` export SSH_ASKPASS=~/.local/bin/ssh-askpass-zigtk export SSH_ASKPASS_REQUIRE=prefer ``` `SSH\_ASKPASS\_REQUIRE=prefer`, from*OpenSSH*8\.4 onward, tells*OpenSSH*to use the dialog even when a terminal is available, as long as a graphical session is present\. On a systemd user session, the same two variables go in`~/\.config/environment\.d/ssh\-askpass\.conf`as plain`KEY=VALUE`lines with an absolute path, since that file neither expands`~`nor runs a shell\. Log out and back in, and the next`ssh\-add`,`git`pull or`go get`that needs a passphrase without a terminal gets the dialog\.

Similar Articles

Ziggity – A terminal UI for Git, written in Zig

Hacker News Top

Ziggity is a terminal UI for Git, inspired by lazygit, written in Zig. It provides a keyboard-driven interface for staging, committing, branching, rebasing, and inspecting history.

We Rewrote the Ghostty GTK Application

Mitchell Hashimoto

Mitchell Hashimoto details the rewrite of Ghostty's GTK application to fully embrace the GObject type system from Zig, improving stability, features, and memory safety, verified with Valgrind.

Gooey: A GPU-accelerated UI framework for Zig

Hacker News Top

Gooey is a GPU-accelerated UI framework for Zig, targeting macOS, Linux, and browser via Metal, Vulkan/Wayland, and WebGPU/WASM. It offers declarative UI, animations, theming, accessibility, and zero external dependencies.

What Zig felt like, coming from Rust

Lobsters Hottest

An experienced Rust developer shares their journey of learning Zig by reimplementing a JSONPath tool, highlighting differences in IDE support and code organization.