Harden browser release and credential handling

ober

ddb100751e7fd010b2e0b3f71332d2d015eef030

diff --git a/.build.yml b/.build.yml
index 8ff03e1..2a28239 100644
--- a/.build.yml
+++ b/.build.yml
@@ -118,12 +118,10 @@ tasks:
       # Offline Qt GUI / snapshot harness (offscreen, headless). Kept hermetic
       # in CI (JWB_TEST_NO_NETWORK) so it does not depend on live example.com;
       # the full networked path runs locally and on the amd64 verifier.
-      # --no-sandbox: Chromium's zygote can't sandbox inside the CI container.
       # Exported env (not make vars): the Makefile's `?=` defaults defer to env,
       # and JERBOA_BROWSER_LIB must reach the scheme process directly.
       export QT_QPA_PLATFORM=offscreen JWB_TEST_NO_NETWORK=1
-      export QTWEBENGINE_DISABLE_SANDBOX=1
-      export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu"
+      export QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu"
       export JERBOA_HOME="$HOME/jerboa" SCHEME="$(command -v scheme)"
       export JERBOA_BROWSER_LIB="$PWD/qt-webengine/build/libjerboa_browser.so"
       make test-gui
@@ -131,8 +129,7 @@ tasks:
       cd jerboa-browser
       # Stage 1 buffer-model functional tests (offscreen, hermetic in CI).
       export QT_QPA_PLATFORM=offscreen JWB_TEST_NO_NETWORK=1
-      export QTWEBENGINE_DISABLE_SANDBOX=1
-      export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu"
+      export QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu"
       export JERBOA_HOME="$HOME/jerboa" SCHEME="$(command -v scheme)"
       export JERBOA_BROWSER_LIB="$PWD/qt-webengine/build/libjerboa_browser.so"
       make test-buffers
@@ -151,8 +148,7 @@ tasks:
       # Stage 2 keymap+minibuffer+command controller functional tests
       # (offscreen, hermetic): synthetic key tokens drive real buffer state.
       export QT_QPA_PLATFORM=offscreen JWB_TEST_NO_NETWORK=1
-      export QTWEBENGINE_DISABLE_SANDBOX=1
-      export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu"
+      export QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu"
       export JERBOA_HOME="$HOME/jerboa" SCHEME="$(command -v scheme)"
       export JERBOA_BROWSER_LIB="$PWD/qt-webengine/build/libjerboa_browser.so"
       make test-commands
@@ -162,8 +158,7 @@ tasks:
       # the app-wide event filter → poll queue → emacs keymap/command dispatch
       # (offscreen, hermetic). Proves the GUI's key path, not just the controller.
       export QT_QPA_PLATFORM=offscreen JWB_TEST_NO_NETWORK=1
-      export QTWEBENGINE_DISABLE_SANDBOX=1
-      export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu"
+      export QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu"
       export JERBOA_HOME="$HOME/jerboa" SCHEME="$(command -v scheme)"
       export JERBOA_BROWSER_LIB="$PWD/qt-webengine/build/libjerboa_browser.so"
       make test-keys
@@ -173,8 +168,7 @@ tasks:
       # the command controller against rendered data: pages (offscreen, hermetic
       # — data: bypasses the no-network policy; find needs a rendered frame).
       export QT_QPA_PLATFORM=offscreen JWB_TEST_NO_NETWORK=1
-      export QTWEBENGINE_DISABLE_SANDBOX=1
-      export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu"
+      export QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu"
       export JERBOA_HOME="$HOME/jerboa" SCHEME="$(command -v scheme)"
       export JERBOA_BROWSER_LIB="$PWD/qt-webengine/build/libjerboa_browser.so"
       make test-nav
@@ -185,8 +179,7 @@ tasks:
       # hermetic — data: bypasses the no-network policy; the overlay reads
       # getBoundingClientRect, so the harness shows + sizes + pumps first).
       export QT_QPA_PLATFORM=offscreen JWB_TEST_NO_NETWORK=1
-      export QTWEBENGINE_DISABLE_SANDBOX=1
-      export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu"
+      export QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu"
       export JERBOA_HOME="$HOME/jerboa" SCHEME="$(command -v scheme)"
       export JERBOA_BROWSER_LIB="$PWD/qt-webengine/build/libjerboa_browser.so"
       make test-hint
@@ -196,8 +189,7 @@ tasks:
       # masked prompt, save/fill/forget autofill against a rendered data: form
       # (offscreen, hermetic). The vault touches no env/config/disk by design.
       export QT_QPA_PLATFORM=offscreen JWB_TEST_NO_NETWORK=1
-      export QTWEBENGINE_DISABLE_SANDBOX=1
-      export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu"
+      export QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu"
       export JERBOA_HOME="$HOME/jerboa" SCHEME="$(command -v scheme)"
       export JERBOA_BROWSER_LIB="$PWD/qt-webengine/build/libjerboa_browser.so"
       make test-pass
@@ -208,8 +200,7 @@ tasks:
       # rendered offline into a new buffer (offscreen, hermetic). --disable-gpu
       # is required: the bindings page would otherwise FATAL the GPU surface.
       export QT_QPA_PLATFORM=offscreen JWB_TEST_NO_NETWORK=1
-      export QTWEBENGINE_DISABLE_SANDBOX=1
-      export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu"
+      export QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu"
       export JERBOA_HOME="$HOME/jerboa" SCHEME="$(command -v scheme)"
       export JERBOA_BROWSER_LIB="$PWD/qt-webengine/build/libjerboa_browser.so"
       make test-polish
@@ -222,8 +213,7 @@ tasks:
       # session also builds the engine from the built-in starter list. The
       # engine_match semantics are covered by the C++ ctest in the verify task.
       export QT_QPA_PLATFORM=offscreen JWB_TEST_NO_NETWORK=1
-      export QTWEBENGINE_DISABLE_SANDBOX=1
-      export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu"
+      export QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu"
       export JERBOA_HOME="$HOME/jerboa" SCHEME="$(command -v scheme)"
       export JERBOA_BROWSER_LIB="$PWD/qt-webengine/build/libjerboa_browser.so"
       make test-adblock
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..74848ef
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,50 @@
+name: CI
+
+on:
+  push:
+    branches: [main, master]
+  pull_request:
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+env:
+  JERBOA_VERSION: v0.2.3
+  JERBUILD: ${{ github.workspace }}/.jerboa/bin/jerbuild
+  QT_QPA_PLATFORM: offscreen
+  QTWEBENGINE_CHROMIUM_FLAGS: --disable-gpu
+  JWB_TEST_NO_NETWORK: "1"
+
+jobs:
+  verify:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Install system tools
+        run: |
+          set -eu
+          sudo apt-get update
+          sudo apt-get install -y --no-install-recommends \
+            build-essential git curl ca-certificates cmake pkg-config \
+            qt6-base-dev qt6-webengine-dev xvfb
+          command -v cargo
+          cargo --version
+
+      - name: Install Jerboa toolchain
+        run: sh support/ensure-jerboa.sh "$JERBOA_VERSION" .jerboa/bin
+
+      - name: Install cargo-audit
+        run: cargo install cargo-audit --locked
+
+      - name: Build Qt WebEngine backend
+        run: |
+          cmake -S qt-webengine -B qt-webengine/build -DCMAKE_BUILD_TYPE=Release
+          cmake --build qt-webengine/build --parallel 2
+
+      - name: Verify
+        run: make verify
+
+      - name: Release evidence
+        run: make release-evidence
diff --git a/.github/workflows/security-baseline.yml b/.github/workflows/security-baseline.yml
new file mode 100644
index 0000000..28a713e
--- /dev/null
+++ b/.github/workflows/security-baseline.yml
@@ -0,0 +1,35 @@
+name: Security Baseline
+
+on:
+  push:
+    branches: [main, master]
+  pull_request:
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+jobs:
+  baseline:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Required release files
+        run: |
+          set -eu
+          test -f LICENSE
+          test -f SECURITY.md
+          test -f .gitignore
+          find . -maxdepth 1 -iname "README*" -type f | grep -q .
+
+      - name: High-confidence secret scan
+        run: |
+          set -eu
+          pattern="(BEGIN (RSA|OPENSSH|EC|DSA|PRIVATE) KEY|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-(ant-api03|proj|svcacct)-[A-Za-z0-9_-]{30,}|AKIA[0-9A-Z]{16})"
+          matches="$(git grep -n -I -E "$pattern" -- . ":!*.png" ":!*.jpg" ":!*.jpeg" ":!*.gif" ":!*.so" ":!*.dylib" ":!*.o" ":!*.a" ":!*.boot" ":!*.tar.gz" || true)"
+          if [ -n "$matches" ]; then
+            echo "$matches"
+            echo "High-confidence secret pattern found."
+            exit 1
+          fi
diff --git a/.gitignore b/.gitignore
index 91a32e3..99a9dd8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,8 @@ build/
 /jerboa-browser
 jerboa-browser-static-qt-linux-amd64.tar.gz
 .bcache/
+.jerboa/bin/
+dist/
 *.so
 *.wpo
 *.boot
diff --git a/.jerboa/security.json b/.jerboa/security.json
new file mode 100644
index 0000000..d57b732
--- /dev/null
+++ b/.jerboa/security.json
@@ -0,0 +1,27 @@
+{
+  "version": 1,
+  "repo": "jerboa-browser",
+  "extends": ["jerboa:gui", "jerboa:network-client", "jerboa:crypto", "jerboa:ffi"],
+  "paths": {
+    "production": ["scheme/**/*.ss", "qt-webengine/src/**/*.{cpp,h}", "include/**/*.h", "build-binary.ss", "packaging/**/*.sh", "support/**/*.sh", "Makefile"],
+    "tests": ["scheme/**/*-test.ss", "qt-webengine/tests/**"],
+    "generated": ["build/**", "dist/**", "target/**", "qt-webengine/build/**", "test-artifacts/**", "*.so", "*.dylib", "*.wpo", "*.boot"],
+    "vendor": ["vendor/**", "qt-webengine/vendor/**"],
+    "docs": ["README.md", "SECURITY.md", "docs/**", "*.md"]
+  },
+  "policy": {
+    "failOn": ["critical", "high"],
+    "imports": { "directChezscheme": "deny" },
+    "ffi": { "allowed": true, "requireLazyLoading": true, "requireBoundaryDocs": true },
+    "browser": {
+      "chromiumSandbox": "required",
+      "defaultProfile": "off-record",
+      "persistentProfileRequiresEncryptedVault": true,
+      "envPassphrase": "deny"
+    },
+    "process": { "shellInterpolation": "deny" },
+    "network": { "denyByDefaultCapabilities": true },
+    "secrets": { "forbidLogging": true, "ramOnlyPasswordVault": true, "strictStateFilePermissions": true }
+  },
+  "suppressions": []
+}
diff --git a/.jerbuild b/.jerbuild
index f20f7bf..945cf2f 100644
--- a/.jerbuild
+++ b/.jerbuild
@@ -8,4 +8,3 @@
 (output "jerboa-browser")
 (libdirs "vendor/jerboa-yubikey/lib" "scheme" "vendor/jerboa-fuse/lib")
 (pre-build "sh support/ensure-vendor.sh jerboa-yubikey https://git.sr.ht/~lisp/jerboa-yubikey")
-(pre-build "sh support/gen-browser-vault-secret.sh scheme/browser/build-vault-secret.ss")
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/Makefile b/Makefile
index dd90b95..1a743c0 100644
--- a/Makefile
+++ b/Makefile
@@ -1,17 +1,27 @@
 # jerbuild bundles Chez Scheme + the jerboa stdlib, so building the browser
-# entry needs only `jerbuild` + a C compiler — no jerboa source checkout. (The
-# full Qt/WebKit GUI bundle additionally needs the native wrapper.)
-JERBUILD ?= jerbuild
-JH := $(shell $(JERBUILD) --jerboa-home 2>/dev/null)
+# entry needs only `jerbuild` + a C compiler. CI uses a pinned project-local
+# release toolchain; developer machines can still override JERBUILD explicitly.
+JERBOA_VERSION ?= v0.2.3
+JERBOA_TOOL_DIR ?= $(CURDIR)/.jerboa/bin
+JERBUILD ?= $(shell if [ -x "$(JERBOA_TOOL_DIR)/jerbuild" ] && [ -x "$(JERBOA_TOOL_DIR)/jerboa" ]; then \
+	printf '%s\n' "$(JERBOA_TOOL_DIR)/jerbuild"; \
+elif command -v jerbuild >/dev/null 2>&1 && command -v jerboa >/dev/null 2>&1; then \
+	command -v jerbuild; \
+else \
+	printf '%s\n' "$(JERBOA_TOOL_DIR)/jerbuild"; \
+fi)
+JH := $(shell "$(JERBUILD)" --jerboa-home 2>/dev/null)
 JERBOA_YUBIKEY_URL ?= https://git.sr.ht/~lisp/jerboa-yubikey
 JERBOA_YUBIKEY_DIR := vendor/jerboa-yubikey
 JERBOA_YUBIKEY_LIB := $(JERBOA_YUBIKEY_DIR)/lib
 JEXEC       := $(JERBUILD) exec --libdirs $(CURDIR)/$(JERBOA_YUBIKEY_LIB):$(CURDIR)/scheme:$(CURDIR)/vendor/jerboa-fuse/lib:$(JH)/lib
-JERBUILD_ERROR := "ERROR: jerbuild not found on PATH (or '$(JERBUILD) --jerboa-home' failed). Install jerbuild, or set JERBUILD=/path/to/jerbuild."
+JERBUILD_ERROR := "ERROR: jerbuild unavailable (or '$(JERBUILD) --jerboa-home' failed). Run make ensure-jerboa-tools, install jerbuild, or set JERBUILD=/path/to/jerbuild."
 BIN_DIR ?= $(HOME)/.local/bin
+DIST_DIR ?= dist/release-evidence
 
 CC ?= cc
 CARGO ?= cargo
+CARGO_AUDIT ?= $(shell command -v cargo-audit 2>/dev/null || printf '%s/.cargo/bin/cargo-audit' "$$HOME")
 DOCKER ?= docker
 DOCKER_PLATFORM ?= linux/amd64
 DOCKER_BUILD_FLAGS ?=
@@ -31,7 +41,6 @@ STATIC_QT_TARBALL ?= jerboa-browser-static-qt-linux-amd64.tar.gz
 SOEXT := $(if $(filter Darwin,$(shell uname -s)),dylib,so)
 FUSE_SHARED_FLAG := $(if $(filter Darwin,$(shell uname -s)),-dynamiclib,-shared)
 FUSE_SHIM := libjerboa_fuse_mount.so
-BUILD_VAULT_SECRET := scheme/browser/build-vault-secret.ss
 YUBIKEY_NATIVE_SHARED := $(JERBOA_YUBIKEY_DIR)/yubikey-native/target/release/libyubikey_native.$(SOEXT)
 # `make binary` links the static archive from a temp dir and leaves no shared
 # lib behind, so build the cdylib from jerbuild's bundled crate if it isn't
@@ -50,9 +59,6 @@ lib/libjerboa_native.$(SOEXT): | check-jerbuild
 $(FUSE_SHIM): vendor/jerboa-fuse/src/mount_helper.c
 	$(CC) $(FUSE_SHARED_FLAG) -fPIC -O2 -o $@ $<
 
-$(BUILD_VAULT_SECRET):
-	sh support/gen-browser-vault-secret.sh $@
-
 vendor-yubikey:
 	sh support/ensure-vendor.sh jerboa-yubikey "$(JERBOA_YUBIKEY_URL)"
 	@test -f "$(JERBOA_YUBIKEY_LIB)/yubikey/auth.sls" || \
@@ -65,7 +71,7 @@ lib/libyubikey_native.$(SOEXT): vendor-yubikey
 
 yubikey-native: lib/libyubikey_native.$(SOEXT)
 
-.PHONY: all build binary install run test test-keymap test-minibuffer test-commands test-keys test-nav test-hint test-pass test-polish test-securestore test-adblock test-gui test-buffers repl clean help check-jerbuild check-docker static-qt docker-static-qt vault-secret rotate-vault-secret vendor-yubikey yubikey-native
+.PHONY: all build binary install run test test-all test-keymap test-minibuffer test-commands test-keys test-nav test-hint test-pass test-polish test-securestore test-adblock test-gui test-buffers test-native repl security audit verify release-evidence clean help check-jerbuild check-docker ensure-jerboa-tools static-qt docker-static-qt vendor-yubikey yubikey-native
 .DEFAULT_GOAL := help
 
 all: binary
@@ -73,12 +79,26 @@ all: binary
 check-jerbuild:
 	@test -n "$(JH)" || { echo $(JERBUILD_ERROR) >&2; exit 1; }
 
+ensure-jerboa-tools:
+	@if [ -x "$(JERBOA_TOOL_DIR)/jerbuild" ] && [ -x "$(JERBOA_TOOL_DIR)/jerboa" ]; then \
+	  echo "=== Using downloaded Jerboa toolchain: $(JERBOA_TOOL_DIR) ==="; \
+	elif command -v jerbuild >/dev/null 2>&1 && command -v jerboa >/dev/null 2>&1; then \
+	  echo "=== Using Jerboa toolchain from PATH: $$(command -v jerbuild) ==="; \
+	else \
+	  echo "=== Fetching Jerboa $(JERBOA_VERSION) release tools into $(JERBOA_TOOL_DIR) ==="; \
+	  sh support/ensure-jerboa.sh "$(JERBOA_VERSION)" "$(JERBOA_TOOL_DIR)"; \
+	fi
+	@"$(JERBUILD)" --jerboa-home >/dev/null || { \
+	  echo "ERROR: Jerboa toolchain is unavailable; set JERBUILD=/path/to/jerbuild or JERBOA_VERSION=<tag>"; \
+	  exit 1; \
+	}
+
 check-docker:
 	@command -v $(DOCKER) >/dev/null 2>&1 || \
 	  { echo "ERROR: Docker executable '$(DOCKER)' not found. Install Docker or set DOCKER=/path/to/docker."; exit 1; }
 
 # Build the self-contained native ./jerboa-browser via .jerbuild.
-binary: check-jerbuild vendor-yubikey $(FUSE_SHIM) $(BUILD_VAULT_SECRET)
+binary: check-jerbuild vendor-yubikey $(FUSE_SHIM)
 	$(JERBUILD) build
 	@echo "" && ls -lh jerboa-browser && file jerboa-browser
 
@@ -100,7 +120,10 @@ test test-keymap test-minibuffer test-gui test-buffers repl: check-jerbuild vend
 # Run the (browser) test suite in the interpreter (no binary needed). Depends on
 # the pure secure-store unit tests so they run as part of `make test`.
 test: test-securestore
-	$(JEXEC) scheme/browser-test.ss
+	QT_QPA_PLATFORM=offscreen QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu \
+	  $(JEXEC) scheme/browser-test.ss
+
+test-all: test test-keymap test-minibuffer test-commands test-keys test-nav test-hint test-pass test-polish test-adblock test-gui test-buffers test-native
 
 # Stage 8 secure-store unit tests: bookmark (de)serialize, AES-256-GCM seal/open,
 # RSA-OAEP wrap + simulated card unwrap, tamper detection, masked-DK zeroing.
@@ -118,65 +141,129 @@ test-minibuffer:
 
 # Stage 2 keymap+minibuffer+command controller functional tests (offscreen).
 test-commands:
-	QT_QPA_PLATFORM=offscreen \
+	QT_QPA_PLATFORM=offscreen QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu \
 	  $(JEXEC) scheme/browser-commands-test.ss
 
 # Stage 2 end-to-end Qt key-capture test: real synthetic Qt key events flow
 # through the app-wide filter → poll queue → controller (offscreen, headless).
 test-keys:
-	QT_QPA_PLATFORM=offscreen \
+	QT_QPA_PLATFORM=offscreen QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu \
 	  $(JEXEC) scheme/browser-keys-test.ss
 
 # Stage 3 navigation-parity functional tests: scroll/zoom/find/copy driven
 # through the command controller against rendered data: pages (offscreen).
 test-nav:
-	QT_QPA_PLATFORM=offscreen \
+	QT_QPA_PLATFORM=offscreen QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu \
 	  $(JEXEC) scheme/browser-nav-test.ss
 
 # Stage 4 hint-mode functional tests: JS link-hint overlay + follow (this/new
 # buffer) driven through the command controller against rendered data: pages.
 test-hint:
-	QT_QPA_PLATFORM=offscreen \
+	QT_QPA_PLATFORM=offscreen QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu \
 	  $(JEXEC) scheme/browser-hint-test.ss
 
 # Stage 5 password-manager functional tests: in-RAM vault round-trip + zeroing,
 # masked prompt, save/fill/forget autofill against a rendered data: form.
 test-pass:
-	QT_QPA_PLATFORM=offscreen \
+	QT_QPA_PLATFORM=offscreen QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu \
 	  $(JEXEC) scheme/browser-pass-test.ss
 
 # Stage 6 polish functional tests: in-RAM bookmarks + commands, M-x docs, the
 # status modes + echo-area wiring, and the describe-bindings help page.
 test-polish:
-	QT_QPA_PLATFORM=offscreen \
+	QT_QPA_PLATFORM=offscreen QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu \
 	  $(JEXEC) scheme/browser-polish-test.ss
 
 # Stage 7 ad/tracker blocking (adblock-rust): the process-global toggle ABI and
 # the toggle-adblock command + echo. The engine_match semantics are unit-tested
 # in C++ (cd qt-webengine/build && ctest -R adblock).
 test-adblock:
-	QT_QPA_PLATFORM=offscreen \
+	QT_QPA_PLATFORM=offscreen QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu \
 	  $(JEXEC) scheme/browser-adblock-test.ss
 
 # Offline Qt GUI / snapshot harness (headless). Writes PNGs to ./test-artifacts.
 test-gui:
-	QT_QPA_PLATFORM=offscreen \
+	QT_QPA_PLATFORM=offscreen QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu \
 	  $(JEXEC) scheme/browser-gui-test.ss
 
 # Stage 1 buffer-model functional tests (headless). Writes PNGs to ./test-artifacts.
 test-buffers:
-	QT_QPA_PLATFORM=offscreen \
+	QT_QPA_PLATFORM=offscreen QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu \
 	  $(JEXEC) scheme/browser-buffers-test.ss
 
+test-native:
+	@if [ -d qt-webengine/build ] && command -v ctest >/dev/null 2>&1; then \
+	  ctest --test-dir qt-webengine/build --output-on-failure; \
+	else \
+	  echo "native Qt/WebEngine ctest skipped: qt-webengine/build or ctest is unavailable"; \
+	fi
+
+security:
+	sh scripts/security-check.sh
+
+audit:
+	@mkdir -p "$(DIST_DIR)"
+	@if [ -x "$(CARGO_AUDIT)" ] && [ -f qt-webengine/vendor/adblock-rust-ffi/Cargo.toml ]; then \
+	  cd qt-webengine/vendor/adblock-rust-ffi && "$(CARGO_AUDIT)" audit > "$(CURDIR)/$(DIST_DIR)/rustsec-adblock-ffi.txt"; \
+	else \
+	  echo "cargo-audit not installed or adblock Cargo.toml missing; release owner must review before publication." > "$(DIST_DIR)/rustsec-adblock-ffi.txt"; \
+	fi
+	@if [ -x "$(CARGO_AUDIT)" ] && [ -f "$(JERBOA_YUBIKEY_DIR)/yubikey-native/Cargo.toml" ]; then \
+	  cd "$(JERBOA_YUBIKEY_DIR)/yubikey-native" && "$(CARGO_AUDIT)" audit > "$(CURDIR)/$(DIST_DIR)/rustsec-yubikey-native.txt"; \
+	else \
+	  echo "cargo-audit not installed or YubiKey native Cargo.toml missing; release owner must review before publication." > "$(DIST_DIR)/rustsec-yubikey-native.txt"; \
+	fi
+
+verify: test-all security audit
+
+release-evidence: verify binary
+	@rm -rf "$(DIST_DIR)"
+	@mkdir -p "$(DIST_DIR)"
+	@git status --short > "$(DIST_DIR)/git-status.txt"
+	@git rev-parse HEAD > "$(DIST_DIR)/git-commit.txt" 2>/dev/null || true
+	@{ \
+	  echo "date=$$(date -u +%Y-%m-%dT%H:%M:%SZ)"; \
+	  echo "uname=$$(uname -a)"; \
+	  echo "jerbuild=$(JERBUILD)"; \
+	  echo "jerboa_home=$(JH)"; \
+	  "$(JERBUILD)" --version 2>/dev/null || true; \
+	  command -v cmake >/dev/null 2>&1 && cmake --version | head -1 || true; \
+	  command -v "$(CARGO)" >/dev/null 2>&1 && "$(CARGO)" --version || true; \
+	} > "$(DIST_DIR)/build-environment.txt"
+	@QT_QPA_PLATFORM=offscreen QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu ./jerboa-browser version > "$(DIST_DIR)/browser-version.txt" 2>&1 || true
+	@find scheme qt-webengine/src qt-webengine/tests include support packaging -type f \( -name '*.ss' -o -name '*.sls' -o -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.h' -o -name '*.sh' -o -name '*.md' -o -name 'CMakeLists.txt' \) -print | sort | xargs shasum -a 256 > "$(DIST_DIR)/source-sha256.txt"
+	@find qt-webengine/vendor/adblock-rust-ffi "$(JERBOA_YUBIKEY_DIR)/yubikey-native" -maxdepth 2 -type f \( -name Cargo.toml -o -name Cargo.lock \) -print 2>/dev/null | sort | xargs shasum -a 256 > "$(DIST_DIR)/native-cargo-sha256.txt"
+	@shasum -a 256 Makefile .jerbuild .jerboa/security.json SECURITY.md docs/threat-model.md docs/credential-handling.md docs/ffi-boundary.md docs/webengine-sandbox.md docs/release-evidence.md scripts/security-check.sh > "$(DIST_DIR)/release-inputs-sha256.txt"
+	@shasum -a 256 jerboa-browser > "$(DIST_DIR)/binary-sha256.txt"
+	@if command -v otool >/dev/null 2>&1; then \
+	  { otool -L jerboa-browser 2>/dev/null || true; otool -L qt-webengine/build/libjerboa_browser.* 2>/dev/null || true; otool -L lib/libyubikey_native.* 2>/dev/null || true; } > "$(DIST_DIR)/native-linkage.txt"; \
+	elif command -v ldd >/dev/null 2>&1; then \
+	  { ldd jerboa-browser 2>/dev/null || true; ldd qt-webengine/build/libjerboa_browser.* 2>/dev/null || true; ldd lib/libyubikey_native.* 2>/dev/null || true; } > "$(DIST_DIR)/native-linkage.txt"; \
+	else \
+	  echo "no otool/ldd available" > "$(DIST_DIR)/native-linkage.txt"; \
+	fi
+	@if command -v "$(CARGO)" >/dev/null 2>&1 && [ -f qt-webengine/vendor/adblock-rust-ffi/Cargo.toml ]; then \
+	  "$(CARGO)" metadata --format-version 1 --manifest-path qt-webengine/vendor/adblock-rust-ffi/Cargo.toml > "$(DIST_DIR)/cargo-metadata-adblock-ffi.json"; \
+	fi
+	@if command -v "$(CARGO)" >/dev/null 2>&1 && [ -f "$(JERBOA_YUBIKEY_DIR)/yubikey-native/Cargo.toml" ]; then \
+	  "$(CARGO)" metadata --format-version 1 --manifest-path "$(JERBOA_YUBIKEY_DIR)/yubikey-native/Cargo.toml" > "$(DIST_DIR)/cargo-metadata-yubikey-native.json"; \
+	fi
+	@if [ -x "$(CARGO_AUDIT)" ] && [ -f qt-webengine/vendor/adblock-rust-ffi/Cargo.toml ]; then \
+	  cd qt-webengine/vendor/adblock-rust-ffi && "$(CARGO_AUDIT)" audit > "$(CURDIR)/$(DIST_DIR)/rustsec-adblock-ffi.txt"; \
+	else \
+	  echo "cargo-audit not installed or adblock Cargo.toml missing; release owner must review before publication." > "$(DIST_DIR)/rustsec-adblock-ffi.txt"; \
+	fi
+	@if [ -x "$(CARGO_AUDIT)" ] && [ -f "$(JERBOA_YUBIKEY_DIR)/yubikey-native/Cargo.toml" ]; then \
+	  cd "$(JERBOA_YUBIKEY_DIR)/yubikey-native" && "$(CARGO_AUDIT)" audit > "$(CURDIR)/$(DIST_DIR)/rustsec-yubikey-native.txt"; \
+	else \
+	  echo "cargo-audit not installed or YubiKey native Cargo.toml missing; release owner must review before publication." > "$(DIST_DIR)/rustsec-yubikey-native.txt"; \
+	fi
+	@echo "release evidence written to $(DIST_DIR)"
+
 # REPL with (browser) preloaded, in the interpreter.
 repl:
 	$(JEXEC) scheme/browser-repl.ss
 
-vault-secret: $(BUILD_VAULT_SECRET)
-
-rotate-vault-secret:
-	sh support/gen-browser-vault-secret.sh --force $(BUILD_VAULT_SECRET)
-
 # Docker wrapper matching the jerboa-emacs static-qt workflow. Qt WebEngine is
 # not emitted as one fully static executable; the artifact is a bundled Linux
 # tarball with the native browser binary plus the Qt WebEngine runtime.
@@ -225,7 +312,7 @@ clean:
 	rm -f $(FUSE_SHIM)
 	rm -f lib/libyubikey_native.$(SOEXT)
 	rm -f $(STATIC_QT_TARBALL)
-	rm -rf .bcache test-artifacts
+	rm -rf .bcache test-artifacts dist
 	find scheme -name '*.so' -delete 2>/dev/null || true
 	find scheme -name '*.wpo' -delete 2>/dev/null || true
 
@@ -235,8 +322,8 @@ help:
 	@echo "  make binary   build the self-contained native ./jerboa-browser"
 	@echo "  make install  build + install to $(BIN_DIR)/jerboa-browser"
 	@echo "  make static-qt build the bundled Linux Qt/WebEngine tarball via Docker"
-	@echo "  make vault-secret create the local build vault secret, if missing"
-	@echo "  make rotate-vault-secret replace the local build vault secret"
+	@echo "  make verify   run full tests, security checks, and dependency audit"
+	@echo "  make release-evidence write release evidence under dist/release-evidence"
 	@echo "  make vendor-yubikey clone jerboa-yubikey into vendor/"
 	@echo "  make yubikey-native build lib/libyubikey_native for direct USB"
 	@echo "  make test     run the (browser) test suite (interpreter)"
diff --git a/README.md b/README.md
index fbeb644..1b073ea 100644
--- a/README.md
+++ b/README.md
@@ -133,16 +133,11 @@ Explicit overrides are still available:
 
 - `JERBOA_BROWSER_VAULT_PASSPHRASE_FILE` — 0600 file containing a recovery or
   test passphrase.
-- `JERBOA_BROWSER_VAULT_PASSPHRASE` — local testing only.
 - `JERBOA_BROWSER_VAULT_UNLOCK=prompt` — terminal passphrase prompt.
-- `JERBOA_BROWSER_VAULT_UNLOCK=build-secret` — development fallback.
 
-The build-secret fallback is not production-grade: `make binary` creates
-`scheme/browser/build-vault-secret.ss` if it is missing, and that generated,
-ignored module holds a random build-local passphrase as masked bytes rather
-than a printable string. Normal rebuilds preserve it; `make rotate-vault-secret`
-replaces it. Anyone with the generated source or binary can still recover that
-secret, so use it only for development or migration.
+The browser intentionally does not accept a vault passphrase through environment
+variables and does not embed a build-local fallback secret. Use the YubiKey PIV
+default, a protected passphrase file, or the interactive prompt.
 
 ## Build / test
 
@@ -152,6 +147,8 @@ make static-qt        # bundled Linux Qt/WebEngine tarball via Docker (linux/amd
 make vendor-yubikey   # clone https://git.sr.ht/~lisp/jerboa-yubikey into vendor/
 make yubikey-native   # build lib/libyubikey_native for direct USB YubiKey access
 make test             # (browser) suite + secure-store unit tests; also test-{keymap,…,polish}
+make verify           # full test set, security checks, and dependency audit evidence
+make release-evidence # write dist/release-evidence/ for release review
 make test-securestore # just the pure secure-store crypto/format unit tests
 make test-gui         # offline Qt snapshot harness (offscreen, headless)
 ```
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..897406a
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,43 @@
+# Security Policy
+
+`jerboa-browser` is security-sensitive. It embeds Qt WebEngine, exposes native
+FFI, handles website credentials, and can persist browser profile data. Treat it
+as private/experimental until the release gates below are complete.
+
+## Supported Status
+
+No public production-support commitment exists yet. A public production release
+requires:
+
+- `make verify` passes on a clean checkout.
+- `make release-evidence` produces a complete `dist/release-evidence/` bundle.
+- `jerboa_security_scan` reports no medium-or-higher findings in non-vendored
+  production source.
+- The Qt WebEngine backend builds and its native tests pass on every supported
+  target OS.
+- RustSec/native dependency audits are clean or documented as accepted release
+  risks.
+- Target-OS evidence confirms the Chromium/Qt WebEngine sandbox is enabled.
+
+## Security Expectations
+
+- Web content is hostile. Do not grant network, downloads, clipboard, file, or
+  persistent-storage capabilities unless the caller explicitly requested them.
+- The default WebEngine profile must remain off-record. Persistent cookies,
+  cache, local storage, and downloads require the browser-owned encrypted vault.
+- Do not accept browser-profile vault secrets through process environment
+  variables and do not embed build-local fallback secrets.
+- Website passwords stay RAM-only and must not be written to profile storage,
+  config files, logs, command lines, or environment variables.
+- Keep Qt WebEngine's sandbox enabled. Do not set `QTWEBENGINE_DISABLE_SANDBOX`
+  in code, packaging, docs, or CI.
+- Keep application Scheme source on `(scheme)`/Jerboa imports. Direct
+  `(chezscheme)` imports are denied in production/test source.
+- Native boundaries must be documented, length-checked, and covered by release
+  evidence.
+
+## Reporting
+
+Before public release, report issues privately to the repository owner. After a
+public release, replace this section with a dedicated advisory contact,
+supported versions, and disclosure window.
diff --git a/build-binary.ss b/build-binary.ss
index 4e3d53a..dc18544 100644
--- a/build-binary.ss
+++ b/build-binary.ss
@@ -15,7 +15,10 @@
 ;;; Compiled library objects go to ./.bcache so the read-only Jerboa tree is
 ;;; never written to.
 
-(import (chezscheme))
+(import (scheme)
+        (only (std security taint)
+              safe-delete-file
+              safe-system))
 
 (define repo (current-directory))
 (define bcache (format "~a/.bcache" repo))
@@ -56,7 +59,48 @@
 ;; --- library search path -------------------------------------------------
 ;; (source . object) pairs: read .ss/.sls from the repo + Jerboa stdlib, write
 ;; the compiled .so under ./.bcache (NOT into the read-only Jerboa tree).
-(system (format "rm -rf ~a" bcache))
+(define (build-path-safe? path)
+  (and (string? path)
+       (> (string-length path) 0)
+       (let loop ((i 0))
+         (cond ((= i (string-length path)) #t)
+               ((char=? (string-ref path i) #\nul) #f)
+               (else (loop (+ i 1)))))))
+
+(define (require-build-path who path)
+  (unless (build-path-safe? path)
+    (error who "invalid build path" path))
+  path)
+
+(define (shell-quote s)
+  (let ((p (open-output-string)))
+    (display "'" p)
+    (let loop ((i 0))
+      (when (< i (string-length s))
+        (let ((ch (string-ref s i)))
+          (if (char=? ch #\')
+              (display "'\\''" p)
+              (display ch p)))
+        (loop (+ i 1))))
+    (display "'" p)
+    (get-output-string p)))
+
+(define (rm-rf path)
+  (let ((checked (require-build-path 'rm-rf path)))
+    (safe-system (format "rm -rf -- ~a" (shell-quote checked)))))
+
+(define (call-with-replace-output-file path proc)
+  (let ((p (open-file-output-port
+             (require-build-path 'call-with-replace-output-file path)
+             (file-options no-fail)
+             (buffer-mode block)
+             (native-transcoder))))
+    (dynamic-wind
+      (lambda () (void))
+      (lambda () (proc p))
+      (lambda () (close-port p)))))
+
+(rm-rf bcache)
 (mkdir bcache)
 (library-directories
   (list (cons (format "~a/scheme" repo) (format "~a/scheme" bcache))
@@ -68,7 +112,7 @@
          (data (get-bytevector-all p))
          (n (bytevector-length data)))
     (close-port p)
-    (call-with-output-file out
+    (call-with-replace-output-file out
       (lambda (o)
         (fprintf o "/* Auto-generated — do not edit */\n")
         (fprintf o "static const unsigned char ~a[] = {\n" array)
@@ -80,8 +124,7 @@
             (when (= 15 (modulo i 16)) (fprintf o "\n"))
             (lp (+ i 1))))
         (fprintf o "\n};\n")
-        (fprintf o "static const unsigned int ~a = ~a;\n" size-name n))
-      'replace)
+        (fprintf o "static const unsigned int ~a = ~a;\n" size-name n)))
     (printf "  ~a: ~a bytes\n" out n)))
 
 ;; All compiled .so under dir (recursively) — the library closure to embed.
@@ -121,7 +164,7 @@
 
 ;; --- [3/4] generate C bootstrap, compile + link --------------------------
 (printf "[3/4] Compiling + linking...\n")
-(call-with-output-file "jb-main.c"
+(call-with-replace-output-file "jb-main.c"
   (lambda (o)
     (fprintf o "/* Auto-generated — do not edit */\n")
     (fprintf o "#define _GNU_SOURCE\n#include <stdlib.h>\n#include <stdio.h>\n")
@@ -158,11 +201,15 @@
     (fprintf o "  Sregister_boot_file_bytes(\"jerboa-browser\", (void*)jb_boot_data, jb_boot_size);\n")
     (fprintf o "  Sbuild_heap(NULL, NULL);\n")
     (fprintf o "  int status = Sscheme_script(prog_path, argc, (const char **)argv);\n")
-    (fprintf o "  unlink(prog_path); Sscheme_deinit(); return status;\n}\n"))
-  'replace)
+    (fprintf o "  unlink(prog_path); Sscheme_deinit(); return status;\n}\n")))
 
 (define cc (or (getenv "CC") "cc"))
-(define (sh cmd) (printf "  ~a\n" cmd) (let ((rc (system cmd))) (unless (= rc 0) (printf "Error (rc=~a)\n" rc) (exit 1))))
+(define (sh cmd)
+  (printf "  ~a\n" cmd)
+  (let ((rc (safe-system cmd)))
+    (unless (= rc 0)
+      (printf "Error (rc=~a)\n" rc)
+      (exit 1))))
 
 (sh (format "~a -c -O2 -I~a -o jb-main.o jb-main.c" cc chez-dir))
 (if macos?
@@ -181,9 +228,9 @@
 
 ;; --- [4/4] cleanup -------------------------------------------------------
 (printf "[4/4] Cleanup...\n")
-(for-each (lambda (f) (when (file-exists? f) (delete-file f)))
+(for-each (lambda (f) (when (file-exists? f) (safe-delete-file f)))
   '("jb-main.c" "jb-main.o" "jb_program.h" "jb_petite_boot.h" "jb_scheme_boot.h"
     "jb_boot.h" "jerboa-browser.boot" "scheme/browser-main.so"))
-(system (format "rm -rf ~a" bcache))
+(rm-rf bcache)
 
 (printf "\nDone! Binary: ./jerboa-browser\n")
diff --git a/docs/credential-handling.md b/docs/credential-handling.md
new file mode 100644
index 0000000..5bbb768
--- /dev/null
+++ b/docs/credential-handling.md
@@ -0,0 +1,46 @@
+# Credential Handling
+
+## Website Passwords
+
+The password manager in `(browser passwords)` is intentionally RAM-only. It does
+not write credentials to config files, environment variables, command lines, or
+the browser profile. Stored secrets are XOR-masked with a per-session pad and
+wiped after each `vault-with-secret` use.
+
+Autofill reconstitutes plaintext only long enough to inject into the selected
+page form. A compromised page that receives autofill can read the filled fields,
+so autofill remains an explicit user action.
+
+## Bookmarks
+
+Bookmarks may persist through `(browser secure-store)`. The store uses a random
+256-bit data key with AES-256-GCM. The data key is wrapped to the YubiKey PIV
+slot public key with RSA-OAEP-SHA256; unlock requires the YubiKey, PIN, and
+touch when configured by the slot policy.
+
+The in-RAM data-key session mirrors the password vault: the data key is masked
+with a random pad and wiped when locked.
+
+## Browser Profile Vault
+
+Persistent WebEngine profile data is opt-in with `JERBOA_BROWSER_VAULT=1`. The
+browser creates its own encrypted FUSE vault and points Qt cache, storage, and
+download directories inside that mount before opening the WebEngine profile.
+
+Production unlock order:
+
+- Default: YubiKey PIV-derived passphrase.
+- Recovery/testing: `JERBOA_BROWSER_VAULT_PASSPHRASE_FILE`, expected to be a
+  local 0600 file.
+- Testing: `JERBOA_BROWSER_VAULT_UNLOCK=prompt`, which reads from the terminal
+  with echo disabled.
+
+The browser does not accept profile-vault passphrases through process
+environment variables and does not embed a generated build-local secret. A
+release checkout must not contain `scheme/browser/build-vault-secret.ss`.
+
+## Diagnostics
+
+Diagnostics may record URLs, page titles, dependency versions, and linkage, but
+must not record passwords, PIV PINs, data keys, passphrase file contents,
+cookies, local storage, or bookmark plaintext.
diff --git a/docs/ffi-boundary.md b/docs/ffi-boundary.md
new file mode 100644
index 0000000..f58b62c
--- /dev/null
+++ b/docs/ffi-boundary.md
@@ -0,0 +1,40 @@
+# FFI Boundary
+
+## Qt WebEngine Backend
+
+The Scheme `(browser)` library binds the C ABI implemented under
+`qt-webengine/src`. The ABI uses integer handles for contexts, views, and
+windows; the native registry validates handle kind before each operation.
+
+FFI expectations:
+
+- Load `libjerboa_browser` through `(jerboa ffi)` `load-shared-object*`.
+- Use `define-c-lambda` bindings instead of direct Chez `foreign-procedure`
+  imports in application libraries.
+- Convert strings through explicit UTF-8/length-aware wrappers in the C++ shim.
+- Return tagged `(ok ...)` or `(err ...)` values at the Scheme boundary.
+- Treat WebEngine page content as hostile even when the Scheme caller is
+  trusted.
+
+## Native Dependency Boundaries
+
+- `libjerboa_browser` wraps Qt WebEngine and the adblock-rust FFI.
+- `libyubikey_native` handles direct USB CCID/FIDO2 support when present.
+- `libjerboa_fuse_mount.so` is the browser-owned encrypted vault mount helper.
+- `libjerboa_native` provides Rust crypto used by secure-store tests and builds.
+
+All native dependencies must be represented in release evidence with source or
+Cargo lockfile hashes, RustSec status where applicable, and dynamic linkage.
+
+## Memory and Ownership
+
+Scheme owns high-level controller state. Native code owns WebEngine objects and
+frees them through explicit close calls. Returned native strings are copied at
+the Scheme boundary or borrowed only long enough to construct a Scheme value.
+
+Release review should re-check:
+
+- Stale-handle rejection after close.
+- UTF-8/NUL/length handling for URL, JavaScript, title, status, and path inputs.
+- Adblock FFI pointer cleanup for redirect buffers.
+- Context destruction order: views before profile/interceptor cleanup.
diff --git a/docs/release-evidence.md b/docs/release-evidence.md
new file mode 100644
index 0000000..1b84e95
--- /dev/null
+++ b/docs/release-evidence.md
@@ -0,0 +1,28 @@
+# Release Evidence
+
+Run:
+
+```sh
+make release-evidence
+```
+
+The target runs `make verify`, builds `./jerboa-browser`, and writes evidence to
+`dist/release-evidence/`.
+
+Expected files include:
+