Wren Linux

wren-linux / tools / HANDOFF.md (as plain text)

Handoff (read this first)

Rewritten 2026-11-11, updated the same day after the owner pulled 4c6f6f0 and ran update.sh (the 2026-11-09 version is in git). That day the owner also rewrote the whole history from 6b0203c on (to replace a contact address that was in files), so every commit id from then on is new, the ones named in this file included (they were translated), and the rewritten commits carry no signatures: the next wren-release signs again. A computer that held the old pin refuses an unsigned newest commit until then. Remove this file, and its line at the top of CLAUDE.md, once the work below is done or has moved on.

This file is public: no unfixed security findings go in it, or in any commit or doc. A few low ones are still open in the private review notes; the owner has them. Ask, don't assume there are none.

Greet the owner, say what you found, and report early and often. The owner reads the code and takes the decisions below as made: don't reopen them.

1. Where things stand

Waiting on claude for the owner to pull: what comes after 4c6f6f0 (the owner's main, pulled and installed with bash update.sh --all on 2026-11-11): 179e501, 51bcc1b, dcb69d3, d864664, fc0256c, then this file. They are described in "Done after the pull" below. Everything before it, which the owner has:

Done after the pull (2026-11-11), one commit each:

Tests: the full suite was clean on 179e501 (147 ok) and on the tip (144 ok; the three fewer are the removed Kali, Parrot and Arch checks, luks-askpass, udiskie's page and the CI checks), each with only claude-vm's own failures: two setup-vps checks, and the Tor Browser AppArmor warning. The suite ran on the commits before the rewrite; the tip differs from the tested tree only in the contact address in SECURITY.md and its test, which test-wren-cmd.sh checks.

Tried for real in claude-vm: wren-watch as root under its systemd unit, another user's accept and diff through setpriv, links to root-only files; the signing-key extra installed for real, every signer command through sudo -u wren-signer, its firewall dropping all it sends, PYTHON* settings ignored even when forced past sudo, then removed. The signer's sudo rule asks a password even for a user with NOPASSWD for everything (sudo takes the last rule that matches).

2. Decided with the owner (2026-11-10/11)

3. The queue, in order

  1. Done: the owner pulled 4c6f6f0 and ran update.sh --all (2026-11-11).

  2. Done: apt over Tor (179e501).

  3. Done 2026-11-11 (the Tor Browser levels). Five commits:

    • 8453749: the Safest level locked by the browser's own policy file (data/tor-browser-policies- safest.json): slider + custom + svg/mathml/gfx locked, about:config blocked; shown on the real 16.0.1 headless that the shield's write is then silently ignored. javascript.* can't be policy-locked, so user.js still turns it off.
    • de01e77: install_browser, write_userjs/write_policies, prepare_profile, tor_browser_running move to lib/browser-sandbox.sh (seams: TB_PREFS_SRC, TB_LEVEL_NOTES, optional OLD_USERJS).
    • d9dd8e6: the torbrowser AppArmor fixes move to the lib, broadened to both users (fail-closed: missing rules deny, so this only lets each browser reach its OWN files). tor-browser's --remove keeps the shared fixes while the unsafe sibling is installed (tor_browser_sibling).
    • a5a35c6: extras/tor-browser-unsafe.sh - a second user at Safer, its own browser copy, profile, firewall, command; red address bar + tab (userChrome, pages can't see it); needs tor-browser first. Isolation shown: the unsafe user gets Permission denied reading the Safest user's home. Whonix's Workstation now gets the SAME Safest lock (f75789e): data/whonix-torbrowser.conf installs as /usr/local/etc/torbrowser.d/50_wren.conf and re-applies user.js + the policy file at every browser start (Whonix's tb-starter sources *.conf there), so the lock survives Whonix's own browser updates; whonix-update-browser reinforces the policy after its update too. Docs match (whonix.md: overview, LIMITS, MANUAL INSTALL). DECIDED, not a gap: NO second (less-locked) browser in the Workstation - the desktop's tor-browser-unsafe covers that, and a second one would double whonix update's wipe-and-restore; whonix update keeps updating Tor Browser (the lock re-applies at every start regardless, and Tor Browser's own updater keeps your profile). Owed to a real desktop: the shield shows the locked level and won't move, and the unsafe frame shows red, in wmenu (Super+d) and the Wren menu (Browse privately) - desktop verified headless only; plus the Workstation's locked level on a real Whonix screen.

    **NEXT (Whonix: a second Workstation for JavaScript) - DESIGNED 2026-11-11 with the owner, NOT started.** A sibling to this item. Instead of one locked browser, two Workstation VMs on the one Gateway: the current safe one (Safest, JS off) and a new unsafe one (Safer, JS on) for sites that need JavaScript. Separate VMs, not a second browser in one VM: stronger isolation, and each resets/updates on its own. (This is the two-VM route the owner chose; the "NO second browser" note above was about a second browser inside one Workstation, a different thing.) Decided with the owner:

    • Three launch commands, one implementation (symlinks dispatch on $0 into a shared whonix core, so no drift): whonix-noscript (Gateway + the Safest/no-JS Workstation), whonix-javascript (Gateway + the Safer/JS Workstation, red frame), whonix-full (Gateway + both). Each takes the verbs/flags: live (now the default - see amnesia), --save, --reset, usb, stop. Bare whonix stays the familiar safe launch (= whonix-noscript) and keeps whonix stop / whonix update / whonix gateway managing all VMs. Safe by default: the JS VM only starts when explicitly named (whonix-javascript/whonix-full), never from the habitual whonix. Names chosen by the owner (capability, not safe/unsafe); 'noscript' echoes Tor Browser's NoScript. Below, "the unsafe/safe VM" is shorthand for the javascript/noscript Workstation.
    • Amnesia by default, --save to persist. Both Workstations forget on shutdown (the Gateway stays persistent - its Tor guards). The amnesic overlay is RAM-backed (tmpfs on the host) so session plaintext never touches the SSD; init_on_free=1 (already set, parts/boot-hardening.sh) zeroes it when the VM exits, so a VM shutdown clears it with no host power-off. --save runs that VM on its persistent disk instead. update is always persistent (it writes the disk).
    • Persistence = a per-VM data disk, no inner LUKS. A qcow2 on the host, encrypted at rest by the host's mandated full-disk encryption (opsec.md: encrypted LVM). Holds the KeePassXC .kdbx (its own master password) + a short README/ENCRYPT.txt with vetted gpg -c commands and the "attach small files inside KeePassXC" tip. In the SAFE (noscript) VM the vault also holds the **wallet data dirs (Feather ~/.config/feather or its set dir, Electrum ~/.electrum) and SimpleX**, each self-encrypted by its own password; both wallets are lightweight (re-sync over the Gateway's Tor each session, no blockchain to persist). The unsafe VM has none of these. MONEY SAFETY: losing a wallet file loses the coins, so the wallet dirs must be bind-mounted to the vault before the wallets are used, and opening a wallet with the vault closed must WARN, not silently write it into amnesic space that vanishes on reboot (verify the exact Feather path on the real install). Workflow rule for the docs: **vault = ciphertext only** - do plaintext work on the amnesic side, gpg -c there, move only the .gpg in; the plaintext dies with the amnesic session. Honest limits to state: a gpg'd original can't be securely wiped on SSD (host FDE is the at-rest backstop); "at rest" = powered off (zram-only, below, drops hibernate), not suspended. Branch: a host without FDE leaves the data disk + Downloads plaintext at rest - the build should detect FDE and warn, or offer an inner-LUKS fallback.
    • KeePassXC: a separate DB per VM (different services; the safe VM's secrets never reach the unsafe one). Whonix likely ships it already - verify dpkg -l | grep keepassxc on the real Workstation; if present, configure not install. Seed ~/.config/keepassxc/keepassxc.ini at build: LockDatabaseIdleSeconds=60 (1-min idle-lock), ScreenLock + Minimize lock, ClearClipboardTimeout=10. Verify the exact INI keys against the installed version (renamed across releases). Amnesia re-asserts the config every boot, so no policy-lock is needed.
    • Strip the unsafe VM to browser-only + KeePassXC(own vault). Principle: only what the JS workflow needs; nothing holding value or long-term identity. Remove Electrum; do NOT add Feather or SimpleX (SimpleX lives in the safe Whonix); drop any other value/identity app (e.g. HexChat with saved logins). Exact list from the real image: `dpkg -l | grep -iE 'keepassxc|electrum|monero|hexchat|onionshare'` and the menu.
    • Red frame on the unsafe browser. Reuse the desktop's data/tor-browser-userchrome.css (red address bar + selected tab, #5e1a1a) in the unsafe Workstation's Tor Browser, so at a glance you know you're in the JS-on VM, not the Safest one - the same signal as the desktop's tor-browser-unsafe. Browser chrome only (pages can't see it; adds nothing to fingerprint). Applied via the lock's re-apply path: copy userChrome.css into the profile's chrome/ at every start (extend 50_wren.conf's copy step) + `toolkit.legacyUserProfileCustomizations.stylesheets= true` in the Safer user.js, so it survives Whonix's update-wipe. Safe VM stays default (no marker = Safest); optional later: a subtle distinct marker on the safe VM too.
    • Bookmarks: in KeePassXC, not the browser (owner, 2026-11-11). Wren ships NO bookmark list - the user keeps their onion links as entries in the vault's KeePassXC DB (no-JS sites in the noscript VM's DB, JS sites in the javascript VM's). Three wins: (a) no public-repo liability of Wren vouching for .onion links that change hands; (b) the right persistence model - browser bookmarks would be wiped by amnesia each reboot, the vault persists; (c) better opsec - your list of destinations lives in the locked DB, not in the browser profile a compromised page could read. Trade-off: copy the URL from KeePassXC into Tor Browser (no bookmark toolbar); fine for a modest list. Drops the policy-managed-bookmarks idea.
    • zram, no disk swap. Host: extras/zram.sh on + no disk swap partition, so QEMU's RAM (the session plaintext) can never spill to the SSD (its own comment: a swap file "would leave passwords and keys on the disk"). zram-only means no hibernate - fine, power off instead. Guest: the Workstation ships swapless (checked on the real VM), so guest zram is a pure add - a RAM cushion + better RAM use on an 11 GB laptop, not security-load-bearing. NB zram is RAM, not a disk: only its config (/etc/systemd/zram-generator.conf) goes into the image's filesystem at build time; nothing is provisioned on a virtual disk.
    • Snapshot after build, whonix [target] --reset. Snapshot each VM pristine right after it is built (virsh snapshot-create-as, qemu:///session); --reset reverts. OPEN: --reset also undoes whonix update - offer a re-baseline (re-snapshot after a known-good update)? Whonix VMs first; claude-vm (mockvps) is a separate second step.
    • USB transfer = the safe window. libvirt USB passthrough (<hostdev>) to the (amnesic) Workstation - the host never mounts the stick, so a hostile stick can't touch the real machine. Never a shared folder (virtio-fs/9p): that breaks the guest->host boundary the project forbids. As a command, e.g. whonix [target] usb. Host write access to the vault itself was considered and DECLINED (owner, 2026-11-11): mounting a used guest filesystem on the host exposes the host kernel to an untrusted filesystem (a deferred guest->host attack, worst from the unsafe VM). The only host touches are seeding the vault at build (pristine, pre-boot - the put pattern) and copying the whole qcow2 as an opaque blob for backup (no mount); every file move in/out at runtime goes through USB passthrough. Open decisions (owner's call) before/while building: (1) RESOLVED - command names whonix-noscript / whonix-javascript / whonix-full, bare whonix = safe launch + all-VM verbs. (2) SimpleX vs amnesia in the SAFE VM - APPROACH DECIDED (owner, 2026-11-11): do it the KeePass way. SimpleX's data dir lives on the persistent vault, linked into the amnesic home by a bind mount (or symlink) recreated each session when the vault is unlocked - the Tails Dotfiles/persistence model. SimpleX encrypts its own database with a passphrase (SQLCipher), so identity keys + history are self- encrypted at rest like KeePass's .kdbx, with host FDE under that. VERIFY on the real install: SimpleX's exact data path(s) (the desktop/AppImage build may not use ~/.local/share/simplex), and that the build's SimpleX exposes the database-passphrase setting. (3) RESOLVED - bookmarks live in KeePassXC (the vault), Wren ships no list (see the Bookmarks bullet). (4) --reset re-baseline or factory-only; (5) RESOLVED (owner, 2026-11-11) - the javascript VM is AMNESIA-ONLY: no whole-VM --save, the vault is its only persistence, every JS session starts clean (backup still works: copy the vault qcow2 blob). --save stays available for the noscript VM. (6) the exact strip list, from the real image; (7) confirm KeePassXC already ships. Build order (each its own commit + tests; this is installer/VM code -> full suite; most needs the owner's real machine + Whonix images - this VM can write the code and unit-test the command parsing and config generation, but can't integration-test VM creation or a real Whonix screen): A. parameterise whonix for a target; define the unsafe domain; extract/clone the ws image; apply the Safer lock (data/tor-browser-safer.js + -policies-safer.json already exist). B. amnesia-by-default + --save; RAM-backed overlay (tmpfs). C. per-VM persistent data disk; separate KeePassXC DB + 60s idle-lock config; README + gpg -c. D. strip list for the unsafe image. E. managed bookmarks. F. zram (host no-disk-swap; guest zram config into the image). G. snapshot + --reset. H. USB passthrough command. Owed to a real desktop throughout: every VM-creation and on-screen check (locked levels, red frame, the browsers, the KeePassXC lock, the reset/update/live/save behaviours). Don't-forget wiring (surfaced 2026-11-11): update the Wren menu ("Browse privately"), Super+d/wmenu, and data/keys.txt for the new commands; keep whonix live as an alias (live is now the default) with a note, don't just drop it; the upgrade path must turn an existing single Whonix-Workstation into the noscript one and ADD the javascript VM without clobbering it (update.sh/reinstall); budget note in docs - whonix-full is three VMs on the host and snapshots grow (prune occasionally); the two Workstations are circuit-unlinkable but share the Gateway's Tor entry guards (standard multi-WS Whonix - note it, don't imply separate guards).
  4. Sandboxing: thumbnails, then wren-unpack, then SimpleX. One commit and its tests each.

  5. Documents for the owner (they can't be done from here, these make them easy): a first-release checklist (domain, vps-client.sh --origin, wren-release, an install from the image on a second computer, update.sh against the server); the DNS TXT record for the signing key's fingerprint and the Verify page naming it; a brief for an outside review and pentest (the threat model, the sandbox users and firewall rules, the update and signing chain, root code that reads users' files, the Whonix boundary, the sudo rules, how to run the suite); the update path's design (the key on the security key, renewed subkeys the primary vouches for, before 2028-11-03) for the owner to agree; "if the maintainer is gone" in SECURITY.md.

  6. Faster tests (plan drawn up 2026-11-11, not started; measure before trusting any number). Why slow: 114 independent tools/test-NAME.sh run strictly one after another in one process, plus the inline shellcheck (~152s) and the .deb build (~398s); on a multi-CPU VM the cores sit idle. Rule above all: every check kept, the same commit reports the same checks every run, any failure still names its check and fails the suite, and a --jobs 1 serial fallback stays for good.

    • Phase 0 (ship first, tiny, zero-risk): opt-in per-group timing in dev_test, printed sorted with --timings, default output unchanged. Gives the real hot-spot list (my classifying greps over-matched, so don't trust them) and the before/after yardstick for every later phase.
    • Phase 1 (the win): dispatch the independent files into a pool of size nproc (raise with claude-vm --cpus), each writing to its own output file; collect and print in a FIXED order so the report is byte-stable; aggregate exit = any failure. Run ALONE, never beside anything: the pty / real-time tests (menu, timer, tty-read, the watchers — exact list from Phase 0) and the few that use real sudo/systemctl/nft or a VM instead of the pretend sudo.
    • Phase 2: build the expensive shared artifacts once — the .deb and a throwaway signing key in a temp GNUPGHOME — and point the groups that need them at the prebuilt copy (the package rebuild ~398s is the biggest single saver). Fold the inline shellcheck in as one parallel job.
    • Phase 3 (optional, needs root): for the few that truly need real root and can't sit beside others, a throwaway unshare+chroot (or a cheap container) each, so they parallelise without touching the VM or each other, and without losing "a bad test fails, it doesn't change the computer". Heaviest to build; only if Phases 1–2 don't hit the target. Ship phase by phase, each proven by a full run with the SAME check counts before and after. The experiments can't run beside another test run (one at a time). TRIED 2026-11-11, and the evidence REORDERS the plan: Phase 0 shipped (49963fa, --timings). Real data, --offline on the 6-CPU VM: build-deb 456s (the pole - it runs the whole suite recursively), whonix-cmd 96, wren-menu 69, battery-bar 61, extras-flow 53, site 43, then a long small tail; inline shellcheck ~150s. A --jobs N pool (pty tests alone) was written and tried, then REVERTED: --jobs 6 ran in 955s (28% off ~1320s) but FAILED a check the serial run passes - onionshare-tor's "one at a time: a second is refused". Cause: build-deb runs the whole suite recursively, so its inner onionshare-tor ran beside the outer pooled one and they collided on OnionShare's global one-instance lock. So parallelism is NOT a clean win yet: (a) the gain is small while build-deb (35% of the run) is the pole, and (b) tests holding a global/singleton resource (onionshare-tor found; tor-service, vault, secure-dns, wren-watch and more are candidates) race with build-deb's recursive copy, and those races are intermittent - hard to shake out reliably, risky on the harness that is the project's check. DO PHASE 2 FIRST: stop build-deb re-running the WHOLE test.sh (build and check the package, don't recurse the full suite) and/or cache the .deb; THEN Phase 1, with a per-test audit of fixed resources (ports, sockets, locks, fixed paths) done with the owner, not just the pty set. The reverted pool code was mechanically sound (order and counts correct in isolation) and can be rebuilt once build-deb stops recursing.
  7. Done: the retirements (51bcc1b), and the GitHub/CI cleanup (d864664, fc0256c).

  8. Install questions asked up front (the owner's plan): design first, with the owner. Each extra a --ask that writes its answers; secrets never written; what can't be asked ahead listed at the start. Mullvad's lockdown question is the first user. With it, the owner's prompt sound: if the sound extra is picked, an up-front question "Play a sound when Wren Linux asks you something? (gong)", yes by default. Played from the shared prompt (tty_read, lib/core.sh) and before sudo asks, never for quick menus or with no sound server or nobody there; the timer's own sounds (lib/alarm-sounds.py). Changed later from a Sounds line in the Wren menu and wrenctl sound prompt gong|bell|wren|off. (vps-client's per-run gong then goes through it.)

  9. From the earlier list: release (wren-release --tag, an image too); the owner's last free ultra review kept for the new update path (show the scope first); a Display section in the menu; console-only's cage per program; a separate admin login as an extra; iso2usb, a [y/n/b] answer, keepassxc in a scratchpad, coding-agent.sh.

  10. Idea parked (owner, 2026-11-11): a read-only, headless, always-on Claude "security watchdog" that reads logs, connections, browser-side JS etc. and flags dangers (asks before any action). Appealing on a security distro, and read-only + ask-first is the right posture, BUT three problems before it could be a plan: (a) PRIVACY CONTRADICTION - Claude runs via Anthropic's servers, so streaming logs/connections/browsing to it is the opposite of what Wren protects; would have to be on-demand + scoped + glaringly opt-in, or a LOCAL model (weaker, heavy on a laptop). (b) It can't run 24/7 on Pro/consumer limits - continuous whole-system inference is a metered-API workload, costly, and likely against automated-use terms. (c) PROMPT INJECTION via the data it reads - a crafted log line or page JS could carry instructions to the monitor itself; a serious surface for a read-everything agent. Viable shape: a LOCAL, user-invoked "explain/triage this log or connection" assistant where real tools (auditd, AIDE, suricata/zeek, firewall logs) do the detecting and Claude interprets on demand - not an always-on cloud watcher.

4. Not tried on a real desktop yet

The two locked Tor Browser levels on a real screen: the shield at Safest/Safer and refusing to move, about:config blocked, the unsafe one's red address bar and tab, and both in wmenu (Super+d) and the Wren menu. Verified here only headless (the browser starts as its own user; files and locks in place).

The bar's "startup changed" with the Python wren-watch, and its window; wren-key renew with two windows open (it should refuse, and ask sudo once); the wide code pages on the site; wren-learn's manual window; the regrouped Install apps list in foot and on the console (stars there); a --backup to a real stick and --reconnect --from it after a reinstall. Earlier, still untried: pings from another device; the watch window's 1 with a wrong password and with Ctrl+C; browser-sandbox clip by its key; a fresh install from the image; a Firefox profile's window name.

5. Lessons from 2026-11-09

6. Lessons from 2026-11-10/11

7. Lessons from 2026-11-11