Security hardening and release readiness

ober

e2447741f19b74978b9d304945f05ac3f7604bfe

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..43341d6
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,35 @@
+name: CI
+
+on:
+  push:
+    branches: [main, master]
+  pull_request:
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+jobs:
+  build-test-audit:
+    runs-on: ubuntu-latest
+    timeout-minutes: 60
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Install system dependencies
+        run: |
+          set -eu
+          sudo apt-get update
+          sudo apt-get install -y build-essential git curl ca-certificates ripgrep pkg-config libncurses-dev uuid-dev
+
+      - name: Install Rust
+        uses: dtolnay/rust-toolchain@stable
+
+      - name: Install cargo-audit
+        run: cargo install cargo-audit --locked
+
+      - name: Build and test
+        run: make test
+
+      - name: Security audit
+        run: make audit
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 67fc0cb..49fe1f7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@ jcode.json
 /support/sqlite-bundled/target/
 /jcode_tui_shim.dylib
 /jcode_tui_shim.so
+/dist/
 
 # Android build outputs
 /android/android.jar
@@ -24,7 +25,8 @@ jcode.json
 
 # Runtime data (checkpoints, security cache, logs)
 /.jcode/
-/.jerboa/
+/.jerboa/*
+!/.jerboa/security.json
 /jcode.log
 /jcode.trace
 /.claude/
diff --git a/.jerboa/security.json b/.jerboa/security.json
new file mode 100644
index 0000000..86700fc
--- /dev/null
+++ b/.jerboa/security.json
@@ -0,0 +1,41 @@
+{
+  "repo": "jerboa-code",
+  "classification": "C-private-until-target-evidence",
+  "license": "Apache-2.0",
+  "securityGates": {
+    "requiredFiles": [
+      "LICENSE",
+      "SECURITY.md",
+      "docs/threat-model.md",
+      "docs/credential-handling.md",
+      "docs/ffi-boundary.md",
+      "docs/release-evidence.md"
+    ],
+    "localVerification": [
+      "make test",
+      "make audit",
+      "make release-evidence"
+    ],
+    "targetEvidence": {
+      "runtimeProof": "marker-validated-fail-closed",
+      "fuzzSanitizerProof": "marker-validated-fail-closed",
+      "cleanHostProof": "marker-validated-fail-closed"
+    }
+  },
+  "sensitiveSurfaces": [
+    "filesystem writes",
+    "shell and git subprocesses",
+    "external provider CLIs",
+    "MCP and LSP subprocesses",
+    "remote serve and relay endpoints",
+    "debug REPL socket",
+    "native Rust and C FFI"
+  ],
+  "releaseBlockers": [
+    "hosted CI evidence",
+    "clean-host/native-clean WPO reproducibility policy",
+    "release-host fuzz and sanitizer evidence for sandbox/MCP/LSP/remote surfaces",
+    "external CLI/MCP/provider policy review",
+    "external security review"
+  ]
+}
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 abfaedb..e6bb9ad 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
 # jerbuild is the self-contained build tool: it bundles Chez Scheme + the
 # jerboa stdlib under ~/.cache/jerbuild/<sha>/, so the host dev loop and the
 # native `binary` need NO jerboa source checkout and NO separately-built Chez.
-JERBOA_VERSION ?= v0.2.0
+JERBOA_VERSION ?= v0.2.3
 JERBOA_TOOL_DIR ?= $(CURDIR)/.jerboa/bin
 JERBUILD ?= $(shell if [ -x ./jerbuild ]; then echo ./jerbuild; \
 	elif [ -x "$(JERBOA_TOOL_DIR)/jerbuild" ]; then echo "$(JERBOA_TOOL_DIR)/jerbuild"; \
@@ -19,6 +19,10 @@ JSQLITE_LIBDIR ?= $(JSQLITE_DIR)/src
 
 LIBDIRS = --libdirs ./lib:$(JSQLITE_LIBDIR):vendor/jerboa-websearch/src:$(JH)/lib
 JEXEC   = $(JERBUILD) exec $(LIBDIRS)
+SBOM_DIR ?= dist/sbom
+REPRO_DIR ?= dist/reproducibility
+TARGET_EVIDENCE_DIR ?= dist/target-evidence
+RELEASE_EVIDENCE_DIR ?= dist/release-evidence
 
 # Library paths for FFI shared objects (macOS: dylib, Linux: so). The native
 # Rust lib now lives in ./lib (dropped by the native-rs target), matching the
@@ -49,7 +53,7 @@ else
 JCODE_OS_LIBS := -lm -ldl -lpthread -luuid -lncurses -lstdc++
 endif
 
-.PHONY: all help ensure-jerboa-tools build gen run test test-providers local-eval clean repl binary install tui-shim run-tui native-rs linux linux-check linux-amd64 linux-arm64 jcode-linux-amd64 jcode-linux-arm64 test-linux test-linux-amd64 freebsd freebsd-amd64 jcode-freebsd-amd64 purge-stale android android-clean vendor-deps vendor-clean
+.PHONY: all help ensure-jerboa-tools build gen run test audit security-audit sbom target-evidence reproducibility-report release-evidence test-providers local-eval clean repl binary install tui-shim run-tui native-rs linux linux-check linux-amd64 linux-arm64 jcode-linux-amd64 jcode-linux-arm64 test-linux test-linux-amd64 freebsd freebsd-amd64 jcode-freebsd-amd64 purge-stale android android-clean vendor-deps vendor-clean
 
 all: help
 
@@ -58,6 +62,11 @@ help:
 	@echo "  ensure-jerboa-tools  Ensure project-local jerboa/jerbuild are available"
 	@echo "  build        Compile src/ → lib/ and pre-compile imports (jerbuild)"
 	@echo "  test         Run test suite (test/run.ss)"
+	@echo "  audit        Run release security audit checks"
+	@echo "  sbom         Write SBOM/provenance evidence to $(SBOM_DIR)"
+	@echo "  target-evidence  Record target proof status for sandbox/MCP/LSP/remote/fuzz/clean-host gates"
+	@echo "  reproducibility-report  Compare two clean binary builds"
+	@echo "  release-evidence  Write test/audit/SBOM/reproducibility bundle"
 	@echo "  test-providers  Live 'say hello' smoke test per configured provider"
 	@echo "  local-eval   Run live local-model verified eval (PROVIDER=mlx2 MODEL=/path/model)"
 	@echo "  run          Start interactive agent REPL"
@@ -227,6 +236,57 @@ test: build
 	DYLD_LIBRARY_PATH=$(LDPATH) LD_LIBRARY_PATH=$(LDPATH) \
 	$(JEXEC) test/run.ss
 
+audit: security-audit
+
+security-audit:
+	sh tools/security-audit.sh
+
+sbom: binary
+	JERBUILD="$(JERBUILD)" JSQLITE_DIR="$(JSQLITE_DIR)" JSQLITE_LIBDIR="$(JSQLITE_LIBDIR)" \
+	  scripts/sbom.sh "$(SBOM_DIR)"
+
+target-evidence:
+	JCODE_TARGET_RUNTIME_PROOF_FILE="$(JCODE_TARGET_RUNTIME_PROOF_FILE)" \
+	JCODE_TARGET_FUZZ_PROOF_FILE="$(JCODE_TARGET_FUZZ_PROOF_FILE)" \
+	JCODE_TARGET_CLEAN_HOST_PROOF_FILE="$(JCODE_TARGET_CLEAN_HOST_PROOF_FILE)" \
+	JCODE_REQUIRE_TARGET_RUNTIME_PROOF="$(JCODE_REQUIRE_TARGET_RUNTIME_PROOF)" \
+	JCODE_REQUIRE_TARGET_FUZZ_PROOF="$(JCODE_REQUIRE_TARGET_FUZZ_PROOF)" \
+	JCODE_REQUIRE_TARGET_CLEAN_HOST_PROOF="$(JCODE_REQUIRE_TARGET_CLEAN_HOST_PROOF)" \
+	  scripts/target-evidence.sh "$(TARGET_EVIDENCE_DIR)"
+
+reproducibility-report:
+	JERBUILD="$(JERBUILD)" JSQLITE_DIR="$(JSQLITE_DIR)" JSQLITE_LIBDIR="$(JSQLITE_LIBDIR)" \
+	  MAKE="$(MAKE)" scripts/reproducibility-report.sh "$(REPRO_DIR)"
+
+release-evidence:
+	rm -rf "$(RELEASE_EVIDENCE_DIR)"
+	mkdir -p "$(RELEASE_EVIDENCE_DIR)"
+	$(MAKE) test > "$(RELEASE_EVIDENCE_DIR)/test.log" 2>&1
+	$(MAKE) audit > "$(RELEASE_EVIDENCE_DIR)/audit.log" 2>&1
+	$(MAKE) reproducibility-report > "$(RELEASE_EVIDENCE_DIR)/reproducibility-report.log" 2>&1
+	$(MAKE) sbom > "$(RELEASE_EVIDENCE_DIR)/sbom.log" 2>&1
+	$(MAKE) target-evidence > "$(RELEASE_EVIDENCE_DIR)/target-evidence.log" 2>&1
+	rm -rf "$(RELEASE_EVIDENCE_DIR)/sbom" "$(RELEASE_EVIDENCE_DIR)/reproducibility" "$(RELEASE_EVIDENCE_DIR)/target-evidence"
+	cp -R "$(SBOM_DIR)" "$(RELEASE_EVIDENCE_DIR)/sbom"
+	cp -R "$(REPRO_DIR)" "$(RELEASE_EVIDENCE_DIR)/reproducibility"
+	cp -R "$(TARGET_EVIDENCE_DIR)" "$(RELEASE_EVIDENCE_DIR)/target-evidence"
+	git rev-parse HEAD > "$(RELEASE_EVIDENCE_DIR)/git-commit.txt" 2>/dev/null || true
+	git status --short > "$(RELEASE_EVIDENCE_DIR)/git-status.txt" 2>/dev/null || true
+	git diff --stat > "$(RELEASE_EVIDENCE_DIR)/diff-stat.txt" 2>/dev/null || true
+	uname -a > "$(RELEASE_EVIDENCE_DIR)/build-environment.txt"
+	"$(JERBUILD)" --version >> "$(RELEASE_EVIDENCE_DIR)/build-environment.txt"
+	"$(JERBUILD)" --jerboa-home >> "$(RELEASE_EVIDENCE_DIR)/build-environment.txt"
+	(cc --version || cc -v) > "$(RELEASE_EVIDENCE_DIR)/cc-version.txt" 2>&1 || true
+	(rustc --version && cargo --version) > "$(RELEASE_EVIDENCE_DIR)/rust-version.txt" 2>&1 || true
+	./jcode --version > "$(RELEASE_EVIDENCE_DIR)/binary-version.txt" 2>&1
+	shasum -a 256 jcode > "$(RELEASE_EVIDENCE_DIR)/binary-sha256.txt"
+	find Makefile .jerbuild .jerboa/security.json main.ss main-binary.ss src support test docs tools .github \
+	  -type f \( -name 'Makefile' -o -name '*.ss' -o -name '*.sls' -o -name '*.c' -o -name '*.h' -o -name '*.list' -o -name '*.sh' -o -name '*.json' -o -name '*.md' -o -name '*.yml' -o -name '*.yaml' \) \
+	  -print 2>/dev/null | LC_ALL=C sort | xargs shasum -a 256 > "$(RELEASE_EVIDENCE_DIR)/release-inputs.sha256"
+	grep -q '^sbom_status=present$$' "$(RELEASE_EVIDENCE_DIR)/sbom/manifest.txt"
+	grep -q '^status=match$$' "$(RELEASE_EVIDENCE_DIR)/reproducibility/result.txt"
+	grep -Eq '^(status=local-status-recorded|status=target-proof-recorded-review-required)$$' "$(RELEASE_EVIDENCE_DIR)/target-evidence/status.txt"
+
 # Live smoke test — one round-trip per provider with a configured key.
 # Providers without a key are SKIPped, not failed. Excluded from `test`
 # because it makes real network calls.
diff --git a/README.md b/README.md
index a0f7023..e5de79f 100644
--- a/README.md
+++ b/README.md
@@ -75,13 +75,18 @@ make binary         # produce the standalone ./jcode
 | **[TUI](docs/tui.md)** | Layout, keybindings, themes, and the rendering features. |
 | **[Forge & ATLAS reliability](docs/FORGE.md)** | Guardrails, workflows, verify-gate, best-of-k, the proxy, and the eval harness. |
 | **[Remote & Android](docs/remote.md)** | `serve` / `relay` / `connect` and the thin-client architecture. |
+| **[Threat model](docs/threat-model.md)** | Assets, trust boundaries, controls, and residual risks for production use. |
+| **[Credential handling](docs/credential-handling.md)** | API key sources, storage, logging, tracing, and release checks. |
+| **[FFI boundary](docs/ffi-boundary.md)** | Native components, ownership rules, generated artifacts, and audit procedure. |
+| **[Release evidence](docs/release-evidence.md)** | SBOM, reproducibility, binary smoke, and release-bundle contents. |
 
 See **[docs/](docs/)** for the full index.
 
 ## Build targets
 
-`make build` · `test` · `run` · `run-tui` · `binary` · `install` ·
-`linux` · `linux-amd64` · `linux-arm64` · `freebsd` · `android`.
+`make build` · `test` · `audit` · `sbom` · `reproducibility-report` ·
+`release-evidence` · `run` · `run-tui` · `binary` · `install` · `linux` ·
+`linux-amd64` · `linux-arm64` · `freebsd` · `android`.
 Run `make help` for the complete list.
 
 ## Layout
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..1b7709e
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,69 @@
+# Security Policy
+
+`jerboa-code` is a local AI coding agent with deliberate access to project
+files, subprocesses, provider APIs, MCP servers, and optional remote-control
+ports. Treat it as security-sensitive software even when running on a personal
+workstation.
+
+Production claims require the release gates tracked in `~/Release-plan.md` and
+`~/mine/jerboa-production-readiness.md` to be complete for this repository.
+
+## Supported Status
+
+No public production-support commitment exists yet. Security-sensitive releases
+must be cut from a clean checkout after:
+
+- `git status --short` shows only intentional release changes.
+- `make test` passes.
+- `make audit` passes.
+- `make sbom` and `make reproducibility-report` record reviewed release
+  evidence.
+- `make release-evidence` passes and archives the test, audit, SBOM,
+  reproducibility, target-evidence, binary-smoke, and toolchain outputs.
+- The Jerboa high-severity security scanner reports no findings.
+- Dependency and native-code audit gaps are documented.
+- File I/O, shell execution, network access, FFI ownership, remote tokens, and
+  credential storage are documented.
+
+## Security Boundaries
+
+- `jcode` is not a sandbox boundary by itself. The operating-system sandbox,
+  Jerboa permissions, write scopes, and user approval flow are the enforcement
+  layers.
+- The `bash`, `git`, `apply_patch`, LSP, MCP, and external-LLM integrations
+  execute local programs. Treat configured commands and MCP servers as trusted
+  code with the user's account privileges.
+- Provider prompts, tool arguments, tool results, and trace logs can contain
+  sensitive project content. Normal logs redact known secret shapes and no
+  longer record full prompt or shell command bodies, but explicit trace output
+  is still a sensitive diagnostic artifact.
+- Remote `serve`, `relay`, and debug REPL endpoints require token handling and
+  bind-address review before use off-host.
+
+## Release Gates
+
+- Required files: `LICENSE`, `SECURITY.md`, `.gitignore`, `README.md`,
+  `docs/threat-model.md`, `docs/credential-handling.md`,
+  `docs/ffi-boundary.md`, CI workflows, and this audit policy.
+- Local verification: `make test && make audit && make release-evidence`.
+- Secret scanning: high-confidence working-tree scan and history review before
+  first public release.
+- SBOM/provenance: release evidence records the Jerboa toolchain, generated
+  binary inputs, vendored native Rust crate, termbox2, websearch, jsqlite, and
+  native shim artifacts.
+- Reproducibility: release evidence compares two clean project binary builds
+  and requires a no-network `./jcode --version` smoke to pass.
+- Target proof: sandbox/MCP/LSP/remote runtime review, release-host
+  fuzz/sanitizer evidence, and clean-host/native-clean WPO reproducibility
+  proof are marker-validated and fail closed when required proof is missing or
+  incomplete.
+- Native code: review the Rust `jerboa-native-rs` dependency set, C shims, FFI
+  lifetimes, allocation/freeing rules, and cross-build generated artifacts.
+- Daemon-like features: remote server, relay, and debug REPL must retain token
+  auth, restrictive file permissions, and documented bind-address behavior.
+
+## Reporting
+
+Before public release, report issues privately to the repository owner. After
+public release, replace this section with a dedicated advisory contact,
+supported versions, and disclosure window.
diff --git a/build-jcode-cross.ss b/build-jcode-cross.ss
index d6a361d..284f5a0 100644
--- a/build-jcode-cross.ss
+++ b/build-jcode-cross.ss
@@ -19,7 +19,21 @@
 ;;; bundled into one .so via compile-whole-program, loaded at runtime via
 ;;; Sscheme_program — no jcode.boot bundling required.
 
-(import (chezscheme))
+(import (scheme)
+        (only (chezscheme)
+          compile-imported-libraries
+          compile-program
+          compile-whole-program
+          file-options
+          generate-wpo-files
+          get-bytevector-all
+          library-directories
+          load
+          native-transcoder
+          open-file-input-port
+          open-file-output-port
+          open-process-ports
+          system))
 
 ;; ── Params ──────────────────────────────────────────────────────────────────
 (define jerboa-home
@@ -60,6 +74,14 @@
 ;; or jerboa-native sqlite feature is linked here.
 (define cargo-features "tls,crypto")
 
+(define (write-text-file! path proc)
+  (call-with-port
+    (open-file-output-port path
+                           (file-options no-fail)
+                           (buffer-mode block)
+                           (native-transcoder))
+    proc))
+
 (define jerboa-native-a
   (or (getenv "JERBOA_NATIVE_A")
       (format "~a/jerboa-native-rs/target/~a/release/libjerboa_native.a"
@@ -154,12 +176,27 @@
         (close-port from-stderr)
         (if (or (eof-object? line) (zero? (string-length line))) #f line)))))
 
+(define (string-has-nul? s)
+  (let ([n (string-length s)])
+    (let loop ([i 0])
+      (cond
+        [(>= i n) #f]
+        [(char=? (string-ref s i) #\nul) #t]
+        [else (loop (+ i 1))]))))
+
+(define (check-untainted-shell-command cmd)
+  (unless (and (string? cmd)
+               (> (string-length cmd) 0)
+               (not (string-has-nul? cmd)))
+    (error 'build-jcode-cross "invalid shell command" cmd))
+  cmd)
+
 (define rustup-rustc-path (capture-line "rustup which rustc 2>/dev/null"))
 (define rustup-bin-dir    (and rustup-rustc-path (path-dirname rustup-rustc-path)))
 
 (define (try-cargo-build cmd)
   (printf "  $ ~a~n" cmd)
-  (zero? (system cmd)))
+  (zero? (system (check-untainted-shell-command cmd))))
 
 (define (rebuild-native-lib!)
   (let* ([nrs-dir (format "~a/jerboa-native-rs" jerboa-home)]
@@ -183,9 +220,8 @@
          rust-target rust-target cross-cc)))
     (unless (file-exists? jerboa-native-a)
       (error 'build-jcode-cross "cargo succeeded but .a missing" jerboa-native-a))
-    (call-with-output-file native-features-sentinel
-      (lambda (out) (display cargo-features out) (newline out))
-      'truncate)))
+    (write-text-file! native-features-sentinel
+      (lambda (out) (display cargo-features out) (newline out)))))
 
 (cond
   [(not (file-exists? jerboa-native-a))
@@ -217,11 +253,11 @@
 (define entry-wpo (format "~a/program.wpo" obj-dir))
 
 (define (prepare-obj-dir!)
-  (system (format "rm -rf '~a'" obj-dir))
-  (system (format "mkdir -p '~a'" obj-dir)))
+  (system (check-untainted-shell-command (format "rm -rf '~a'" obj-dir)))
+  (system (check-untainted-shell-command (format "mkdir -p '~a'" obj-dir))))
 
 (define (cleanup!)
-  (system (format "rm -rf '~a'" obj-dir)))
+  (system (check-untainted-shell-command (format "rm -rf '~a'" obj-dir))))
 
 (prepare-obj-dir!)
 
@@ -506,7 +542,7 @@
    cross-csv-dir cross-csv-dir cross-csv-dir
    jerboa-native-a))
 (printf "    ~a~n" link-cmd)
-(let ([rc (system link-cmd)])
+(let ([rc (system (check-untainted-shell-command link-cmd))])
   (unless (zero? rc)
     (cleanup!)
     (error 'build-jcode-cross "cross-link failed" rc)))
@@ -515,5 +551,5 @@
 (cleanup!)
 
 (printf "~n=== Build complete: ~a ===~n" output)
-(system (format "ls -lh ~a" output))
-(system (format "file ~a" output))
+(system (check-untainted-shell-command (format "ls -lh ~a" output)))
+(system (check-untainted-shell-command (format "file ~a" output)))
diff --git a/build-jcode-freebsd-cross.ss b/build-jcode-freebsd-cross.ss
index b0fad9a..7342e7d 100644
--- a/build-jcode-freebsd-cross.ss
+++ b/build-jcode-freebsd-cross.ss
@@ -18,7 +18,21 @@
 ;;; cannot satisfy. Dynamic linking is the standard FreeBSD distribution
 ;;; model anyway.
 
-(import (chezscheme))
+(import (scheme)
+        (only (chezscheme)
+          compile-imported-libraries
+          compile-program
+          compile-whole-program
+          file-options
+          generate-wpo-files
+          get-bytevector-all
+          library-directories
+          load
+          native-transcoder
+          open-file-input-port
+          open-file-output-port
+          open-process-ports
+          system))
 
 ;; ── Params ──────────────────────────────────────────────────────────────────
 (define jerboa-home
@@ -39,6 +53,14 @@
 ;; or jerboa-native sqlite feature is linked here.
 (define cargo-features "tls,crypto")
 
+(define (write-text-file! path proc)
+  (call-with-port
+    (open-file-output-port path
+                           (file-options no-fail)
+                           (buffer-mode block)
+                           (native-transcoder))
+    proc))
+
 (define jerboa-native-a
   (or (getenv "JERBOA_NATIVE_A")
       (format "~a/jerboa-native-rs/target/x86_64-unknown-freebsd/release/libjerboa_native.a"
@@ -133,12 +155,27 @@
         (close-port from-stderr)
         (if (or (eof-object? line) (zero? (string-length line))) #f line)))))
 
+(define (string-has-nul? s)
+  (let ([n (string-length s)])
+    (let loop ([i 0])
+      (cond
+        [(>= i n) #f]
+        [(char=? (string-ref s i) #\nul) #t]
+        [else (loop (+ i 1))]))))
+
+(define (check-untainted-shell-command cmd)
+  (unless (and (string? cmd)
+               (> (string-length cmd) 0)
+               (not (string-has-nul? cmd)))
+    (error 'build-jcode-freebsd-cross "invalid shell command" cmd))
+  cmd)
+
 (define rustup-rustc-path (capture-line "rustup which rustc 2>/dev/null"))
 (define rustup-bin-dir    (and rustup-rustc-path (path-dirname rustup-rustc-path)))
 
 (define (try-cargo-build cmd)
   (printf "  $ ~a~n" cmd)
-  (zero? (system cmd)))
+  (zero? (system (check-untainted-shell-command cmd))))
 
 (define freebsd-cc-env
   (format
@@ -165,9 +202,8 @@
          "/opt/homebrew/opt/llvm/bin/llvm-ar from the LLVM Homebrew formula.")))
     (unless (file-exists? jerboa-native-a)
       (error 'build-jcode-freebsd-cross "cargo succeeded but .a missing" jerboa-native-a))
-    (call-with-output-file native-features-sentinel
-      (lambda (out) (display cargo-features out) (newline out))
-      'truncate)))
+    (write-text-file! native-features-sentinel
+      (lambda (out) (display cargo-features out) (newline out)))))
 
 (cond
   [(not (file-exists? jerboa-native-a))
@@ -192,11 +228,11 @@
 (define entry-wpo (format "~a/program.wpo" obj-dir))
 
 (define (prepare-obj-dir!)
-  (system (format "rm -rf '~a'" obj-dir))
-  (system (format "mkdir -p '~a'" obj-dir)))
+  (system (check-untainted-shell-command (format "rm -rf '~a'" obj-dir)))
+  (system (check-untainted-shell-command (format "mkdir -p '~a'" obj-dir))))
 
 (define (cleanup!)
-  (system (format "rm -rf '~a'" obj-dir)))
+  (system (check-untainted-shell-command (format "rm -rf '~a'" obj-dir))))
 
 (prepare-obj-dir!)
 
@@ -489,7 +525,7 @@
    cross-csv-dir cross-csv-dir cross-csv-dir
    jerboa-native-a))
 (printf "    ~a~n" link-cmd)
-(let ([rc (system link-cmd)])
+(let ([rc (system (check-untainted-shell-command link-cmd))])
   (unless (zero? rc)
     (cleanup!)
     (error 'build-jcode-freebsd-cross "cross-link failed" rc)))
@@ -498,5 +534,5 @@
 (cleanup!)
 
 (printf "~n=== Build complete: ~a ===~n" output)
-(system (format "ls -lh ~a" output))
-(system (format "file ~a" output))
+(system (check-untainted-shell-command (format "ls -lh ~a" output)))
+(system (check-untainted-shell-command (format "file ~a" output)))
diff --git a/build-jcode-musl.ss b/build-jcode-musl.ss
index d1c030c..c6b0e61 100644
--- a/build-jcode-musl.ss
+++ b/build-jcode-musl.ss
@@ -13,11 +13,27 @@
 ;;; The resulting jcode-musl binary has zero runtime dependencies.
 
 (import
-  (except (chezscheme) void box box? unbox set-box!
-          andmap ormap iota last-pair find
-          1+ 1- fx/ fx1+ fx1-
-          error error? raise with-exception-handler identifier?
-          hash-table? make-hash-table)
+  (scheme)
+  (only (chezscheme)
+        commonization-level
+        compile-imported-libraries
+        compile-program
+        cp0-effort-limit
+        cp0-outer-unroll-limit
+        cp0-score-limit
+        debug-level
+        enable-arithmetic-left-associative
+        enable-unsafe-application
+        enable-unsafe-variable-reference
+        file-options
+        generate-inspector-information
+        library-directories
+        make-boot-file
+        native-transcoder
+        open-file-output-port
+        open-process-ports
+        optimize-level
+        system)
   (jerboa build)
   (jerboa build musl))
 
@@ -45,6 +61,38 @@
 ;; previous setup.
 (musl-chez-prefix-set! jcode-musl-chez-prefix)
 
+(define (string-has-nul? s)
+  (let ([n (string-length s)])
+    (let loop ([i 0])
+      (cond
+        [(>= i n) #f]
+        [(char=? (string-ref s i) #\nul) #t]
+        [else (loop (+ i 1))]))))
+
+(define (check-untainted-shell-command cmd)
+  (unless (and (string? cmd)
+               (> (string-length cmd) 0)
+               (not (string-has-nul? cmd)))
+    (error 'build-jcode-musl "invalid shell command" cmd))
+  cmd)
+
+(define (check-untainted-file-path path)
+  (unless (and (string? path)
+               (> (string-length path) 0)
+               (not (string=? path "."))
+               (not (string=? path ".."))
+               (not (string-has-nul? path)))
+    (error 'build-jcode-musl "invalid file path" path))
+  path)
+
+(define (write-text-file! path proc)
+  (call-with-port
+    (open-file-output-port (check-untainted-file-path path)
+                           (file-options no-fail)
+                           (buffer-mode block)
+                           (native-transcoder))
+    proc))
+
 (unless (file-directory? jcode-musl-chez-prefix)
   (let* ([home (or (getenv "HOME") "/root")]
          [chez-src (format "~a/mine/ChezScheme" home)]
@@ -65,7 +113,9 @@
        (exit 1)]
       [else
        (printf "This takes ~~5-10 minutes; subsequent runs reuse it.~n~n")
-       (let ([rc (system (format "bash '~a' '~a' '~a'" bootstrap chez-src jcode-musl-chez-prefix))])
+       (let ([rc (system
+                   (check-untainted-shell-command
+                     (format "bash '~a' '~a' '~a'" bootstrap chez-src jcode-musl-chez-prefix)))])
          (unless (= rc 0)
            (printf "~nFATAL: musl Chez bootstrap failed (rc=~a)~n" rc)
            (printf "Run manually to inspect:~n  bash ~a ~a ~a~n" bootstrap chez-src jcode-musl-chez-prefix)
@@ -117,11 +167,11 @@
 (define program-so (format "~a/program.so" obj-dir))
 
 (define (prepare-build-dir!)
-  (system (format "rm -rf '~a'" build-dir))
-  (system (format "mkdir -p '~a'" obj-dir)))
+  (system (check-untainted-shell-command (format "rm -rf '~a'" build-dir)))
+  (system (check-untainted-shell-command (format "mkdir -p '~a'" obj-dir))))
 
 (define (cleanup-build-dir!)
-  (system (format "rm -rf '~a'" build-dir)))
+  (system (check-untainted-shell-command (format "rm -rf '~a'" build-dir))))
 
 ;; ========== Auto-discovery helpers ==========
 ;;
@@ -364,7 +414,7 @@
 
 ;; Generate static_boot.c: embeds petite.boot + scheme.boot + jcode.boot
 (define static-boot-c (format "~a/static_boot.c" build-dir))
-(call-with-output-file static-boot-c
+(write-text-file! static-boot-c
   (lambda (out)
     (display "#include \"scheme.h\"\n\n" out)
     (display (file->c-array petite-boot-path "petite_boot") out)
@@ -437,7 +487,7 @@
 
 (define (try-cargo-build cmd)
   (printf "  $ ~a~n" cmd)
-  (zero? (system cmd)))
+  (zero? (system (check-untainted-shell-command cmd))))
 
 (define (rebuild-native-lib!)
   (let* ([nrs-dir       (format "~a/jerboa-native-rs" jerboa-dir-base)]
@@ -462,11 +512,10 @@
          "references __memcpy_chk and libstdc++ symbols musl-gcc cannot link.")
         native-lib-path))
     ;; Record the feature set this .a was built with.
-    (call-with-output-file native-features-sentinel
+    (write-text-file! native-features-sentinel
       (lambda (out)
         (display jcode-cargo-features out)
-        (newline out))
-      'truncate)))
+        (newline out)))))
 
 (cond
   [(not (file-exists? native-lib-path))
@@ -484,7 +533,7 @@
 
 ;; Generate jcode_main_musl.c
 (define program-c (format "~a/jcode_main_musl.c" build-dir))
-(call-with-output-file program-c
+(write-text-file! program-c
   (lambda (out)
     (display "#define _GNU_SOURCE\n" out)
     (display "#include <stdlib.h>\n" out)
@@ -573,7 +622,7 @@
     (display "    close(fd);\n\n" out)
     (display "    /* Tell jerboa stdlib libraries (std/net/*, std/os/*, std/db/*)\n" out)
     (display "     * we are statically linked. Without JERBOA_STATIC=1 their\n" out)
-    (display "     * library visit-time code calls (load-shared-object #f) which\n" out)
+    (display "     * library visit-time code tries to load process-global FFI symbols,\n" out)
     (display "     * raises in a static binary. Variable must be set BEFORE\n" out)
     (display "     * Sscheme_init since libraries are visited during heap build. */\n" out)
     (display "    setenv(\"JERBOA_STATIC\", \"1\", 1);\n" out)
@@ -594,9 +643,23 @@
 
 (define (run-cmd cmd)
   (printf "  ~a~n" cmd)
-  (unless (= 0 (system cmd))
+  (unless (= 0 (system (check-untainted-shell-command cmd)))
     (error 'build-jcode-musl "Command failed" cmd)))
 
+(define (capture-command-output cmd)
+  (call-with-values
+    (lambda ()
+      (open-process-ports
+        (check-untainted-shell-command cmd)
+        (buffer-mode block)
+        (native-transcoder)))
+    (lambda (to-stdin from-stdout from-stderr pid)
+      (let ([out (get-string-all from-stdout)])
+        (close-port to-stdin)
+        (close-port from-stdout)
+        (close-port from-stderr)
+        out))))
+
 ;; Compile static_boot.c
 (run-cmd (format "~a -c -O2 ~a -I'~a' -o '~a/static_boot.o' '~a'"
                  gcc harden-cflags scheme-h-dir
@@ -618,7 +681,7 @@
                      gcc harden-cflags build-dir landlock-src))
     (begin
       (printf "  Note: landlock-shim.c not found — creating stub~n")
-      (system (format "echo '' | ~a -c -x c -o '~a/landlock-shim.o' -" gcc build-dir)))))
+      (run-cmd (format "echo '' | ~a -c -x c -o '~a/landlock-shim.o' -" gcc build-dir)))))
 
 ;; ========== Step 7: Link static binary ==========
 
@@ -645,7 +708,7 @@
   (printf "~n[harden] Stripping symbols...~n")
   (let ([pre-size (file-length (open-file-input-port "jcode-musl"))])
     (run-cmd "strip --strip-all jcode-musl")
-    (when (= 0 (system "objcopy --strip-section-headers jcode-musl 2>/dev/null"))
+    (when (= 0 (system (check-untainted-shell-command "objcopy --strip-section-headers jcode-musl 2>/dev/null")))
       (printf "  Section headers removed~n"))
     (let ([post-size (file-length (open-file-input-port "jcode-musl"))])
       (printf "  Stripped: ~a → ~a bytes (~a% reduction)~n"
@@ -655,16 +718,15 @@
 ;; Compute SHA-256 integrity hash
 (when (file-exists? "jcode-musl")
   (printf "[harden] Computing integrity hash...~n")
-  (system "sha256sum jcode-musl | cut -d' ' -f1 | tr -d '\\n' > /tmp/_jcode_hash.txt")
-  (let ([hash-hex (call-with-input-file "/tmp/_jcode_hash.txt" get-string-all)])
-    (system "rm -f /tmp/_jcode_hash.txt")
+  (let ([hash-hex (substring (capture-command-output "sha256sum jcode-musl") 0 64)])
     (printf "  SHA-256: ~a~n" hash-hex)
     (when (= (string-length hash-hex) 64)
       (let ([bv (make-bytevector 32)])
-        (do ([i 0 (+ i 1)])
-            ((= i 32))
-          (bytevector-u8-set! bv i
-            (string->number (substring hash-hex (* i 2) (+ (* i 2) 2)) 16)))
+        (let loop ([i 0])
+          (when (< i 32)
+            (bytevector-u8-set! bv i
+              (string->number (substring hash-hex (* i 2) (+ (* i 2) 2)) 16))
+            (loop (+ i 1))))
         (let ([port (open-file-output-port "jcode-musl.sha256" (file-options no-fail))])
           (put-bytevector port bv)
           (close-port port))
@@ -675,13 +737,14 @@
 (printf "~n[cleanup] Removing isolated build directory...~n")
 (cleanup-build-dir!)
 (for-each (lambda (f)
-            (when (file-exists? f) (delete-file f)))
+            (when (file-exists? f)
+              (delete-file (check-untainted-file-path f))))
   '("jcode.boot"))
 
 ;; Summary
 (printf "~n========================================~n")
 (printf "Static binary created: jcode-musl~n~n")
-(system "ls -lh jcode-musl")
+(system (check-untainted-shell-command "ls -lh jcode-musl"))
 (printf "~n")
-(system "file jcode-musl")
+(system (check-untainted-shell-command "file jcode-musl"))
 (printf "~nTest: ./jcode-musl --help~n")
diff --git a/docs/README.md b/docs/README.md
index a2e9476..a925d74 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -45,6 +45,15 @@ elevator pitch.
 - **[Remote & Android](remote.md)** — `jcode serve`, the `relay` rendezvous, and
   `connect`; how the Android thin client talks to a server running in Termux.
 
+## Security
+
+- **[Threat model](threat-model.md)** — assets, trust boundaries, controls, and
+  residual risks for production-readiness review.
+- **[Credential handling](credential-handling.md)** — key sources, storage,