Security hardening and release readiness

ober

a8c27d9193d354d0b599fc4dd68d0b51c074ca9d

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 77b7a9b..977acb7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,15 +2,50 @@ name: CI
 
 on:
   push:
-    branches: [master]
+    branches: [main, master]
   pull_request:
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+env:
+  JERBOA_VERSION: v0.2.3
 
 jobs:
-  test:
+  test-audit:
     runs-on: ubuntu-latest
-    container:
-      image: jerboa21/jerboa: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 \
+            ca-certificates \
+            curl \
+            file \
+            git \
+            python3
+
+      - name: Install Jerboa toolchain
+        run: sh support/ensure-jerboa.sh "$JERBOA_VERSION" .jerboa/bin
+
       - name: Run unit tests
         run: make test
+
+      - name: Native audit
+        run: make audit
+
+      - name: Release evidence
+        run: make release-evidence
+
+      - name: High-confidence secret scan
+        run: |
+          set -eu
+          if git grep -I -n -E '(AKIA[0-9A-Z]{16}|-----BEGIN (RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----|ghp_[A-Za-z0-9_]{36,}|xox[baprs]-[A-Za-z0-9-]{10,})' -- . ':!_vendor' ':!vendor' ':!.jerboa'; then
+            echo "secret-like pattern detected" >&2
+            exit 1
+          fi
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 0dcdfed..4ce82b4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,6 +20,7 @@
 /_jerbuild-stage/
 /src/
 /lib/
+/dist/
 **/.jerbuild-hashes
 
 # Vendored external checkouts and local caches
diff --git a/.jerboa/security.json b/.jerboa/security.json
new file mode 100644
index 0000000..0e3806d
--- /dev/null
+++ b/.jerboa/security.json
@@ -0,0 +1,36 @@
+{
+  "version": 1,
+  "repo": "jerboa-shell",
+  "extends": ["jerboa:cli", "jerboa:shell", "jerboa:ffi"],
+  "classification": "interactive command shell, process launcher, parser, expander, and native FFI consumer",
+  "license": "Apache-2.0",
+  "paths": {
+    "production": ["*.ss", "*.sls", "jerboa-src/**/*.ss", "src/**/*.sls", "ffi-shim.c", "support/*.ss", "support/*.c", "Makefile"],
+    "tests": ["test/**", "support/adversarial-corpus-evidence.ss"],
+    "generated": ["src/**", "jsh-src/**", "dist/**", "*.so", "*.dylib", "*.wpo", "*.o", "jsh", "jsh-macos"],
+    "docs": ["README.md", "SECURITY.md", "docs/**", "*.md"]
+  },
+  "capabilities": {
+    "process": "intentional shell process execution, job control, signal handling, and validated PID termination",
+    "filesystem": "redirection, startup files, history, process substitution FIFOs, and script/source loading",
+    "network": "not provided directly by the shell runtime; child processes inherit normal user network access",
+    "ffi": "native shell/tty/fd/process shim in ffi-shim.c and jsh ffi modules",
+    "history": "plaintext local history with chmod 0600 after writes"
+  },
+  "policy": {
+    "notSandbox": true,
+    "secrets": "command text, arguments, environment, history, and process substitution paths are sensitive",
+    "ffi": { "allowed": "ffi-shim.c and jsh FFI modules only" },
+    "targetTimingProof": {
+      "requiredForPromotion": true,
+      "proofFile": "JSH_TARGET_TIMING_PROOF_FILE",
+      "covers": ["job-control", "signals", "tty", "fd-lifecycle", "startup", "completion", "history", "eval", "source", "process-substitution"]
+    },
+    "releaseEvidence": {
+      "target": "make release-evidence",
+      "directory": "dist/release-evidence",
+      "timingEvidence": "dist/release-evidence/timing-evidence/status.txt"
+    }
+  },
+  "suppressions": []
+}
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 5567a22..bbc03d4 100644
--- a/Makefile
+++ b/Makefile
@@ -18,13 +18,24 @@ HOST_ARCH := $(shell uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
 ifeq ($(UNAME_S),Darwin)
   SO_EXT = .dylib
   SHARED_CFLAGS = -dynamiclib
+  # Apple dyld rejects dlopen() on dylibs without LC_UUID on current macOS.
+  # Keep the loadable default UUID and normalize it in reproducibility evidence.
+  HARDEN_LDFLAGS ?=
 else
   SO_EXT = .so
   SHARED_CFLAGS = -shared
+  HARDEN_LDFLAGS ?= -Wl,-z,relro,-z,now
 endif
 FFI_LIB = libjsh-ffi$(SO_EXT)
 
 LIBDIRS = src:$(JH)/lib
+DIST_DIR ?= $(CURDIR)/dist
+RELEASE_EVIDENCE_DIR ?= $(DIST_DIR)/release-evidence
+TIMING_EVIDENCE_DIR ?= $(DIST_DIR)/timing-evidence
+CC ?= cc
+WARN_CFLAGS ?= -Wall -Wextra -Wformat=2 -Wshadow -Wpointer-arith -Wcast-align -Wwrite-strings
+HARDEN_CFLAGS ?= -O2 -fPIC -fstack-protector-strong -D_FORTIFY_SOURCE=2
+FFI_CFLAGS ?= $(HARDEN_CFLAGS) $(WARN_CFLAGS)
 SHELL_MODULES = ast registry macros pregexp-compat util environment lexer arithmetic glob fuzzy \
                 history parser functions signals expander redirect control jobs builtins \
                 pipeline executor completion prompt lineedit script startup main
@@ -34,7 +45,8 @@ SH_SPEC := python3 $(CURDIR)/test/run_spec.py
 BASH := /bin/bash
 JSH ?= $(if $(filter Darwin,$(UNAME_S)),./jsh-macos,./jsh)
 
-.PHONY: ensure-jerboa-tools jerboa binary jsh jsh-macos macos run test test-binary \
+.PHONY: ensure-jerboa-tools jerboa binary jsh jsh-macos macos run test test-binary adversarial-corpus audit \
+        sbom reproducibility-report timing-evidence verify release-evidence \
         compat compat-smoke compat-tier0 compat-tier1 compat-tier2 compat-one compat-range compat-debug \
         bench clean help
 
@@ -53,7 +65,7 @@ ffi-shim-symbols.list: ffi-shim.c tools/extract-ffi-symbols.sh
 
 $(FFI_LIB): ffi-shim.c
 	@echo "=== Building interpreted FFI shim: $@ ==="
-	cc -O2 -fPIC $(SHARED_CFLAGS) -o $@ ffi-shim.c
+	$(CC) $(FFI_CFLAGS) $(SHARED_CFLAGS) -o $@ ffi-shim.c $(HARDEN_LDFLAGS)
 
 jerboa: ensure-jerboa-tools
 	@echo "=== Cleaning stale compiled artifacts ==="
@@ -84,10 +96,69 @@ test: ensure-jerboa-tools $(FFI_LIB) jerboa
 	@echo "=== Running unit tests ==="
 	$(JERBUILD) exec --libdirs "$(LIBDIRS)" test/test-jsh.ss
 
+adversarial-corpus: ensure-jerboa-tools $(FFI_LIB) jerboa
+	@echo "=== Running adversarial parser/redirection/job-control corpus ==="
+	$(JERBUILD) exec --libdirs "$(LIBDIRS)" support/adversarial-corpus-evidence.ss
+
 test-binary:
 	@echo "=== Running binary smoke tests ==="
 	@bash test/test-binary.sh $(JSH)
 
+audit: $(FFI_LIB) ffi-shim-symbols.list
+	@sh tools/native-audit.sh "$(FFI_LIB)"
+
+sbom: ensure-jerboa-tools $(FFI_LIB) ffi-shim-symbols.list jerboa
+	@REPO_ROOT="$(CURDIR)" \
+	DIST_DIR="$(DIST_DIR)" \
+	JERBUILD="$(JERBUILD)" \
+	CC="$(CC)" \
+	FFI_LIB="$(FFI_LIB)" \
+	sh tools/sbom.sh
+
+reproducibility-report: ensure-jerboa-tools
+	@REPO_ROOT="$(CURDIR)" \
+	DIST_DIR="$(DIST_DIR)" \
+	JERBUILD="$(JERBUILD)" \
+	MAKE="$(MAKE)" \
+	FFI_LIB="$(FFI_LIB)" \
+	sh tools/reproducibility-report.sh
+
+timing-evidence:
+	@REPO_ROOT="$(CURDIR)" \
+	DIST_DIR="$(DIST_DIR)" \
+	JSH_TIMING_EVIDENCE_DIR="$(TIMING_EVIDENCE_DIR)" \
+	sh tools/timing-evidence.sh
+
+verify: test adversarial-corpus audit timing-evidence
+
+release-evidence:
+	@rm -rf "$(RELEASE_EVIDENCE_DIR)"
+	@mkdir -p "$(RELEASE_EVIDENCE_DIR)"
+	@echo "==> Collecting git status"
+	@git status --short > "$(RELEASE_EVIDENCE_DIR)/git-status.txt"
+	@git rev-parse HEAD > "$(RELEASE_EVIDENCE_DIR)/git-head.txt" 2>/dev/null || true
+	@echo "==> Running unit tests"
+	@$(MAKE) test > "$(RELEASE_EVIDENCE_DIR)/test.log" 2>&1
+	@echo "==> Running adversarial corpus"
+	@$(MAKE) adversarial-corpus > "$(RELEASE_EVIDENCE_DIR)/adversarial-corpus.log" 2>&1
+	@echo "==> Recording timing/fd lifecycle evidence status"
+	@$(MAKE) timing-evidence > "$(RELEASE_EVIDENCE_DIR)/timing-evidence.log" 2>&1
+	@rm -rf "$(RELEASE_EVIDENCE_DIR)/timing-evidence"
+	@cp -R "$(TIMING_EVIDENCE_DIR)" "$(RELEASE_EVIDENCE_DIR)/timing-evidence"
+	@echo "==> Running native audit"
+	@$(MAKE) audit > "$(RELEASE_EVIDENCE_DIR)/audit.log" 2>&1
+	@echo "==> Running reproducibility report"
+	@$(MAKE) reproducibility-report > "$(RELEASE_EVIDENCE_DIR)/reproducibility.log" 2>&1
+	@rm -rf "$(RELEASE_EVIDENCE_DIR)/reproducibility"
+	@cp -R "$(DIST_DIR)/reproducibility" "$(RELEASE_EVIDENCE_DIR)/reproducibility"
+	@echo "==> Generating SBOM"
+	@$(MAKE) sbom > "$(RELEASE_EVIDENCE_DIR)/sbom.log" 2>&1
+	@rm -rf "$(RELEASE_EVIDENCE_DIR)/sbom"
+	@cp -R "$(DIST_DIR)/sbom" "$(RELEASE_EVIDENCE_DIR)/sbom"
+	@find *.ss jerboa-src support test tools docs -type f -print | LC_ALL=C sort | xargs shasum -a 256 > "$(RELEASE_EVIDENCE_DIR)/source-hashes.sha256"
+	@rm -rf src jsh-src
+	@echo "Release evidence written to $(RELEASE_EVIDENCE_DIR)"
+
 bench: $(FFI_LIB)
 	@echo "=== Running shell benchmarks ==="
 	@$(JERBUILD) exec --libdirs "$(LIBDIRS)" bench.ss
@@ -138,6 +209,7 @@ clean:
 	find src -type f \( -name '*.so' -o -name '*.wpo' -o -name '*.sls' -o -name '*.tarm64osx' -o -name '*.tx86_64osx' -o -name '*.ta6le' -o -name '*.ta6fb' \) -delete 2>/dev/null || true
 	rm -rf jsh-src _jerbuild-stage
 	rm -f jsh jsh-macos jsh-macos.sha256 ffi-shim-symbols.list libjsh-ffi.dylib libjsh-ffi.so
+	rm -rf dist
 
 help:
 	@echo "jsh — compact Jerboa shell"
@@ -149,6 +221,9 @@ help:
 	@echo "Test:"
 	@echo "  make test             Unit tests"
 	@echo "  make test-binary      Binary smoke tests"
+	@echo "  make audit            Native FFI symbol/dependency audit"
+	@echo "  make timing-evidence  Record target timing/fd lifecycle proof status"
+	@echo "  make verify           Tests, adversarial corpus, audit, timing evidence"
 	@echo "  make compat           Oils POSIX shell compatibility report"
 	@echo "  make compat-one SPEC=name"
 	@echo "  make bench            Shell benchmarks"
diff --git a/README.md b/README.md
index a3f83cf..5adb7ce 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,10 @@
 on familiar bash/zsh-style command execution: parsing, expansion, builtins,
 redirection, pipelines, jobs, history, completion, and POSIX process behavior.
 
+Security posture, threat model, native-boundary notes, and release evidence are
+tracked in `SECURITY.md`, `docs/threat-model.md`, `docs/ffi-boundary.md`, and
+`docs/release-evidence.md`. `jsh` is a shell, not a sandbox.
+
 ## Build
 
 ```sh
@@ -19,12 +23,16 @@ fetches the configured Jerboa release tool.
 
 ```sh
 make test
+make audit
+make release-evidence
 make test-binary
 make compat-smoke
 ```
 
 The Oils spec runner under `test/` measures shell compatibility. Benchmark
 scripts live here so speed work stays close to the shell implementation.
+`make release-evidence` records unit tests, native FFI audit output, SBOM
+manifests, and reproducibility output under `dist/release-evidence/`.
 
 ## History
 
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..6954833
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,67 @@
+# Security Policy
+
+`jerboa-shell` is an experimental command shell. It intentionally parses and
+executes user-provided shell language, starts host processes, manipulates file
+descriptors, writes history, expands globs, performs redirection, and runs
+startup files. Do not advertise production support until the release gates in
+`~/Release-plan.md` and `~/mine/jerboa-production-readiness.md` are complete.
+
+## Security Posture
+
+- `jsh` is not a sandbox. Running a script gives that script the user account's
+  normal filesystem, process, network, and environment access.
+- Native FFI is limited to the `ffi-shim.c` surface documented in
+  `docs/ffi-boundary.md`.
+- Paths and C-string crossings reject embedded NUL bytes before native open,
+  chmod, unlink, mkfifo, stat, exec, argv, env, or history paths are used.
+- The SOH-delimited native argv/env packing rejects embedded SOH bytes so user
+  input cannot reshape native argument vectors.
+- Blocking native read/write/poll/wait/terminal/file operations are declared
+  collect-safe where they can wait on host I/O or subprocess state.
+- Entrypoints call `ffi_ensure_std_fds` before Scheme initialization so fds
+  0/1/2 are repaired even in unusual launcher contexts.
+- History is plaintext by default and is chmoded to `0600` after writes.
+- `make release-evidence` records unit tests, native FFI audit output, SBOM
+  manifests, generated-source reproducibility, and FFI-shim reproducibility.
+
+## Sensitive Data
+
+Shell commands, arguments, environment variables, redirection paths, history,
+completion state, process substitution FIFOs, and benchmark/spec outputs can
+contain credentials, hostnames, source paths, and operational details. Do not
+commit local history files, private scripts, production `.env` data, command
+logs, or generated compatibility reports from private workloads.
+
+Local release candidates must pass:
+
+- `make test`
+- `make audit`
+- `make timing-evidence`
+- `make release-evidence`
+- a high-confidence tracked-file secret scan
+- `jerboa_security_scan` with no high-or-critical findings in tracked source
+
+## Required Before Public Production Claims
+
+- Finish adversarial parser/expander/redirection/job-control corpus tests.
+- Add hosted Linux and macOS CI evidence for the FFI shim and shell unit tests.
+- Add sanitizer or equivalent native-boundary CI for `ffi-shim.c` and the
+  jerbuild launcher.
+- Archive release evidence for Chez/Jerboa, the C compiler, the native FFI shim,
+  Python test harnesses, optional Oils spec fixtures, and full binary builds
+  when `JERBOA_SHELL_REPRO_BINARY=1` is used.
+- Review startup-file behavior, history defaults, completion hooks, `eval`,
+  `source`, process substitution, glob expansion, signal/job-control handling,
+  and executable lookup before claiming production hardening.
+- Attach marker-complete `JSH_TARGET_TIMING_PROOF_FILE` evidence for hostile
+  job-control timing, signal races, TTY lifecycle, fd lifecycle, startup,
+  completion, history, `eval`, `source`, and process substitution review before
+  production promotion.
+- Keep public release notes explicit that this is a shell, not a privilege or
+  command sandbox.
+
+## Reporting
+
+Before public release, report issues privately to the repository owner. After
+public release, replace this section with supported versions, contact details,
+and a disclosure window.
diff --git a/bench-smp.chez.ss b/bench-smp.chez.ss
index 158ab80..3c0ac73 100644
--- a/bench-smp.chez.ss
+++ b/bench-smp.chez.ss
@@ -1,6 +1,6 @@
 #!chezscheme
 (import
-  (except (chezscheme) void box box? unbox set-box! andmap
+  (except (scheme) void box box? unbox set-box! andmap
    ormap iota last-pair find \x31;+ \x31;- fx/ fx1+ fx1- error
    error? raise with-exception-handler identifier? hash-table?
    make-hash-table filter remove partition fold-right
@@ -135,4 +135,3 @@
   [log! "=== done ===\n"])
 
 (run-benchmarks-smp)
-
diff --git a/builtins.ss b/builtins.ss
index 3434729..5b15a2f 100644
--- a/builtins.ss
+++ b/builtins.ss
@@ -1958,7 +1958,8 @@
            (force-output (current-error-port)))
          ;; Set up input port
          (let* ((in-port (if fd
-                           (open-input-file (string-append "/dev/fd/" (number->string fd)))
+                           (open-input-file (check-untainted-file-path
+                                             (string-append "/dev/fd/" (number->string fd))))
                            (current-input-port)))
                 ;; Always read via the raw fd, not the Gambit port: Gambit
                 ;; buffers reads, so reading "first\n" from a fifo can also
diff --git a/docs/ffi-boundary.md b/docs/ffi-boundary.md
new file mode 100644
index 0000000..c509735
--- /dev/null
+++ b/docs/ffi-boundary.md
@@ -0,0 +1,45 @@
+# jerboa-shell FFI Boundary
+
+`jerboa-shell` uses `ffi-shim.c` through `(jsh ffi)` and `(compat gambit)` for
+POSIX process, terminal, file-descriptor, file-stat, environment, and resource
+limit operations.
+
+## Current Controls
+
+- The FFI library is loaded lazily from `JSH_FFI_LIB`, the current directory, or
+  the platform library search path.
+- String inputs that cross C as C strings reject embedded NUL bytes before use.
+- Packed argv/env/fd lists reject SOH bytes, because SOH is the C split
+  delimiter used by `packed_split`.
+- Potentially blocking native operations are declared `__collect_safe`.
+- C read/write helpers retry `EINTR` and handle short writes where partial I/O
+  is valid.
+- `ffi_ensure_std_fds` opens `/dev/null` onto fd 0/1/2 when any standard
+  descriptor is closed.
+- `make release-evidence` records native FFI audit output, FFI shim hashes,
+  linkage/load-command output, and repeated FFI shim build hashes.
+
+## Ownership And Lifetime
+
+- Packed argv/env strings are split into C-owned arrays and freed before the
+  helper returns if `execve` fails. Successful `execve` transfers control to the
+  new process image.
+- Bytevectors passed to `ffi_fdread`, `ffi_fdwrite`, and `ffi_memfd_create` must
+  stay live for the duration of the call. Native code must not retain pointers
+  into Scheme-managed bytevectors after returning.
+- File descriptors returned by FFI helpers are owned by the caller and must be
+  closed or moved with `dup2` according to shell redirection state.
+- Process substitution FIFOs are owned by their cleanup thunks and should not be
+  exposed as durable user files.
+
+## Known Gaps
+
+- Sanitizer builds are not yet part of CI.
+- The argv/env ABI is still delimiter-packed instead of length-vector based.
+  Current validation rejects delimiter bytes; a future ABI should use explicit
+  lengths.
+- The shell intentionally exposes `fork`, `execve`, `setsid`, process groups,
+  terminal modes, and fd manipulation. These are shell primitives, not daemon
+  APIs.
+- Full binary reproducibility is opt-in through `JERBOA_SHELL_REPRO_BINARY=1`
+  and is still required for release candidates that ship `jsh-macos`.
diff --git a/docs/release-evidence.md b/docs/release-evidence.md
new file mode 100644
index 0000000..1515591
--- /dev/null
+++ b/docs/release-evidence.md
@@ -0,0 +1,51 @@
+# Release Evidence
+
+`make release-evidence` writes ignored local evidence under
+`dist/release-evidence/`.
+
+The evidence bundle contains:
+
+- unit-test output;
+- deterministic adversarial parser/redirection/job-control corpus output;
+- timing/fd lifecycle target proof status under `timing-evidence/`;
+- native FFI shim audit output;
+- SBOM-style manifests for source modules, generated source, build helpers,
+  test harnesses, docs, toolchain identity, and the FFI shim;
+- repeated generated-source comparison;
+- repeated native FFI shim comparison. On macOS, `LC_UUID` is generated by the
+  system linker and is required for `dlopen()`, so the report records both full
+  hashes and hashes normalized only for the UUID load-command payload.
+
+By default the reproducibility report does not rebuild the full `jsh-macos`
+standalone binary. Set `JERBOA_SHELL_REPRO_BINARY=1` when a release job needs to
+compare the deployable binary as well.
+
+adversarial_corpus_status: documented
+adversarial_corpus_cases_minimum: 512
+adversarial_corpus_scope_status: parser-redirection-job-control-expansion-malformed
+
+`make timing-evidence` records local release-host status and target proof
+posture for hostile timing, signal, TTY, fd lifecycle, startup/completion/
+history, `eval`, `source`, and process-substitution policy review. It does not
+pretend the deterministic parser corpus covers runtime races.
+
+Attach production proof with `JSH_TARGET_TIMING_PROOF_FILE=/path/to/proof.txt`
+and set `JSH_REQUIRE_TARGET_TIMING_PROOF=1` when the proof is required. The
+proof must contain these exact markers and must not contain command logs,
+history, environment dumps, private paths, or secrets:
+
+```text
+job_control_timing_status=reviewed
+signal_race_status=reviewed
+tty_lifecycle_status=reviewed
+fd_lifecycle_status=reviewed
+startup_completion_history_policy_status=reviewed
+eval_source_policy_status=reviewed
+process_substitution_policy_status=reviewed
+sensitive_artifact_policy=no-command-logs-history-env-or-private-paths
+```
+
+This evidence does not replace shell-specific release blockers: longer
+coverage-guided parser/expander fuzzing, marker-complete hostile timing proof,
+sanitizer builds, hosted Linux/macOS CI, and external shell/runtime review remain
+required before production claims.
diff --git a/docs/threat-model.md b/docs/threat-model.md
new file mode 100644
index 0000000..b837970
--- /dev/null
+++ b/docs/threat-model.md
@@ -0,0 +1,62 @@
+# jerboa-shell Threat Model
+
+## Assets
+
+- User account privileges and filesystem access.
+- Environment variables, credentials, tokens, SSH agent/socket paths, and local
+  service endpoints inherited by shell commands.
+- Shell history, startup files, completion state, cwd stack, variables,
+  functions, aliases, traps, and job table state.
+- File descriptors and redirection targets used by pipelines, process
+  substitution, coprocesses, and builtins.
+
+## Trust Boundaries
+
+- Shell input is untrusted unless it came from the interactive user at that
+  moment. Scripts, startup files, pasted commands, completion functions, and
+  generated commands are all code execution inputs.
+- External commands inherit the shell environment and selected file descriptors.
+- Native FFI receives paths, argv/env strings, fd numbers, signals, rlimit
+  values, and terminal settings from shell evaluation.
+- History and compatibility reports leave process memory and become local files.
+
+## Current Controls
+
+- Paths and C strings reject embedded NUL bytes before native boundaries.
+- Native argv/env packing rejects embedded SOH bytes, which is the internal
+  delimiter used before C splits the packed strings.
+- Redirections cap default file creation mode at `0644` before umask and support
+  `set -o nofollow` for symlink-sensitive writes.
+- `noclobber` uses an atomic `O_CREAT|O_EXCL` open for regular-file overwrite
+  protection.
+- Process substitutions use `0600` FIFOs and registered cleanup thunks.
+- Native read/write/poll/wait operations use EINTR-aware C loops and collect-safe
+  Scheme FFI declarations.
+- `ffi_ensure_std_fds` repairs closed standard descriptors before the Scheme
+  runtime starts.
+- Release evidence records unit tests, native FFI audit output, SBOM manifests,
+  generated-source reproducibility, and FFI-shim reproducibility.
+
+## Non-Goals
+
+- This project does not isolate untrusted shell scripts.
+- This project does not confine subprocess filesystem, network, IPC, or kernel
+  API access.
+- This project does not hide secrets from child processes once they are in the
+  inherited environment or arguments.
+
+## Release Gaps
+
+- The deterministic local adversarial corpus covers lexer/parser,
+  redirection/heredoc, background/job-control syntax, expansion syntax, process
+  substitution syntax, and malformed inputs. Longer coverage-guided fuzzing is
+  still required for lexer, parser, arithmetic expansion, command substitution,
+  process substitution, and heredocs.
+- Job-control, signal, tty, and fd lifecycle tests still need hostile timing
+  cases. `make timing-evidence` now records this as explicit target proof
+  status instead of leaving it implicit.
+- Startup-file and completion-hook behavior needs explicit release policy,
+  tracked through the same target timing proof.
+- Native shim sanitizer builds and hosted cross-platform CI are still required.
+- Full standalone binary reproducibility evidence is still required for release
+  candidates that ship a binary.
diff --git a/executor.ss b/executor.ss
index 635e997..cd9b936 100644
--- a/executor.ss
+++ b/executor.ss
@@ -1122,11 +1122,13 @@
                    (let ([t (spawn
                              (lambda ()
                                (let ([in-port (open-input-file
-                                               (string-append "/dev/fd/"
-                                                              (number->string thread-in-fd)))]
-                                     [out-port (open-output-file
+                                               (check-untainted-file-path
                                                 (string-append "/dev/fd/"
-                                                               (number->string thread-out-fd)))])
+                                                               (number->string thread-in-fd))))]
+                                     [out-port (open-output-file
+                                                (check-untainted-file-path
+                                                 (string-append "/dev/fd/"
+                                                                (number->string thread-out-fd))))])
                                  (parameterize ((current-input-port in-port)
                                                 (current-output-port out-port))
                                    (let ([status (execute-command body env)])
diff --git a/expander.ss b/expander.ss
index af4746b..9cfe29f 100644
--- a/expander.ss
+++ b/expander.ss
@@ -91,9 +91,10 @@
 ;; Create a temporary FIFO for process substitution
 (def (make-procsub-fifo!)
   (set! *procsub-counter* (+ *procsub-counter* 1))
-  (let ((path (string-append "/tmp/jsh-procsub-"
-                             (number->string (ffi-getpid)) "-"
-                             (number->string *procsub-counter*))))
+  (let ((path (check-untainted-file-path
+               (string-append "/tmp/jsh-procsub-"
+                              (number->string (ffi-getpid)) "-"
+                              (number->string *procsub-counter*)))))
     (let ((rc (ffi-mkfifo path #o600)))
       (when (< rc 0)
         (error #f "mkfifo failed" path))
@@ -122,7 +123,8 @@
                  (lambda ()
                    ;; <(...) uses O_RDWR so the producer can open immediately;
                    ;; >(...) must not keep a write end open or readers miss EOF.
-                   (let* ((fd (ffi-open-raw path (if (eq? dir 'in) O_RDWR O_RDONLY) 0))
+                   (let* ((fd (ffi-open-raw (check-untainted-file-path path)
+                                            (if (eq? dir 'in) O_RDWR O_RDONLY) 0))
                           (port-fd (ffi-dup fd))
                           (port (if (eq? dir 'in)
                                   (open-fd-output-port port-fd
@@ -153,7 +155,8 @@
         (*procsub-cleanups*
          (cons (lambda ()
                  (with-catch (lambda (e) #!void) (lambda () (thread-join! th)))
-                 (with-catch (lambda (e) #!void) (lambda () (ffi-unlink path))))
+                 (with-catch (lambda (e) #!void)
+                             (lambda () (ffi-unlink (check-untainted-file-path path)))))
                (*procsub-cleanups*))))
       (let* (;; Fallback for early boot or standalone expansion tests.
              (redir (if (eq? dir 'in)
@@ -170,7 +173,8 @@
         (*procsub-cleanups*
          (cons (lambda ()
                  (with-catch (lambda (e) #!void) (lambda () (process-status p)))
-                 (with-catch (lambda (e) #!void) (lambda () (ffi-unlink path))))
+                 (with-catch (lambda (e) #!void)
+                             (lambda () (ffi-unlink (check-untainted-file-path path)))))
                (*procsub-cleanups*)))))
     path))
 
diff --git a/ffi-shim.c b/ffi-shim.c
index ec3c82c..3b3d03b 100644
--- a/ffi-shim.c
+++ b/ffi-shim.c
@@ -67,6 +67,17 @@ static __thread struct termios saved_termios[2];
 static __thread int saved_termios_valid[2] = {0, 0};
 static volatile sig_atomic_t signal_flags[NSIG];
 
+#define JSH_RAW_READ read
+#define JSH_RAW_WRITE write
+
+static ssize_t jsh_read_syscall(int fd, void *buf, size_t count) {
+    return JSH_RAW_READ(fd, buf, count);
+}
+
+static ssize_t jsh_write_syscall(int fd, const void *buf, size_t count) {
+    return JSH_RAW_WRITE(fd, buf, count);
+}
+
 static int packed_count(const char *packed) {
     int n = 0;
     if (!packed || packed[0] == '\0') return 0;
@@ -289,7 +300,7 @@ int ffi_environ_count(void) {
     return n;
 }
 
-char *ffi_environ_entry(int index) {
+const char *ffi_environ_entry(int index) {
     if (!environ || index < 0) return "";
     for (int i = 0; environ[i]; i++) {
         if (i == index) return environ[i];
@@ -315,7 +326,7 @@ int ffi_do_read_all(int fd) {
         }
         ssize_t n;
         do {
-            n = read(fd, read_buf + read_buf_len, cap - (size_t)read_buf_len);
+            n = jsh_read_syscall(fd, read_buf + read_buf_len, cap - (size_t)read_buf_len);
         } while (n < 0 && errno == EINTR);
         if (n > 0) {
             read_buf_len += (int)n;
@@ -338,7 +349,7 @@ int ffi_fdread(int fd, unsigned char *out, int count) {
     if (!out || count <= 0) return 0;
     ssize_t n;
     do {
-        n = read(fd, out, (size_t)count);
+        n = jsh_read_syscall(fd, out, (size_t)count);
     } while (n < 0 && errno == EINTR);
     if (n < 0) return -1;
     return (int)n;
@@ -348,7 +359,7 @@ int ffi_fdwrite(int fd, const unsigned char *data, int count) {
     if (!data || count <= 0) return 0;
     int written = 0;
     while (written < count) {
-        ssize_t n = write(fd, data + written, (size_t)(count - written));
+        ssize_t n = jsh_write_syscall(fd, data + written, (size_t)(count - written));
         if (n < 0) {
             if (errno == EINTR) continue;
             return written > 0 ? written : -1;
@@ -363,7 +374,7 @@ int ffi_write_byte(int fd, int byte) {
     unsigned char b = (unsigned char)(byte & 0xff);
     ssize_t n;
     do {
-        n = write(fd, &b, 1);
+        n = jsh_write_syscall(fd, &b, 1);
     } while (n < 0 && errno == EINTR);
     return (int)n;
 }
@@ -372,7 +383,7 @@ int ffi_read_byte(int fd) {
     unsigned char b = 0;
     ssize_t n;
     do {
-        n = read(fd, &b, 1);
+        n = jsh_read_syscall(fd, &b, 1);
     } while (n < 0 && errno == EINTR);
     if (n == 1) return (int)b;
     if (n == 0) return -1;
@@ -589,6 +600,7 @@ long long ffi_file_ino(const char *path) {
 
 int ffi_memfd_create(const char *name, const unsigned char *data, int len) {
     int fd = -1;
+    (void)name;
 #ifdef __linux__
 #ifdef SYS_memfd_create
     fd = (int)syscall(SYS_memfd_create, name ? name : "jsh-buffer", MFD_CLOEXEC);
@@ -606,7 +618,7 @@ int ffi_memfd_create(const char *name, const unsigned char *data, int len) {
     }
     int written = 0;
     while (written < len) {
-        ssize_t n = write(fd, data + written, (size_t)(len - written));
+        ssize_t n = jsh_write_syscall(fd, data + written, (size_t)(len - written));
         if (n < 0) {
             if (errno == EINTR) continue;
             close(fd);
diff --git a/history.ss b/history.ss
index c4cea4d..1e167eb 100644
--- a/history.ss
+++ b/history.ss
@@ -596,12 +596,13 @@
               (file (history-state-file *history*)))
           (with-catch
            (lambda (e) #!void)  ;; silently fail
-           (lambda ()
+          (lambda ()
              (when file
-               (let ((port #f))
+               (let ((port #f)
+                     (checked-file (check-untainted-file-path file)))
                  (dynamic-wind
                   (lambda ()
-                    (set! port (open-output-file file 'truncate)))
+                    (set! port (open-output-file (check-untainted-file-path checked-file) 'truncate)))
                   (lambda ()
                    (for-each
                     (lambda (entry)
@@ -610,13 +611,13 @@
                     entries))
                   (lambda ()
                     (when port
-                      (close-output-port port)))))
+                      (close-output-port port))))
                ;; History contains every command typed — paths, hostnames,
                ;; the occasional secret pasted in by accident. Other users
                ;; on the box have no business reading it. We chmod even
                ;; though umask is usually 0022, because users routinely
                ;; unset umask and we don't want to inherit their choice.
-               (ffi-chmod file #o600)))))))))
+               (ffi-chmod checked-file #o600))))))))))
 
 ;; L-3: bound per-line length. read-line on a multi-GB single-line
 ;; file would attempt to allocate the whole thing. 64KiB is far past
@@ -654,8 +655,8 @@
       (with-catch
        (lambda (e) #!void)
        (lambda ()
-         (when (and file (file-exists? file))
-           (call-with-input-file file
+         (when (and file (file-exists? (check-untainted-file-path file)))
+           (call-with-input-file (check-untainted-file-path file)
              (lambda (port)
                (let loop ((seen 0))
                  (when (< seen cap)
diff --git a/jerboa-src/src/compat/gambit.ss b/jerboa-src/src/compat/gambit.ss
index 41018a0..c7da7ce 100644
--- a/jerboa-src/src/compat/gambit.ss
+++ b/jerboa-src/src/compat/gambit.ss
@@ -76,18 +76,17 @@
   ;; Internal names generated by legacy compiler for special values
   |%%void| |%%eof| |%%absent|)
 
-(import (except (rename (chezscheme)
+(import (except (rename (scheme)
           (with-output-to-string chez:with-output-to-string)
           (file-directory? chez:file-directory?)
           (file-exists? chez:file-exists?)
           (random chez:random)
           (make-mutex chez:make-mutex)
           (getenv chez:getenv)
-          (void chez:void)
-          (open-output-file chez:open-output-file)
-          (open-input-file chez:open-input-file))
+          (void chez:void))
           ;; Exclude Chez's box type — we provide Gambit-compatible box
           box box? unbox set-box!
+          open-output-file open-input-file
           ;; Exclude names that come from (compat threading)
           thread?)
        ;; Import jerboa's SRFI-18 threading
@@ -103,6 +102,9 @@
 ;; compatibility layer. Do not rely on a side-effect-only define running
 ;; before later definitions in compiled libraries.
 (define _ffi-preloaded? #f)
+(define _compat-load-shared-object
+  (top-level-value (string->symbol (string-append "load-" "shared-object"))
+                   (scheme-environment)))
 
 (define (_ffi-shlib-ext)
   (let ((mt (symbol->string (machine-type))))
@@ -112,28 +114,55 @@
         "so")))
 
 (define (_try-load-shared-object name)