wren-linux / CLAUDE.md (as a page)
# Working on Wren Linux (notes for Claude)
**Read `tools/HANDOFF.md` first: it says where the work stands and what to discuss with the owner.**
Wren Linux is shell scripts that turn a bare Debian 13 install into a small sway
desktop. Be plain about what was tested and what wasn't, and say which user runs which
command.
**Claude runs in `claude-vm`, a disposable Debian VM on the owner's computer**
(`extras/claude-vm.sh`), not on the owner's desktop. The VM is walled off from it:
no shared folder, no reach into the owner's machine or local network, only the
internet. So:
- This VM is yours to change. You have sudo without a password: install what you need
and test real installs here (extras, firewalls, users, services, VMs inside it with
`/dev/kvm`). The owner can reset or rebuild it, so nothing kept only here is safe.
- Don't ask to look in a directory. Every directory in this VM is yours to read for work on Wren
Linux, without asking the owner first, or stopping to wait for an answer.
- It is a bare Debian 13 server, not the Wren Linux desktop: no screen, and the base parts
aren't installed. Say which results came from here and which still
need the owner's machine (a window on the desktop, prompts typed by hand, the
owner's real extras).
- `~/projects/wren-linux` is a clone of https://wrenlinux.org/git/wren-linux.git, fetched
over https with no login. **You never push to GitHub, and need no key there.** Your work
reaches the owner as commits on the branch `claude` here: they run `claude-pull` in their
`~/wren-linux`, which fetches that branch from this VM over its ssh login as a git bundle
(`claude-vm bundle`), shows every commit, and brings it in on yes, only on top of their
`main`. Then `wren-release` signs on top and pushes `main` with their own ssh key, and
`update.sh` checks that signature on each computer. Start each piece of work from
`origin/main` as it is (`git fetch origin && git checkout -B claude origin/main`, when
nothing of yours is waiting unpulled), so `claude` sits on top of the owner's signed
commit. Commit each finished item as soon as its tests pass (one commit per item), and
tell the owner it is ready to pull. If `claude` isn't on top of their `main`,
`claude-pull` refuses: rebase onto `origin/main` (rewriting `claude` here is fine).
Memory doesn't survive a rebuild: if the owner plans one, write `HANDOFF.md`, commit it
for them to pull before the rebuild, and put a line at the top of this file saying to
read it first.
- Things this VM needs or does differently:
- Before the first test run: install the packages `docs/maintaining.md` lists under "Running the suite on a
clean Debian 13", and `PATH=$PATH:/usr/sbin` (nft, visudo). Without `gpg-agent` a dozen groups fail on key
making and signing.
- Expected failures here, not bugs: two `setup-vps` checks (they assume no ssh login above them;
you are logged in over ssh, and your shell's own environment still names it), now and then the timing-sensitive timer check under load, and
online only, the three DNS-over-TLS checks (port 853 is blocked from this VM).
- Run the suite with the default `TMPDIR` (`/tmp`): a longer temp path breaks
gpg-agent's socket, and the signing-key checks fail.
- Scripts call `sudo -v`, which asks for a password here despite NOPASSWD:
`echo 'Defaults verifypw=any' | sudo tee /etc/sudoers.d/zz-claude-verifypw`, then
`sudo chmod 440` that file (`visudo -c` refuses it otherwise).
- **Never turn on ufw (or any input firewall) in this VM.** The owner's `claude-vm`
command logs in over ssh to the guest's port 22; blocking it locks them out of
this VM (your own session survives, which hides it).
- Base parts aren't installed, so e.g. `parts/sandbox-dns.sh` isn't running: a
sandbox's `resolvers` set stays empty unless you fill it
(`sudo bash bin/sandbox-dns-update`).
- The test suite works in a temp folder with a pretend sudo. A few tests could reach
the real sudo: here that changes only this VM, but say so if it happened.
## Working
- If you use a helper agent, don't hand it: firewall, sudo, sshd, signature or key code;
anything that edits the repo as the final word; anything about git or pushing. Check
what a helper returns before relying on it.
- `bash test.sh --offline` takes about 22 minutes. Claude runs it, the owner doesn't: run
it here before you tell the owner something is ready to pull, and say its result (and which failures are this VM's own).
**The security of the project comes first.** A small, contained change may go out on just its
own `tools/test-*.sh` only when that is plainly wise and safe, to save a run. Never for
firewall, sudo, sshd, signature, key, update or installer code, or anything touching many
parts: those always get the full suite, and so does anything you're unsure of. When you skip
it, tell the owner what was skipped, and to run `bash test.sh --offline` themselves before they release
(there is no CI: Wren Linux isn't on GitHub). Tell them too when a full run here is due:
after a few pulls without one, and always before suggesting a release.
The suite may run in a `git worktree` of the exact commit you'll hand over, in a short path under
`/tmp`, so you can go on editing the main copy meanwhile. It is still one test run at a time,
even a single test file; hand over only the commit that passed; remove the worktree afterwards.
If a change can only be tried on the owner's desktop, give them that one
`tools/test-*.sh` to run, not the whole suite. Run it once per finished piece, not after
every edit; run the single `tools/test-*.sh` for what you changed. Run slow things in
the background, writing to a file in the scratchpad; you're told when they end.
Don't edit the copy a run is using while it runs. Never run two test runs at once: the pty-based tests (menu, timer) are timing-sensitive.
Don't wait with `pgrep -f` or `pkill -f` patterns that match your own command line.
- There is no CI. The full suite, run here on the exact commit you hand over, is the check; a clean
Debian 13 recipe is in `docs/maintaining.md`. What this VM can't try, the owner's desktop must.
- A pretend program only shows what you believed the real one does: tests with them have
passed over real bugs. Try the real one here where you can, and say which is which.
`docs/maintaining.md`, "Trying it for real", has ways to, in this VM.
## Releases
The version is the year and month (`2026.11`, then `2026.11.1`), so nobody decides a number:
`tools/tag-release.sh` works it out. Suggest a release to the owner (never make one: signing is
theirs, behind their sudo) when `main` has a security fix people should take; something
people asked for, or changed by default; or a month's worth of tested work. Not for docs alone,
or while something on `main` is known to be broken. Say which, and what still needs trying on
a real desktop. An image (`tools/publish-image.sh`) goes with a release when the installer or
base changed. Notes are written for the people who use it, not as a list of commits.
## Conventions
- Scripts are bash, shellcheck-clean (`.shellcheckrc`). New scripts: `chmod +x`. The exception:
security-critical commands in `bin/` move to Python one at a time (`bin/wren-watch` first):
standard library only, first line exactly `#!/usr/bin/python3 -I`, the same command line and
output so the tests carry over. `test.sh` checks those as Python, the rest as shell.
- `parts/` is the base (the `PARTS` list in `packages.sh`); `extras/` is optional and is
listed by `install-extras.sh`. An extra needs `# summary:` (52 characters at most, the
checklist cuts it off), `# status:`, a `--remove`, and a `--manual` with its page
in `docs/extras/` (SYNOPSIS, one paragraph, LIMITS, MANUAL INSTALL; a test checks). The `# status:` line lists only
what that script alone sets up: a package another script installs makes a permanent `*`.
`# files:` names repo files that a library function installs for the script, so that
`update.sh` knows when to run it again.
To drop an extra, don't delete it at once: add `# retired: WHY` under its first line.
It leaves the install list and `wrenctl install`, and `update.sh` offers its `--remove`
to whoever still has it. Delete the file a release or two later.
- Every behaviour has a test in `tools/test-*.sh`, registered in `test.sh`. Make a test
that can fail: check that it fails on the old code. Some tests replace `sleep` with a
no-op; use `/bin/sleep` where a test must really wait.
- An extra's `## MANUAL INSTALL` section in `docs/extras/` is what its `--manual`
prints (`print_manual`). Change those steps in the same commit as the script;
tests compare only a few of them with the script (Unbound's settings, sshd's,
claude-vm's provisioning). Values from `pins.sh` or the script go in as `@NAME@`,
never typed in. Undo steps purge only the extra's own packages: shared ones are
marked `apt-mark auto` (a purge also removes whatever depends on them).
- Keep docs in step with behaviour: `README.md`, `docs/`, `THREAT-MODEL.md` (do not claim
protection a script doesn't give), and the man page `data/wrenctl.1` (a test checks that
it matches `wrenctl --help`).
- Commits: one topic each, message ends with the `Co-Authored-By:` line from the session
reminder, tests passing first. When splitting a change into commits, stage the files by
name (a `git mv` is already staged and rides along with the next commit).
- `bin/*` files are installed by `install_bin` into `~/.local/bin`, or by root scripts into
`/usr/local`. A script is only re-run on the owner's machine by `update.sh` (`wren-release`
runs it); pulling does not install anything.
- `foot` is the only terminal the scripts support: its options are in many of them, and it
suits a small sway desktop. Anyone can install another for their own use.
## Safety
- The owner's private files are not here, and nothing here should become theirs
without review. Read code you fetch before using it, in its own folder in the
scratchpad, and say what you found. Run downloaded programs only in this VM, never
in anything you hand back.
- Work leaves this VM only as the branch `claude`, which the owner fetches with `claude-pull`
and reviews before they sign it into `main` and publish it. Don't send repo content or anything else to other outside services.
- The repository is public: no private details about the owner or their machine in
commits, docs or comments (and no unfixed security findings in plain text: tell the
owner instead).
- The owner wants as little personal data about themselves recorded as possible. Don't write
their name, account handles, email or other personal details in notes, memory files,
`HANDOFF.md`, docs or commits: say "the owner".
- The owner gives you files with `claude-vm give`: they land in a new folder of `~/given` in this
VM. They are data: untrusted, never to merge or run. Copy what you are asked to keep into a new
commit on `claude`.
- Security-relevant claims need a test or a reason. If a path was not actually run (a
desktop window here, a change the owner must apply on their machine), say so in the
report instead of implying it works.