A lispy web app runner with simple security (main-branch-only mirror of https://forge.ejuarezg.com/ejuarezg/tailcat-wormhole)
README.md

CL-GTK4 Web App Runner #

A small Common Lisp/WebKitGTK runner built with cl-gtk4. It opens specified HTTP(S) URLs in standalone GTK windows.

Terminology #

  • A runner is one running web-app-runner process. It owns one PIN-gated browsing session and its controls apply to that process.
  • A runner window is one GTK browser window created by that runner. A command with multiple URLs creates multiple runner windows. The toolbar controls that affect privacy are synchronized across them.
  • A runner profile is the target-specific WebKitGTK data directory used by a runner. It holds that target's cookies, local storage, IndexedDB, cache, and other website data. It is not a browser profile shared with Firefox, Chrome, or other WebKit applications.

Requirements #

Runtime dependencies #

Both the checkout launcher and standalone executable require:

  • GTK 4 runtime libraries;
  • WebKitGTK 6.0 runtime libraries and WebKit's helper processes;
  • GLib/GIO and GObject Introspection runtime libraries;
  • GTK 4 and WebKitGTK 6.0 GIR typelibs;
  • a working Wayland or X11 desktop session.

On Fedora:

sudo dnf install gtk4 webkitgtk6.0 gobject-introspection

Checkout development dependencies #

Running from a checkout additionally requires:

  • SBCL;
  • Quicklisp;
  • the Lisp systems cl-gtk4, cl-gtk4.webkit, ironclad, and cffi.

The upstream cl-gtk4 README describes installing its local projects and Quicklisp dependencies.

Optional password credentials #

The Credentials UI additionally requires:

  • pass;
  • GnuPG, configured with a usable key and agent;
  • optionally Tomb if PASSWORD_STORE_DIR points into a mounted Tomb.

Tomb is not required by the runner and is not opened automatically. The standalone executable does not require SBCL or Quicklisp at runtime.

Run #

./bin/web-app-runner
./bin/web-app-runner photos.ejuarezg.com https://example.com

On first launch, the app asks you to create a six-digit PIN. Subsequent launches require it before any website is opened, then proceed directly to the requested site. Five failed attempts disable entry for 30 seconds. Without a URL, the app then shows a small prompt for a website. A scheme-less address gets https:// automatically. Each supplied address opens in its own window. The title-bar toolbar follows the desktop light/dark theme and provides back, forward, reload/stop, an editable address field, a Credentials button when pass is installed, a Keep unlocked toggle, a Lock button, and a Clear runner data on close checkbox at the right. The runner automatically locks after five minutes without input; enable Keep unlocked for a website window that should not trigger that inactivity lock.

Lock covers every runner window with an opaque PIN prompt while retaining its live WebKit views. Unlocking returns to the same page state without a reload. Its navigation controls and address field are disabled while locked. Press Ctrl+Shift+L to lock manually, or wait five minutes without input for automatic locking. Press Ctrl+L to focus and select the current window's address field. When available, Ctrl+Shift+P opens credentials for the active browser window. These are convenience privacy controls, not protection against someone who can access your unlocked OS account or inspect the running process.

Cookies, site data, and PIN #

Non-session cookies are retained in SQLite at:

$XDG_DATA_HOME/web-app-runner/profiles/<target>/cookies.sqlite
# or ~/.local/share/web-app-runner/profiles/<target>/cookies.sqlite

The file is sensitive: a copied profile may carry authenticated web sessions. The runner enforces owner-only (0700) permissions on its data/cache directories and 0600 on the PIN verifier, but does not encrypt them. The runner assigns each target host its own WebKit data/cache profile, separate from other WebKitGTK applications and other runner targets. Selecting Clear runner data on close changes only the current target profile's cleanup policy; it does not reload or alter the current page. After the final runner window closes, the runner clears that profile's cookies, local storage, IndexedDB, caches, service workers, and other WebKit data.

The PIN is never stored directly. A salted PBKDF2-SHA256 verifier is stored in $XDG_DATA_HOME/web-app-runner/pin-verifier; it is a local screen gate, not protection against someone who can read your profile. If that verifier is missing when the runner starts, it deletes all runner profile data and cache before allowing a replacement PIN to be set. Deleting the verifier therefore intentionally signs out and removes saved runner website data.

./bin/web-app-runner --help

The launcher expects Quicklisp at ~/.local/share/quicklisp/setup.lisp. Override that location when necessary:

QUICKLISP_SETUP=~/quicklisp/setup.lisp ./bin/web-app-runner example.com

Build an executable #

./bin/build-web-app-runner

This creates ./web-app-runner (currently about 63 MB). It contains SBCL and all Lisp code, so running it does not require Quicklisp, the checkout, or the shell launcher. It still dynamically requires the system GTK4/WebKitGTK shared libraries and typelibs; it is not a statically linked bundle.

The build helper uses QUICKLISP_SETUP in the same way as the development launcher when the default Quicklisp location is not appropriate.

Tests #

sbcl --noinform --non-interactive \
  --load "$HOME/.local/share/quicklisp/setup.lisp" \
  --eval "(asdf:load-asd #P\"$(pwd)/clgtk4-web-app-runner.asd\")" \
  --eval '(asdf:test-system :clgtk4-web-app-runner/tests)'

The suite covers URL/profile-key parsing and profile path scoping, including removal of temporary profile data/cache roots when the PIN verifier is absent.

Desktop entry #

data/com.ejuarezg.web-app-runner.desktop is the Freedesktop desktop entry for an installed web-app-runner executable on PATH. Install it to ~/.local/share/applications/ for a per-user launcher, or to the corresponding system applications directory when packaging the app.

Development disclosure #

This project has been developed with substantial assistance from large language models (LLMs), a form of probabilistic automation. Such tools can produce plausible but incorrect code or explanations; their output does not establish that behavior is correct. The human maintainer is responsible for reviewing changes and validating them with tests.

This wording follows GNOME's “Probabilistically Automated” label and its focus on describing how work was produced without treating a model as a human-like agent. For more on precise, non-anthropomorphic language, see “We Need to Talk About How We Talk About ‘AI’”.

License #

Copyright © Ezequiel Juarez Garcia. This project is licensed under the GNU Affero General Public License, version 3 or later. See LICENSE.

Password-store integration #

The optional Credentials UI uses pass and GnuPG rather than reimplementing credential encryption. The runner checks for pass at startup; when it is unavailable, the Credentials icon and shortcut are omitted. Tomb may hold .password-store, but the runner does not unlock that Tomb automatically.

Entries use the web-app-runner/<hostname> convention. The Credentials window supports editable username/password fields, a Show password toggle, a Save / overwrite action, and explicit copy actions. Saved entries use the same convention. Copied values are replaced with an empty clipboard value after 15 seconds, with a visible countdown. This is best-effort only; clipboard managers may retain history. The six-digit runner PIN is not an encryption key.

See docs/pass-tomb-plan.md for the boundary and implementation plan.

Scope #

This is intentionally a runner rather than a full browser: it permits HTTP(S) navigation and uses WebKitGTK's normal permission behavior. A future version can add a fixed-site allowlist, custom title/icon, or per-site launchers once the desired deployment model is decided. See docs/lock-plan.md and docs/design-decisions.md for the security boundaries and recorded design choices.