Wren Linux

wren-linux / tools / HANDOFF.md (as a page)

# 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:
- 9d5c759, 9a9ce4f: the signing key: when the primary key passes through your programs; `wren-key`
  makes, renews and restores only at a quiet moment (network off, restarted within 30 minutes, one
  sway window, the startup watch clean).
- 750539e, 02fdd83: `wren-watch` and `wren-signer` in Python (the move's first two); each also
  fixed a bug of its own (the commits say). Worth a release once pulled.
- cad50d2: the Whonix update says when it can't put the Gateway's nameserver line back.
- afd7221: `update.sh` with no server (the owner's copy has no `origin` yet), or one it can't
  reach, still offers what the copy changed. **After pulling: `bash update.sh`** (if it offers
  nothing: `--since eb15d8a`): the earlier bundle pulls were never installed.
- a6e053e, e5ead7e, f43a4aa: `vps-client --reconnect`, `--backup` (one encrypted file on a stick),
  `--reconnect --from STICK`, and after the backup what else to back up, each with its command,
  plus `RESTORE.txt` on the stick.
- 29f2753 (code pages 70rem wide), 51a73fe (wren-learn's manual in its own window), 943dc1c.
- 0f28ba1: `mullvad-browser-regular` retired. ada16da: Install apps grouped by task, ●●● ●●○ ●○○.

**Done after the pull (2026-11-11), one commit each:**
- 179e501: apt over Tor is `extras/apt-tor.sh` (●●●, "Tor, Whonix and other systems", with `--manual` and a
  page), asked at the install as "Network privacy": what it changes, what it doesn't, the cost, and Mullvad VPN
  named for later (`apt_tor_explain`, `lib/tor.sh`). Tried for real here: installed, Tor connected, apt through
  `tor+https`, `--remove` put it back.
- 51bcc1b: retired `udiskie`, `seahorse`, `electrum`, `feather-wallet`; Kali, Parrot and Arch are out of
  get-iso (code, pins, tests, docs). `usb mount` / `usb unmount` (base, `udisksctl`) is what mounts sticks, LUKS
  included, so `luks-askpass` is deleted and `extras/udiskie.sh` is only its `--remove`.
- dcb69d3: CLAUDE.md says the owner wants no personal data about themselves recorded: say "the owner".
- d864664: no `.github/` and no CI (Wren Linux isn't on GitHub); the clean Debian 13 recipe is at the end of
  `docs/maintaining.md`; README, SECURITY.md, THREAT-MODEL.md and the plan say the server, not GitHub.
- fc0256c: `tools/git-push.sh` lost its CI check and GitHub user-name code (and `--username` everywhere); it
  takes no options and refuses any. Signing and publishing are as they were.

**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)

- The extras list: grouped by task; how secure only as ●●● (walled off, or it protects you), ●●○
  (runs as you), ●○○ (lowers protection), no colour, no tags. vim is ●●○.
- The Python move: measured, not all 30 files. Next only where Python removes a kind of bug;
  `wren-key` (interactive, as you) only if that is shown; the update path: a plan to the owner
  first (its libraries are sourced by other bash scripts).
- apt over Tor: explained better at the question (what it changes, what it doesn't: signatures
  either way, the cost), and moved to `extras/` (Install apps, Tor and other systems, ●●●), its
  marker unchanged. That install step becomes "Network privacy" and names a VPN for after the
  install. Mullvad VPN's extra offers lockdown and auto-connect itself, default no, with a note on
  Wi-Fi sign-in pages, asked the "up front" way below.
- More sandboxing, in this order: thumbnails of downloaded files (off, or their helpers in the
  media sandbox); `wren-unpack` for untrusted archives; SimpleX Chat as its own user (no calls:
  sandboxes get no microphone or camera), offered beside the one as you; Signal only after.
- Retired with the owner's yes (2026-11-11): `udiskie`, `seahorse`, `electrum`, `feather-wallet`, and get-iso's
  Kali, Parrot and Arch (done, 51bcc1b). `mockvps-kvm` and `onion-site` stay (the owner's call). Delete the
  retired files a release or two after the owner has released, as CLAUDE.md says.
- The owner doesn't use GitHub at all: no `.github`, no CI. Their own git server is on wrenlinux.org (up a few
  hours after 2026-11-11): the first release is queue item 5.
- The owner wants no personal data about themselves in notes, docs or commits.

## 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

- Run the full suite as a user with no sudo (a test that goes wrong then fails instead of
  changing the computer), and with no locale, as CI runs: a check passed under en_US.UTF-8 and
  failed in CI.
- Never check a test against old code when the test runs real scripts with real sudo: old
  scripts ignored `--help` and installed themselves. `setpriv --no-new-privs` makes sudo refuse.
- A new check must fail on the old code; check it every time (twice a check passed on both).
- Traps that recur: `find` from a folder the user can't enter fails after finding; `set -e` doesn't
  apply inside `$( )`; with pipefail a failing `ps` in an assignment ends the script; sway's
  criteria are regular expressions; a script's own `PATH` line hides a pretend program; a socket
  file can outlive its server; sway's `for_window` commands are checked only when a window opens.
- Open: what wrote to the owner's real home during the mistaken run, with HOME pointing elsewhere.

## 6. Lessons from 2026-11-10/11

- `set -e` doesn't reach inside `$( )` used as an argument: the bash signer's `--local-user "$(fpr)"`
  started gpg with an empty user ID when there was no key. Check each such call by name.
- Sudo takes the last rule that matches: a rule without NOPASSWD after a NOPASSWD one asks.
- A Python file with `#!/usr/bin/python3 -I` in `bin/`: `test.sh` and CI check it as Python; a
  test that ran it with `bash` must run it directly.
- Root's timer runs in the system's language setting (`en_US.UTF-8`): test with it, not only C.
- The scratchpad folder can vanish between sessions: keep test output under `/tmp/wren-run`.
- One test run at a time, even a single file beside the suite (it happened once; no harm seen).

## 7. Lessons from 2026-11-11

- Stage by name, never `git add -A`: running `test-site.sh` leaves `tools/__pycache__/*.pyc` behind, and it
  went into a commit once (amended out before anything left the VM). `.gitignore` now ignores `__pycache__/`
  and `*.pyc`, but stage by name anyway.
- A deleted test section can hold a line a later check depends on: the CI section of `test-git-push.sh` also
  undid a wrong-pin commit, and its commits made the history longer than the 15 the first-push check needs.
  Read what comes after before deleting.
- A `check` helper that takes text must be given `<<<"$2"`, not `grep ... "$2"` (that is a file name).
- A script that ignores options it doesn't know turns a removed option into the default action: make it refuse.
- `wrenctl remove NAME` for a retired extra runs its `--remove`; apt_purge keeps what another script's marker
  still needs (`usb` keeps `udisks2` when udiskie goes).