Security hardening and release readiness

ober

7118a9c32951e677e8136841a5eafa9b846c9b39

diff --git a/.jerboa/security.json b/.jerboa/security.json
index d57b732..9c77ed8 100644
--- a/.jerboa/security.json
+++ b/.jerboa/security.json
@@ -15,6 +15,7 @@
     "ffi": { "allowed": true, "requireLazyLoading": true, "requireBoundaryDocs": true },
     "browser": {
       "chromiumSandbox": "required",
+      "targetSandboxProof": "marker-validated-fail-closed",
       "defaultProfile": "off-record",
       "persistentProfileRequiresEncryptedVault": true,
       "envPassphrase": "deny"
diff --git a/Makefile b/Makefile
index 1a743c0..dc20654 100644
--- a/Makefile
+++ b/Makefile
@@ -3,8 +3,11 @@
 # release toolchain; developer machines can still override JERBUILD explicitly.
 JERBOA_VERSION ?= v0.2.3
 JERBOA_TOOL_DIR ?= $(CURDIR)/.jerboa/bin
+JERBOA_SIBLING_TOOL_DIR ?= $(CURDIR)/../jerboa/dist
 JERBUILD ?= $(shell if [ -x "$(JERBOA_TOOL_DIR)/jerbuild" ] && [ -x "$(JERBOA_TOOL_DIR)/jerboa" ]; then \
 	printf '%s\n' "$(JERBOA_TOOL_DIR)/jerbuild"; \
+elif [ -x "$(JERBOA_SIBLING_TOOL_DIR)/jerbuild" ] && [ -x "$(JERBOA_SIBLING_TOOL_DIR)/jerboa" ]; then \
+	printf '%s\n' "$(JERBOA_SIBLING_TOOL_DIR)/jerbuild"; \
 elif command -v jerbuild >/dev/null 2>&1 && command -v jerboa >/dev/null 2>&1; then \
 	command -v jerbuild; \
 else \
@@ -17,7 +20,12 @@ 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 unavailable (or '$(JERBUILD) --jerboa-home' failed). Run make ensure-jerboa-tools, install jerbuild, or set JERBUILD=/path/to/jerbuild."
 BIN_DIR ?= $(HOME)/.local/bin
+BIN ?= jerboa-browser
 DIST_DIR ?= dist/release-evidence
+SBOM_DIR ?= dist/sbom
+REPRO_DIR ?= dist/reproducibility
+BINARY_SMOKE_DIR ?= dist/binary-smoke
+SANDBOX_DIR ?= dist/webengine-sandbox
 
 CC ?= cc
 CARGO ?= cargo
@@ -71,7 +79,7 @@ lib/libyubikey_native.$(SOEXT): vendor-yubikey
 
 yubikey-native: lib/libyubikey_native.$(SOEXT)
 
-.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
+.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 sbom reproducibility-report binary-smoke webengine-sandbox-evidence 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
@@ -82,6 +90,8 @@ check-jerbuild:
 ensure-jerboa-tools:
 	@if [ -x "$(JERBOA_TOOL_DIR)/jerbuild" ] && [ -x "$(JERBOA_TOOL_DIR)/jerboa" ]; then \
 	  echo "=== Using downloaded Jerboa toolchain: $(JERBOA_TOOL_DIR) ==="; \
+	elif [ -x "$(JERBOA_SIBLING_TOOL_DIR)/jerbuild" ] && [ -x "$(JERBOA_SIBLING_TOOL_DIR)/jerboa" ]; then \
+	  echo "=== Using sibling Jerboa release toolchain: $(JERBOA_SIBLING_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 \
@@ -214,9 +224,42 @@ audit:
 	  echo "cargo-audit not installed or YubiKey native Cargo.toml missing; release owner must review before publication." > "$(DIST_DIR)/rustsec-yubikey-native.txt"; \
 	fi
 
+sbom:
+	JBROWSER_SBOM_DIR="$(SBOM_DIR)" \
+	JERBUILD="$(JERBUILD)" \
+	BIN="$(BIN)" \
+	CARGO="$(CARGO)" \
+	STATIC_QT_TARBALL="$(STATIC_QT_TARBALL)" \
+	FUSE_SHIM="$(FUSE_SHIM)" \
+	sh scripts/sbom.sh
+
+reproducibility-report:
+	JBROWSER_REPRO_DIR="$(REPRO_DIR)" \
+	JERBUILD="$(JERBUILD)" \
+	BIN="$(BIN)" \
+	sh scripts/reproducibility-report.sh
+
+binary-smoke:
+	@if [ ! -x "./$(BIN)" ]; then $(MAKE) --no-print-directory binary; fi
+	JBROWSER_BINARY_SMOKE_DIR="$(BINARY_SMOKE_DIR)" \
+	BIN="$(BIN)" \
+	sh scripts/binary-smoke.sh
+
+webengine-sandbox-evidence:
+	JBROWSER_SANDBOX_DIR="$(SANDBOX_DIR)" \
+	JBROWSER_RUN_WEBENGINE_PROCESS_SMOKE="$(JBROWSER_RUN_WEBENGINE_PROCESS_SMOKE)" \
+	JBROWSER_WEBENGINE_PROCESS_SMOKE_MS="$(JBROWSER_WEBENGINE_PROCESS_SMOKE_MS)" \
+	JBROWSER_WEBENGINE_PROCESS_SMOKE_URL="$(JBROWSER_WEBENGINE_PROCESS_SMOKE_URL)" \
+	JBROWSER_WEBENGINE_PROCESS_SMOKE_SAMPLES="$(JBROWSER_WEBENGINE_PROCESS_SMOKE_SAMPLES)" \
+	JBROWSER_REQUIRE_WEBENGINE_PROCESS_SMOKE="$(JBROWSER_REQUIRE_WEBENGINE_PROCESS_SMOKE)" \
+	JBROWSER_TARGET_SANDBOX_PROOF_FILE="$(JBROWSER_TARGET_SANDBOX_PROOF_FILE)" \
+	JBROWSER_REQUIRE_TARGET_SANDBOX_PROOF="$(JBROWSER_REQUIRE_TARGET_SANDBOX_PROOF)" \
+	BIN="$(BIN)" \
+	sh scripts/webengine-sandbox-evidence.sh
+
 verify: test-all security audit
 
-release-evidence: verify binary
+release-evidence: verify reproducibility-report sbom webengine-sandbox-evidence binary-smoke
 	@rm -rf "$(DIST_DIR)"
 	@mkdir -p "$(DIST_DIR)"
 	@git status --short > "$(DIST_DIR)/git-status.txt"
@@ -233,12 +276,12 @@ release-evidence: verify binary
 	@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"
+	@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 scripts/sbom.sh scripts/reproducibility-report.sh scripts/binary-smoke.sh scripts/webengine-sandbox-evidence.sh > "$(DIST_DIR)/release-inputs-sha256.txt"
+	@shasum -a 256 "$(BIN)" > "$(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"; \
+	  { otool -L "$(BIN)" 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"; \
+	  { ldd "$(BIN)" 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
@@ -258,6 +301,17 @@ release-evidence: verify binary
 	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
+	@rm -rf "$(DIST_DIR)/sbom" "$(DIST_DIR)/reproducibility" "$(DIST_DIR)/binary-smoke" "$(DIST_DIR)/webengine-sandbox"
+	@cp -R "$(SBOM_DIR)" "$(DIST_DIR)/sbom"
+	@cp -R "$(REPRO_DIR)" "$(DIST_DIR)/reproducibility"
+	@cp -R "$(BINARY_SMOKE_DIR)" "$(DIST_DIR)/binary-smoke"
+	@cp -R "$(SANDBOX_DIR)" "$(DIST_DIR)/webengine-sandbox"
+	@grep -q '^status=match$$' "$(DIST_DIR)/reproducibility/result.txt"
+	@grep -q '^status=pass$$' "$(DIST_DIR)/binary-smoke/status.txt"
+	@if grep -q '^status=failed-current$$' "$(DIST_DIR)/webengine-sandbox/status.txt"; then \
+	  echo "current WebEngine sandbox evidence failed; see $(DIST_DIR)/webengine-sandbox/status.txt"; \
+	  exit 1; \
+	fi
 	@echo "release evidence written to $(DIST_DIR)"
 
 # REPL with (browser) preloaded, in the interpreter.
@@ -323,6 +377,10 @@ help:
 	@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 verify   run full tests, security checks, and dependency audit"
+	@echo "  make sbom     write SBOM/provenance evidence under dist/sbom"
+	@echo "  make reproducibility-report rebuild the browser binary twice"
+	@echo "  make binary-smoke start the binary through offscreen version"
+	@echo "  make webengine-sandbox-evidence record WebEngine sandbox status"
 	@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"
diff --git a/SECURITY.md b/SECURITY.md
index 897406a..b92fac4 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -13,11 +13,21 @@ requires:
 - `make release-evidence` produces a complete `dist/release-evidence/` bundle.
 - `jerboa_security_scan` reports no medium-or-higher findings in non-vendored
   production source.
+- SBOM/provenance, repeated binary reproducibility, no-secret binary-smoke, and
+  WebEngine sandbox-status evidence are present in the release bundle.
 - 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.
+- Target-OS evidence confirms the Chromium/Qt WebEngine sandbox is enabled. Run
+  WebEngine evidence with `JBROWSER_RUN_WEBENGINE_PROCESS_SMOKE=1`,
+  `JBROWSER_REQUIRE_WEBENGINE_PROCESS_SMOKE=1`,
+  `JBROWSER_TARGET_SANDBOX_PROOF_FILE=/path/to/target-sandbox-proof.txt`, and
+  `JBROWSER_REQUIRE_TARGET_SANDBOX_PROOF=1` in the release environment. Required
+  target proof is marker-validated for observed helper runtime, absence of
+  sandbox-disable args, target OS sandbox inspection, and production sandbox
+  status; missing or incomplete proof fails closed with
+  `status=blocked-target-proof`.
 
 ## Security Expectations
 
diff --git a/docs/release-evidence.md b/docs/release-evidence.md
index 1b84e95..33a54ba 100644
--- a/docs/release-evidence.md
+++ b/docs/release-evidence.md
@@ -6,8 +6,49 @@ Run:
 make release-evidence
 ```
 
-The target runs `make verify`, builds `./jerboa-browser`, and writes evidence to
-`dist/release-evidence/`.
+The target runs `make verify`, refreshes reproducibility/SBOM/sandbox/binary
+smoke evidence, and writes evidence to `dist/release-evidence/`.
+
+`make release-evidence` also runs:
+
+- `make reproducibility-report`, which rebuilds `./jerboa-browser` twice using a
+  fixed Jerbuild object directory without invoking the destructive `make clean`
+  target after an unmeasured warmup and stabilization build. It compares the
+  shipped binary, generated Jerbuild inputs, native runtime inputs, and source
+  manifest.
+- `make sbom`, which records source/release input hashes, Qt WebEngine version
+  status, native runtime hashes, Cargo metadata, static Qt tarball status, and
+  helper-process presence when visible on the local platform.
+- `make binary-smoke`, which starts the shipped binary through the no-secret
+  offscreen `version` path and records macOS code-signature status when
+  available.
+- `make webengine-sandbox-evidence`, which records source-policy status, Qt
+  version status, helper-process presence, optional helper-process smoke, and
+  explicit target-host sandbox blockers.
+
+For release-host WebEngine evidence, run:
+
+```sh
+JBROWSER_RUN_WEBENGINE_PROCESS_SMOKE=1 \
+JBROWSER_REQUIRE_WEBENGINE_PROCESS_SMOKE=1 \
+JBROWSER_TARGET_SANDBOX_PROOF_FILE=/path/to/target-sandbox-proof.txt \
+JBROWSER_REQUIRE_TARGET_SANDBOX_PROOF=1 \
+make webengine-sandbox-evidence
+```
+
+The process smoke uses `jerboa-browser browse about:blank` with
+`JWB_BROWSE_MS=6000`, records `QtWebEngineProcess` argv, rejects `--no-sandbox`,
+and writes `process_smoke_status`, `qtwebengineprocess_argv_status`,
+`platform_sandbox_indicator_status`, `target_sandbox_proof_status`,
+`target_os_sandbox_status`, and `production_sandbox_status` into
+`webengine-sandbox/status.txt`. Target proof files must include
+`target_os_sandbox_status=target-evidence-recorded`,
+`target_process_sandbox_status=target-evidence-recorded`,
+`qtwebengineprocess_runtime_status=observed`,
+`qtwebengineprocess_argv_status=no-sandbox-disable-arg-absent`,
+`platform_sandbox_indicator_status=target-sandbox-indicator-recorded`, and
+`production_sandbox_status=target-evidence-recorded`; missing or incomplete
+required target proof exits with `status=blocked-target-proof`.
 
 Expected files include:
 
@@ -22,6 +63,13 @@ Expected files include:
 - Cargo metadata for adblock and YubiKey native crates when available
 - RustSec audit outputs for adblock and YubiKey native crates when
   `cargo-audit` is installed
+- `sbom/`
+- `reproducibility/`
+- `binary-smoke/`
+- `webengine-sandbox/`
+- `webengine-sandbox/target-sandbox-proof.txt` and
+  `webengine-sandbox/target-sandbox-proof.txt.sha256` when target proof is
+  supplied
 
 Release owners must treat missing `cargo-audit`, skipped Qt native tests, or
 missing target-OS sandbox/process evidence as production blockers, even when the
diff --git a/docs/threat-model.md b/docs/threat-model.md
index d7bda73..275f8c8 100644
--- a/docs/threat-model.md
+++ b/docs/threat-model.md
@@ -71,4 +71,10 @@ vendored YubiKey/FUSE dependencies.
 - Missing project release evidence or dependency audit output.
 - Hosted CI that cannot build the Qt backend and run the browser verification
   suite.
-- No target-OS smoke evidence for sandboxed WebEngine helper processes.
+- No target-OS smoke evidence for sandboxed WebEngine helper processes. Release
+  hosts must record `QtWebEngineProcess` argv and native sandbox inspection
+  output through the marker-validated `JBROWSER_TARGET_SANDBOX_PROOF_FILE`;
+  missing or incomplete required proof fails closed as
+  `status=blocked-target-proof`.
+- No repeated binary reproducibility, SBOM/provenance, or no-secret binary
+  startup evidence for the release candidate.
diff --git a/docs/webengine-sandbox.md b/docs/webengine-sandbox.md
index 6e9b24c..e50d754 100644
--- a/docs/webengine-sandbox.md
+++ b/docs/webengine-sandbox.md
@@ -36,6 +36,51 @@ Release evidence must record:
 - Linkage for `jerboa-browser`, `libjerboa_browser`, `libyubikey_native`, and
   FUSE helper libraries when present.
 - Confirmation that sandbox-disable knobs are absent from release source.
+- `dist/release-evidence/webengine-sandbox/status.txt`, with process-smoke and
+  target sandbox proof statuses.
 
-Target operating systems still need live sandbox/process evidence before a
-public production claim.
+By default, the sandbox evidence target records source policy, local Qt
+versions, helper presence, and explicit target-proof blockers. On a host with a
+built browser and Qt WebEngine runtime, release owners can also run a hermetic
+helper-process smoke:
+
+```sh
+JBROWSER_RUN_WEBENGINE_PROCESS_SMOKE=1 \
+JBROWSER_REQUIRE_WEBENGINE_PROCESS_SMOKE=1 \
+make webengine-sandbox-evidence
+```
+
+That smoke launches `jerboa-browser browse about:blank` with
+`JWB_BROWSE_MS=6000`, observes a direct `QtWebEngineProcess` child, records its
+argv in `qtwebengineprocess-argv.txt`, and fails if the helper argv contains
+`--no-sandbox` or `QTWEBENGINE_DISABLE_SANDBOX`. On Darwin it also records the
+Chromium `--seatbelt-client=` argv marker as a local sandbox indicator. This is
+not, by itself, a production sandbox claim.
+
+For release-host evidence, capture the target OS's native sandbox proof and pass
+it into the evidence bundle:
+
+```sh
+JBROWSER_RUN_WEBENGINE_PROCESS_SMOKE=1 \
+JBROWSER_REQUIRE_WEBENGINE_PROCESS_SMOKE=1 \
+JBROWSER_TARGET_SANDBOX_PROOF_FILE=/path/to/target-sandbox-proof.txt \
+JBROWSER_REQUIRE_TARGET_SANDBOX_PROOF=1 \
+make webengine-sandbox-evidence
+```
+
+`JBROWSER_TARGET_SANDBOX_PROOF_FILE` should contain the platform-specific proof
+used by the release owner, for example target process listings plus OS sandbox
+inspection output. The proof is marker-validated and must contain:
+
+- `target_os_sandbox_status=target-evidence-recorded`
+- `target_process_sandbox_status=target-evidence-recorded`
+- `qtwebengineprocess_runtime_status=observed`
+- `qtwebengineprocess_argv_status=no-sandbox-disable-arg-absent`
+- `platform_sandbox_indicator_status=target-sandbox-indicator-recorded`
+- `production_sandbox_status=target-evidence-recorded`
+
+Missing, empty, or marker-incomplete required proofs fail closed with
+`status=blocked-target-proof`. Accepted target proofs are copied into the
+evidence directory as `target-sandbox-proof.txt` with a SHA-256 sidecar. Target
+operating systems still need live sandbox/process evidence before a public
+production claim.
diff --git a/jerboa-browser.md b/jerboa-browser.md
index 52b52b1..26be80f 100644
--- a/jerboa-browser.md
+++ b/jerboa-browser.md
@@ -360,7 +360,7 @@ recommended next action in `browser/notes/qt-webengine-pin.md`.
 
 Goal: prove the local machine can build and run Qt WebEngine.
 
-Result: PASS on macOS 15.5 arm64, Qt 6.11.0 (Homebrew). Probe loaded
+Result: PASS on macOS/Darwin arm64, Qt 6.11.1 (Homebrew). Probe loaded
 `https://example.com` and `runJavaScript("document.title")` returned
 `"Example Domain"` with the sandbox enabled. Full host facts in
 `notes/qt-webengine-pin.md`.
@@ -405,7 +405,7 @@ Result: PASS. `include/jerboa_browser.h` (full C ABI), `qt-webengine/`
 (`CMakeLists.txt`, `src/browser_internal.h`, `src/browser_abi.cpp`,
 `src/main_demo.cpp`), `README.md` created. `cmake --build` produces
 `libjerboa_browser.dylib` + `jwb_demo`; demo prints
-`jerboa-browser 0.0.1 (Qt WebEngine 6.11.0)` and `runtime initialized OK`
+`jerboa-browser 0.0.1 (Qt WebEngine 6.11.1)` and `runtime initialized OK`
 (exit 0). `jwb_runtime_init` makes the single QApplication (reuses an existing
 one for the Ticket 2.2 host case).
 
@@ -744,11 +744,11 @@ engine, widget, JavaScript bridge, and process model already exist.
 
 ## Open blanks to fill during implementation
 
-- Exact Qt version: **6.11.0** (Homebrew)
-- Exact Qt installation path: **/opt/homebrew** (kegs under `/opt/homebrew/Cellar/qt*/6.11.0`); pass `-DCMAKE_PREFIX_PATH=/opt/homebrew`
+- Exact Qt version: **6.11.1** (Homebrew)
+- Exact Qt installation path: **/opt/homebrew** (kegs under `/opt/homebrew/Cellar/qt*/6.11.1`); pass `-DCMAKE_PREFIX_PATH=/opt/homebrew`
 - Exact required Qt modules: CMake components **Core, Widgets, WebEngineWidgets** (+ transitive WebEngineCore, Quick, Gui, Network, WebChannel, Positioning)
 - Exact CMake package names: `find_package(Qt6 REQUIRED COMPONENTS Core Widgets WebEngineWidgets)`
-- Exact `QtWebEngineProcess` bundle path: **inside the framework** — `/opt/homebrew/Cellar/qtwebengine/6.11.0/lib/QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app` (Chromium `.pak`s in the same framework's `Resources/`)
+- Exact `QtWebEngineProcess` bundle path: **inside the framework** — `/opt/homebrew/Cellar/qtwebengine/6.11.1/lib/QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app` (Chromium `.pak`s in the same framework's `Resources/`)
 - Exact macOS signing/notarization requirements: TBD (Ticket 3.1)
 - Exact Linux package format: TBD (not this host; macOS-first)
 - Exact policy hooks available in this Qt version: TBD (Ticket 1.3)
diff --git a/notes/qt-webengine-pin.md b/notes/qt-webengine-pin.md
index b5fc1e9..dacffdc 100644
--- a/notes/qt-webengine-pin.md
+++ b/notes/qt-webengine-pin.md
@@ -7,13 +7,15 @@ the host or Qt version changes, or if a stop condition is later hit.
 **Status: PASS** — probe built and ran; `runJavaScript("document.title")`
 returned `"Example Domain"` from `https://example.com`; exit code 0; Chromium
 sandbox left enabled (no `QTWEBENGINE_DISABLE_SANDBOX`). No stop condition hit.
-Recorded 2026-05-24.
+Recorded 2026-05-24 and refreshed 2026-06-22 after the Homebrew Qt 6.11.1
+upgrade. Current `pkg-config` and browser runtime evidence report Qt
+WebEngine 6.11.1.
 
 ## Host
 
-- OS: macOS 15.5 (build 24F74)
+- OS: macOS/Darwin host (current release evidence: Darwin 25.5.0)
 - Arch: arm64 (Apple Silicon, `RELEASE_ARM64_T6041`)
-- Kernel: Darwin 24.5.0
+- Kernel: Darwin 25.5.0
 
 ## Toolchain
 
@@ -23,7 +25,7 @@ Recorded 2026-05-24.
 
 ## Qt
 
-- Qt version: **6.11.0**, installed via Homebrew at `/opt/homebrew`
+- Qt version: **6.11.1**, installed via Homebrew at `/opt/homebrew`
 - Install kind: Homebrew split kegs (NOT a monolithic Qt installer tree)
 - CMake package entry point: `/opt/homebrew/lib/cmake/Qt6/Qt6Config.cmake`
   (component configs symlinked from the per-keg dirs; a plain `find` over
@@ -37,7 +39,7 @@ Recorded 2026-05-24.
 `WebEngineWidgets` transitively pulls `WebEngineCore`, `Quick`, `Gui`,
 `Network`, `WebChannel`, `Positioning`. Backing Homebrew formulae present:
 `qtbase`, `qtdeclarative`, `qtwebchannel`, `qtwebengine`, `qtpositioning`
-(all 6.11.0).
+(all 6.11.1 via `pkg-config`).
 
 ## Runtime layout (macOS framework bundle — matters for packaging, Ticket 3.1)
 
@@ -45,7 +47,7 @@ On macOS, Qt WebEngine ships as a **framework**, not loose libs. The helper
 process and Chromium resources live INSIDE the framework:
 
 - `QtWebEngineProcess` (the separate render/JS process):
-  `/opt/homebrew/Cellar/qtwebengine/6.11.0/lib/QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess`
+  `/opt/homebrew/Cellar/qtwebengine/6.11.1/lib/QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess`
   - has an `.entitlements` file → macOS sandbox is applied to the helper.
 - Chromium `.pak` resources + locales:
   `.../QtWebEngineCore.framework/Versions/A/Resources/`
diff --git a/packaging/README.md b/packaging/README.md
index cfeb90c..1197896 100644
--- a/packaging/README.md
+++ b/packaging/README.md
@@ -19,12 +19,12 @@ any still references `/opt/homebrew`.
 
 ### Verified acceptance
 
-Built on macOS 15 (Apple Silicon), Qt 6.11.0. Copied to a fresh `mktemp -d`
+Built on macOS/Darwin Apple Silicon, Qt 6.11.1. Copied to a fresh `mktemp -d`
 with no `/opt/homebrew` on the search path and launched directly:
 
 ```
 $ JWB_HOST_SMOKE=1 JerboaBrowser.app/Contents/MacOS/JerboaBrowser
-jerboa-browser 0.0.1 (Qt WebEngine 6.11.0)
+jerboa-browser 0.0.1 (Qt WebEngine 6.11.1)
 [PASS] runtime init reuses host QApplication
 [PASS] no second QApplication created
 [PASS] show browser pane
diff --git a/scripts/binary-smoke.sh b/scripts/binary-smoke.sh
new file mode 100755
index 0000000..a521aad
--- /dev/null
+++ b/scripts/binary-smoke.sh
@@ -0,0 +1,66 @@
+#!/bin/sh
+set -eu
+
+ROOT=$(cd "$(dirname "$0")/.." && pwd)
+cd "$ROOT"
+
+OUT=${JBROWSER_BINARY_SMOKE_DIR:-${BINARY_SMOKE_DIR:-dist/binary-smoke}}
+BIN=${BIN:-jerboa-browser}
+PLATFORM=$(uname -s)
+REQUIRE_CODESIGN=${JBROWSER_REQUIRE_CODESIGN:-0}
+
+rm -rf "$OUT"
+mkdir -p "$OUT"
+
+write_status() {
+    status=$1
+    version_status=$2
+    exit_code=$3
+    codesign_status=$4
+    reason=$5
+    {
+        echo "repo=jerboa-browser"
+        echo "platform=$PLATFORM"
+        echo "status=$status"
+        echo "offscreen_version_status=$version_status"
+        echo "version_exit_code=$exit_code"
+        echo "codesign_status=$codesign_status"
+        echo "reason=$reason"
+    } > "$OUT/status.txt"
+}
+
+if [ ! -x "./$BIN" ]; then
+    write_status "missing-binary" "missing-binary" "not-run" "not-run" "$BIN is not present or executable"
+    exit 1
+fi
+
+set +e
+QT_QPA_PLATFORM=offscreen QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu "./$BIN" version > "$OUT/version.stdout" 2> "$OUT/version.stderr"
+rc=$?
+set -e
+
+version_status=fail
+if [ "$rc" -eq 0 ] && grep -q 'jerboa-browser' "$OUT/version.stdout" "$OUT/version.stderr" 2>/dev/null; then
+    version_status=pass
+fi
+
+codesign_status=not-applicable
+if [ "$PLATFORM" = "Darwin" ] && command -v codesign >/dev/null 2>&1; then
+    if codesign --verify "./$BIN" > "$OUT/codesign.stdout" 2> "$OUT/codesign.stderr"; then
+        codesign_status=pass
+    else
+        codesign_status=blocked-not-signed
+    fi
+fi
+
+if [ "$version_status" != "pass" ]; then
+    write_status "fail" "$version_status" "$rc" "$codesign_status" "binary did not start through the no-secret offscreen version path"
+    exit 1
+fi
+
+if [ "$REQUIRE_CODESIGN" = "1" ] && [ "$codesign_status" = "blocked-not-signed" ]; then
+    write_status "fail" "$version_status" "$rc" "$codesign_status" "codesign verification is required for this release host"
+    exit 1
+fi
+
+write_status "pass" "$version_status" "$rc" "$codesign_status" "binary reached no-secret offscreen version path"
diff --git a/scripts/reproducibility-report.sh b/scripts/reproducibility-report.sh
new file mode 100755
index 0000000..3f9aa94
--- /dev/null
+++ b/scripts/reproducibility-report.sh
@@ -0,0 +1,241 @@
+#!/bin/sh
+set -eu
+
+ROOT=$(cd "$(dirname "$0")/.." && pwd)
+REPRO_DIR=${JBROWSER_REPRO_DIR:-${REPRO_DIR:-dist/reproducibility}}
+MAKE_CMD=${MAKE:-make}
+JERBUILD=${JERBUILD:-jerbuild}
+BIN=${BIN:-jerboa-browser}
+TMP_PARENT=${JBROWSER_REPRO_TMPDIR:-${TMPDIR:-/tmp}}
+TMP_DIR=$TMP_PARENT/jerboa-browser-repro-$$
+OBJ_DIR=$TMP_DIR/jerbuild-binary-browser
+WARMUP_OBJ_DIR=$TMP_DIR/jerbuild-binary-browser-warmup
+
+cleanup() {
+    rm -rf "$TMP_DIR"
+}
+trap cleanup EXIT HUP INT TERM
+
+cd "$ROOT"
+rm -rf "$TMP_DIR"
+mkdir -p "$TMP_DIR"
+
+sha256_file() {
+    if command -v sha256sum >/dev/null 2>&1; then
+        sha256sum "$1"
+    else
+        shasum -a 256 "$1"
+    fi
+}
+
+source_manifest() {
+    for path in \
+        .build.yml \
+        .dockerignore \
+        .gitignore \
+        .jerbuild \
+        .jerboa/security.json \
+        Dockerfile.qt \
+        LICENSE \
+        Makefile \
+        README.md \
+        SECURITY.md \
+        build-binary.ss \
+        docs \
+        scripts \
+        support \
+        packaging \
+        include \
+        scheme \
+        .github \
+        qt-webengine/CMakeLists.txt \
+        qt-webengine/src \
+        qt-webengine/tests \
+        qt-webengine/host \
+        qt-webengine/probe \
+        qt-webengine/vendor/adblock-rust-ffi/Cargo.toml \
+        qt-webengine/vendor/adblock-rust-ffi/Cargo.lock \
+        qt-webengine/vendor/adblock-rust-ffi/src \
+        vendor/jerboa-yubikey/lib \
+        vendor/jerboa-yubikey/yubikey-native/Cargo.toml \
+        vendor/jerboa-yubikey/yubikey-native/Cargo.lock \
+        vendor/jerboa-yubikey/yubikey-native/src \
+        vendor/jerboa-fuse/lib
+    do
+        if [ -f "$path" ]; then
+            printf '%s\n' "$path"
+        elif [ -d "$path" ]; then
+            find "$path" -type f \
+                ! -path '*/.git/*' \
+                ! -path '*/dist/*' \
+                ! -path '*/.jerboa/bin/*' \
+                ! -path '*/target/*' \
+                ! -path '*/build/*' \
+                ! -name '*.so' \
+                ! -name '*.dylib' \
+                ! -name '*.o' \
+                ! -name '*.boot' \
+                ! -name '*.wpo' \
+                ! -name '*.wp.so'
+        fi
+    done | LC_ALL=C sort -u | while IFS= read -r file; do
+        sha256_file "$file"
+    done
+}
+
+hash_generated_inputs() {
+    label=$1
+    manifest="$TMP_DIR/$label-generated-inputs.sha256"
+    if [ -d "$OBJ_DIR" ]; then
+        (
+            cd "$OBJ_DIR"
+            find . -maxdepth 1 -type f \
+                ! -name "$BIN" \
+                -print | LC_ALL=C sort | while IFS= read -r file; do
+                    sha256_file "$file"
+                done
+        ) > "$manifest"
+    else
+        printf 'missing_obj_dir=%s\n' "$OBJ_DIR" > "$manifest"
+    fi
+}
+
+native_runtime_manifest() {
+    out=$1
+    for pattern in qt-webengine/build/libjerboa_browser.* lib/libjerboa_native.* lib/libyubikey_native.* libjerboa_fuse_mount.so; do
+        for file in $pattern; do
+            if [ -f "$file" ]; then
+                sha256_file "$file"
+            fi
+        done
+    done | LC_ALL=C sort > "$out"
+}
+
+build_once() {
+    label=$1
+    rm -f "$BIN"
+    rm -rf "$OBJ_DIR"
+    mkdir -p "$OBJ_DIR"
+    env \
+        JERBUILD="$JERBUILD" \
+        JERBUILD_BINARY_OBJ_DIR="$OBJ_DIR" \
+        JERBUILD_BINARY_KEEP_OBJ_DIR=1 \
+        "$MAKE_CMD" --no-print-directory binary > "$TMP_DIR/$label-build.log" 2>&1
+    test -f "$BIN"
+    cp "$BIN" "$TMP_DIR/$label-$BIN"
+    sha256_file "$TMP_DIR/$label-$BIN" > "$TMP_DIR/$label-binary.sha256"
+    hash_generated_inputs "$label"
+    native_runtime_manifest "$TMP_DIR/$label-native-runtime.sha256"
+}
+
+warmup_once() {
+    rm -f "$BIN"
+    rm -rf "$WARMUP_OBJ_DIR"
+    mkdir -p "$WARMUP_OBJ_DIR"
+    env \
+        JERBUILD="$JERBUILD" \
+        JERBUILD_BINARY_OBJ_DIR="$WARMUP_OBJ_DIR" \
+        JERBUILD_BINARY_KEEP_OBJ_DIR=1 \
+        "$MAKE_CMD" --no-print-directory binary > "$TMP_DIR/warmup-build.log" 2>&1
+    test -f "$BIN"
+    sha256_file "$BIN" > "$TMP_DIR/warmup-binary.sha256"
+    rm -f "$BIN"
+}
+
+stabilize_once() {
+    rm -f "$BIN"
+    rm -rf "$OBJ_DIR"
+    mkdir -p "$OBJ_DIR"
+    env \
+        JERBUILD="$JERBUILD" \
+        JERBUILD_BINARY_OBJ_DIR="$OBJ_DIR" \
+        JERBUILD_BINARY_KEEP_OBJ_DIR=1 \
+        "$MAKE_CMD" --no-print-directory binary > "$TMP_DIR/stabilization-build.log" 2>&1
+    test -f "$BIN"
+    sha256_file "$BIN" > "$TMP_DIR/stabilization-binary.sha256"
+    rm -f "$BIN"
+    rm -rf "$OBJ_DIR"
+}
+
+{
+    echo "repo=jerboa-browser"
+    echo "jerbuild=$JERBUILD"
+    "$JERBUILD" --version 2>/dev/null || true
+    printf 'jerboa_home='
+    "$JERBUILD" --jerboa-home 2>/dev/null || true
+    echo "bin=$BIN"
+} > "$TMP_DIR/build-env.txt"
+
+warmup_once
+stabilize_once
+
+source_manifest > "$TMP_DIR/source-before.sha256"
+
+build_once first
+build_once second
+
+source_manifest > "$TMP_DIR/source-after.sha256"
+
+if cmp -s "$TMP_DIR/first-$BIN" "$TMP_DIR/second-$BIN"; then
+    binary_status=match
+else
+    binary_status=mismatch
+fi
+
+if cmp -s "$TMP_DIR/first-generated-inputs.sha256" "$TMP_DIR/second-generated-inputs.sha256"; then
+    generated_input_status=match
+else
+    generated_input_status=mismatch
+fi
+
+if cmp -s "$TMP_DIR/first-native-runtime.sha256" "$TMP_DIR/second-native-runtime.sha256"; then
+    native_runtime_input_status=match
+else
+    native_runtime_input_status=mismatch
+fi
+
+if cmp -s "$TMP_DIR/source-before.sha256" "$TMP_DIR/source-after.sha256"; then
+    source_manifest_status=match
+else
+    source_manifest_status=mismatch
+fi
+
+status=match
+for value in "$binary_status" "$generated_input_status" "$native_runtime_input_status" "$source_manifest_status"; do
+    if [ "$value" != "match" ]; then
+        status=mismatch
+    fi
+done
+
+{
+    echo "repo=jerboa-browser"
+    echo "binary_status=$binary_status"
+    echo "generated_input_status=$generated_input_status"
+    echo "native_runtime_input_status=$native_runtime_input_status"
+    echo "source_manifest_status=$source_manifest_status"
+    echo "jerbuild_binary_obj_dir=$OBJ_DIR"
+    echo "jerbuild_binary_keep_obj_dir=1"
+    echo "warmup_build_used=1"
+    echo "warmup_jerbuild_binary_obj_dir=$WARMUP_OBJ_DIR"
+    echo "stabilization_build_used=1"
+    echo "measurement_mode=post-warm"
+    echo "destructive_clean_used=0"
+    echo "status=$status"
+} > "$TMP_DIR/result.txt"
+
+if [ "$binary_status" != "match" ]; then
+    cmp -l "$TMP_DIR/first-$BIN" "$TMP_DIR/second-$BIN" \
+        | sed -n '1,200p' > "$TMP_DIR/binary-byte-diff-sample.txt" || true
+fi
+
+if [ "$generated_input_status" != "match" ]; then
+    diff -u "$TMP_DIR/first-generated-inputs.sha256" "$TMP_DIR/second-generated-inputs.sha256" \
+        > "$TMP_DIR/generated-inputs.diff" || true
+fi
+
+rm -rf "$REPRO_DIR"
+mkdir -p "$REPRO_DIR"
+cp -R "$TMP_DIR"/. "$REPRO_DIR"/
+
+cat "$REPRO_DIR/result.txt"
+[ "$status" = "match" ]
diff --git a/scripts/sbom.sh b/scripts/sbom.sh
new file mode 100755
index 0000000..09253d9
--- /dev/null
+++ b/scripts/sbom.sh
@@ -0,0 +1,186 @@
+#!/bin/sh
+set -eu
+
+ROOT=$(cd "$(dirname "$0")/.." && pwd)
+cd "$ROOT"
+
+OUT=${JBROWSER_SBOM_DIR:-${SBOM_DIR:-dist/sbom}}
+JERBUILD=${JERBUILD:-jerbuild}
+BIN=${BIN:-jerboa-browser}
+CARGO=${CARGO:-cargo}
+STATIC_QT_TARBALL=${STATIC_QT_TARBALL:-jerboa-browser-static-qt-linux-amd64.tar.gz}
+ADBLOCK_MANIFEST=${ADBLOCK_MANIFEST:-qt-webengine/vendor/adblock-rust-ffi/Cargo.toml}
+YUBIKEY_MANIFEST=${YUBIKEY_MANIFEST:-vendor/jerboa-yubikey/yubikey-native/Cargo.toml}
+FUSE_SHIM=${FUSE_SHIM:-libjerboa_fuse_mount.so}
+
+rm -rf "$OUT"
+mkdir -p "$OUT"
+
+sha256_file() {
+    if command -v sha256sum >/dev/null 2>&1; then
+        sha256sum "$1"
+    else
+        shasum -a 256 "$1"
+    fi
+}
+
+write_source_files() {
+    for dir in scheme include support packaging docs scripts .github qt-webengine/src qt-webengine/tests qt-webengine/host qt-webengine/probe qt-webengine/vendor/adblock-rust-ffi/src vendor/jerboa-yubikey/lib vendor/jerboa-fuse/lib; do
+        if [ -d "$dir" ]; then
+            find "$dir" -type f ! -path '*/.git/*' ! -path '*/target/*' ! -path '*/build/*'
+        fi
+    done
+
+    for file in \
+        .build.yml \
+        .dockerignore \
+        .gitignore \
+        .jerbuild \
+        .jerboa/security.json \
+        Dockerfile.qt \
+        LICENSE \
+        Makefile \
+        README.md \
+        SECURITY.md \
+        build-binary.ss \
+        qt-webengine/CMakeLists.txt \
+        qt-webengine/vendor/adblock-rust-ffi/Cargo.toml \
+        qt-webengine/vendor/adblock-rust-ffi/Cargo.lock \
+        vendor/jerboa-yubikey/yubikey-native/Cargo.toml \
+        vendor/jerboa-yubikey/yubikey-native/Cargo.lock
+    do
+        if [ -f "$file" ]; then
+            printf '%s\n' "$file"
+        fi
+    done
+}
+
+write_release_inputs() {
+    for file in \
+        .jerbuild \
+        .jerboa/security.json \
+        Dockerfile.qt \
+        LICENSE \
+        Makefile \
+        README.md \
+        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 \
+        scripts/sbom.sh \
+        scripts/reproducibility-report.sh \
+        scripts/binary-smoke.sh \
+        scripts/webengine-sandbox-evidence.sh \
+        support/ensure-jerboa.sh \
+        support/ensure-vendor.sh
+    do
+        if [ -f "$file" ]; then
+            printf '%s\n' "$file"
+        fi
+    done
+}
+
+qt_status=blocked-unavailable
+if command -v pkg-config >/dev/null 2>&1; then
+    if pkg-config --modversion Qt6Core Qt6WebEngineCore Qt6WebEngineWidgets Qt6Widgets > "$OUT/qt-pkg-config.txt" 2>"$OUT/qt-pkg-config.stderr"; then
+        qt_status=version-recorded
+    fi
+fi
+
+browser_version_status=missing-binary
+if [ -x "./$BIN" ]; then
+    if QT_QPA_PLATFORM=offscreen QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu "./$BIN" version > "$OUT/browser-version.txt" 2>&1; then
+        browser_version_status=version-recorded
+    else
+        browser_version_status=version-failed
+    fi
+fi
+
+qtwebengineprocess_status=missing
+if find /opt/homebrew -path '*QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess' -type f 2>/dev/null | sed -n '1p' > "$OUT/qtwebengineprocess.path"; then
+    if [ -s "$OUT/qtwebengineprocess.path" ]; then
+        qtwebengineprocess_status=present
+    fi
+fi
+
+static_qt_tarb_status=missing
+if [ -f "$STATIC_QT_TARBALL" ]; then
+    static_qt_tarb_status=present
+    sha256_file "$STATIC_QT_TARBALL" > "$OUT/static-qt-tarball-sha256.txt"
+    tar tzf "$STATIC_QT_TARBALL" > "$OUT/static-qt-tarball-files.txt" 2>"$OUT/static-qt-tarball.stderr" || true
+fi
+
+adblock_manifest_status=missing
+if [ -f "$ADBLOCK_MANIFEST" ]; then
+    adblock_manifest_status=present
+fi
+
+yubikey_manifest_status=missing
+if [ -f "$YUBIKEY_MANIFEST" ]; then
+    yubikey_manifest_status=present
+fi
+
+fuse_shim_status=missing
+if [ -f "$FUSE_SHIM" ]; then
+    fuse_shim_status=present
+fi
+
+{
+    echo "repo=jerboa-browser"
+    echo "sbom_status=present"
+    printf 'generated_at_utc='
+    date -u '+%Y-%m-%dT%H:%M:%SZ'
+    if [ -x "$JERBUILD" ] || command -v "$JERBUILD" >/dev/null 2>&1; then
+        printf 'jerbuild_version='
+        "$JERBUILD" --version 2>/dev/null || true
+        printf 'jerboa_home='
+        "$JERBUILD" --jerboa-home 2>/dev/null || true
+    else
+        echo "jerbuild_status=missing"
+    fi
+    echo "qt_status=$qt_status"
+    echo "browser_version_status=$browser_version_status"
+    echo "qtwebengineprocess_status=$qtwebengineprocess_status"
+    echo "static_qt_tarb_status=$static_qt_tarb_status"
+    echo "adblock_manifest_status=$adblock_manifest_status"
+    echo "yubikey_manifest_status=$yubikey_manifest_status"
+    echo "fuse_shim_status=$fuse_shim_status"
+    echo "sandbox_claim=source-policy-local-helper-and-optional-process-evidence-only"
+    echo "target_process_sandbox_status=see-webengine-sandbox-evidence"
+} > "$OUT/manifest.txt"
+
+write_source_files | LC_ALL=C sort -u > "$OUT/source-files.txt"
+while IFS= read -r path; do
+    sha256_file "$path"
+done < "$OUT/source-files.txt" > "$OUT/source-sha256.txt"
+
+write_release_inputs | LC_ALL=C sort -u > "$OUT/release-inputs.txt"
+while IFS= read -r path; do
+    sha256_file "$path"
+done < "$OUT/release-inputs.txt" > "$OUT/release-inputs-sha256.txt"
+
+if [ -f "$BIN" ]; then
+    sha256_file "$BIN" > "$OUT/binary-sha256.txt"
+    file "$BIN" > "$OUT/binary-file.txt" 2>&1 || true
+fi
+
+for lib in qt-webengine/build/libjerboa_browser.* lib/libjerboa_native.* lib/libyubikey_native.* "$FUSE_SHIM"; do
+    for file in $lib; do
+        if [ -f "$file" ]; then
+            sha256_file "$file"
+        fi
+    done
+done > "$OUT/native-runtime-sha256.txt"
+
+if [ "$adblock_manifest_status" = "present" ]; then
+    "$CARGO" metadata --manifest-path "$ADBLOCK_MANIFEST" --locked --format-version 1 \
+        > "$OUT/cargo-metadata-adblock-ffi.json" 2>"$OUT/cargo-metadata-adblock-ffi.stderr" || true
+fi
+
+if [ "$yubikey_manifest_status" = "present" ]; then
+    "$CARGO" metadata --manifest-path "$YUBIKEY_MANIFEST" --locked --format-version 1 \
+        > "$OUT/cargo-metadata-yubikey-native.json" 2>"$OUT/cargo-metadata-yubikey-native.stderr" || true
+fi
diff --git a/scripts/security-check.sh b/scripts/security-check.sh
index ccaedec..14264a6 100755
--- a/scripts/security-check.sh
+++ b/scripts/security-check.sh
@@ -12,6 +12,12 @@ require_file() {
     [ -f "$1" ] || note_fail "missing required release file: $1"
 }
 
+require_text() {
+    file=$1
+    pattern=$2
+    grep -q "$pattern" "$file" 2>/dev/null || note_fail "missing release marker '$pattern' in $file"
+}
+
 require_file LICENSE
 require_file SECURITY.md
 require_file README.md
@@ -22,6 +28,21 @@ require_file docs/credential-handling.md
 require_file docs/ffi-boundary.md
 require_file docs/webengine-sandbox.md
 require_file docs/release-evidence.md
+require_file scripts/security-check.sh
+require_file scripts/sbom.sh
+require_file scripts/reproducibility-report.sh
+require_file scripts/binary-smoke.sh
+require_file scripts/webengine-sandbox-evidence.sh
+
+require_text docs/webengine-sandbox.md 'JBROWSER_RUN_WEBENGINE_PROCESS_SMOKE=1'
+require_text docs/webengine-sandbox.md 'JBROWSER_REQUIRE_TARGET_SANDBOX_PROOF=1'
+require_text docs/webengine-sandbox.md 'qtwebengineprocess-argv.txt'
+require_text docs/webengine-sandbox.md 'target_os_sandbox_status=target-evidence-recorded'
+require_text docs/release-evidence.md 'process_smoke_status'
+require_text docs/release-evidence.md 'target_sandbox_proof_status'
+require_text docs/release-evidence.md 'status=blocked-target-proof'
+require_text SECURITY.md 'JBROWSER_TARGET_SANDBOX_PROOF_FILE'
+require_text scripts/webengine-sandbox-evidence.sh 'status=blocked-target-proof'
 
 scan_paths="scheme build-binary.ss packaging support"
 
@@ -63,7 +84,9 @@ sandbox_matches=$(
         -path ./qt-webengine/build -prune -o \
         -path ./dist -prune -o \
         -path ./docs -prune -o \
-        -type f ! -name '*.md' ! -path './scripts/security-check.sh' -print |
+        -type f ! -name '*.md' \
+        ! -path './scripts/security-check.sh' \
+        ! -path './scripts/webengine-sandbox-evidence.sh' -print |
     xargs grep -n -I -E 'QTWEBENGINE_DISABLE_SANDBOX[[:space:]]*=|--no-sandbox' 2>/dev/null || true
 )
 if [ -n "$sandbox_matches" ]; then
diff --git a/scripts/webengine-sandbox-evidence.sh b/scripts/webengine-sandbox-evidence.sh
new file mode 100755
index 0000000..69d31e4
--- /dev/null
+++ b/scripts/webengine-sandbox-evidence.sh
@@ -0,0 +1,326 @@
+#!/bin/sh
+set -eu
+
+ROOT=$(cd "$(dirname "$0")/.." && pwd)
+cd "$ROOT"
+
+OUT=${JBROWSER_SANDBOX_DIR:-${SANDBOX_DIR:-dist/webengine-sandbox}}
+BIN=${BIN:-jerboa-browser}
+PLATFORM=$(uname -s)
+RUN_PROCESS_SMOKE=${JBROWSER_RUN_WEBENGINE_PROCESS_SMOKE:-0}
+PROCESS_SMOKE_MS=${JBROWSER_WEBENGINE_PROCESS_SMOKE_MS:-6000}
+PROCESS_SMOKE_URL=${JBROWSER_WEBENGINE_PROCESS_SMOKE_URL:-about:blank}
+PROCESS_SMOKE_SAMPLES=${JBROWSER_WEBENGINE_PROCESS_SMOKE_SAMPLES:-10}
+REQUIRE_PROCESS_SMOKE=${JBROWSER_REQUIRE_WEBENGINE_PROCESS_SMOKE:-0}
+TARGET_SANDBOX_PROOF_FILE=${JBROWSER_TARGET_SANDBOX_PROOF_FILE:-}
+REQUIRE_TARGET_SANDBOX_PROOF=${JBROWSER_REQUIRE_TARGET_SANDBOX_PROOF:-0}
+
+rm -rf "$OUT"
+mkdir -p "$OUT"
+
+source_status=absent
+disable_env='QTWEBENGINE_DISABLE_SANDBOX'
+disable_arg='--no-sandbox'
+matches=$(
+    find . \
+        -path ./.git -prune -o \
+        -path ./.jerboa -prune -o \
+        -path ./vendor -prune -o \
+        -path ./qt-webengine/vendor -prune -o \
+        -path ./qt-webengine/build -prune -o \
+        -path ./dist -prune -o \
+        -path ./docs -prune -o \
+        -type f ! -name '*.md' \
+        ! -path './scripts/security-check.sh' \
+        ! -path './scripts/webengine-sandbox-evidence.sh' -print |
+    xargs grep -n -I -E "$disable_env[[:space:]]*=|$disable_arg" 2>/dev/null || true
+)
+if [ -n "$matches" ]; then
+    source_status=present
+    printf '%s\n' "$matches" > "$OUT/sandbox-disable-matches.txt"
+else
+    : > "$OUT/sandbox-disable-matches.txt"
+fi
+
+qt_version_status=blocked-unavailable
+if command -v pkg-config >/dev/null 2>&1; then
+    if pkg-config --modversion Qt6Core Qt6WebEngineCore Qt6WebEngineWidgets Qt6Widgets > "$OUT/qt-pkg-config.txt" 2>"$OUT/qt-pkg-config.stderr"; then
+        qt_version_status=local-version-recorded
+    fi
+fi
+
+browser_version_status=missing-binary