Add shell security gate and sanitize evidence

ober

f12243301eb58cd32650f3f4c58671f430d34f98

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 977acb7..7337f9a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -33,6 +33,9 @@ jobs:
       - name: Install Jerboa toolchain
         run: sh support/ensure-jerboa.sh "$JERBOA_VERSION" .jerboa/bin
 
+      - name: Security gate
+        run: make security
+
       - name: Run unit tests
         run: make test
 
diff --git a/.github/workflows/security-baseline.yml b/.github/workflows/security-baseline.yml
index 28a713e..6e7e53b 100644
--- a/.github/workflows/security-baseline.yml
+++ b/.github/workflows/security-baseline.yml
@@ -23,6 +23,9 @@ jobs:
           test -f .gitignore
           find . -maxdepth 1 -iname "README*" -type f | grep -q .
 
+      - name: Project security gate
+        run: make security
+
       - name: High-confidence secret scan
         run: |
           set -eu
diff --git a/.jerboa/security.json b/.jerboa/security.json
index 0e3806d..bf06afb 100644
--- a/.jerboa/security.json
+++ b/.jerboa/security.json
@@ -32,5 +32,15 @@
       "timingEvidence": "dist/release-evidence/timing-evidence/status.txt"
     }
   },
+  "required_gates": {
+    "local": [
+      "make security",
+      "make verify",
+      "make release-evidence",
+      "make sbom",
+      "make reproducibility-report",
+      "make timing-evidence"
+    ]
+  },
   "suppressions": []
 }
diff --git a/Makefile b/Makefile
index bbc03d4..9c5f199 100644
--- a/Makefile
+++ b/Makefile
@@ -45,7 +45,7 @@ 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 adversarial-corpus audit \
+.PHONY: ensure-jerboa-tools jerboa binary jsh jsh-macos macos run security 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
@@ -92,6 +92,9 @@ jsh: binary
 run: ensure-jerboa-tools $(FFI_LIB) jerboa
 	$(JERBUILD) exec --libdirs "$(LIBDIRS)" jsh.ss
 
+security:
+	@REPO_ROOT="$(CURDIR)" sh tools/security-check.sh
+
 test: ensure-jerboa-tools $(FFI_LIB) jerboa
 	@echo "=== Running unit tests ==="
 	$(JERBUILD) exec --libdirs "$(LIBDIRS)" test/test-jsh.ss
@@ -129,7 +132,7 @@ timing-evidence:
 	JSH_TIMING_EVIDENCE_DIR="$(TIMING_EVIDENCE_DIR)" \
 	sh tools/timing-evidence.sh
 
-verify: test adversarial-corpus audit timing-evidence
+verify: security test adversarial-corpus audit timing-evidence sbom reproducibility-report
 
 release-evidence:
 	@rm -rf "$(RELEASE_EVIDENCE_DIR)"
@@ -137,6 +140,8 @@ release-evidence:
 	@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 security gate"
+	@$(MAKE) security > "$(RELEASE_EVIDENCE_DIR)/security.log" 2>&1
 	@echo "==> Running unit tests"
 	@$(MAKE) test > "$(RELEASE_EVIDENCE_DIR)/test.log" 2>&1
 	@echo "==> Running adversarial corpus"
@@ -145,17 +150,20 @@ release-evidence:
 	@$(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"
+	@grep -q '^status=' "$(RELEASE_EVIDENCE_DIR)/timing-evidence/status.txt"
 	@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"
+	@grep -q '^status=match$$' "$(RELEASE_EVIDENCE_DIR)/reproducibility/report.txt"
 	@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"
+	@sh tools/sanitize-evidence.sh "$(RELEASE_EVIDENCE_DIR)" "$(DIST_DIR)/sbom" "$(DIST_DIR)/reproducibility" "$(TIMING_EVIDENCE_DIR)"
 	@rm -rf src jsh-src
 	@echo "Release evidence written to $(RELEASE_EVIDENCE_DIR)"
 
@@ -219,6 +227,7 @@ help:
 	@echo "  make jsh              Build and copy the native binary to ./jsh"
 	@echo ""
 	@echo "Test:"
+	@echo "  make security         Release security metadata and secret scan"
 	@echo "  make test             Unit tests"
 	@echo "  make test-binary      Binary smoke tests"
 	@echo "  make audit            Native FFI symbol/dependency audit"
diff --git a/SECURITY.md b/SECURITY.md
index 6954833..05092bc 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -3,8 +3,8 @@
 `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.
+startup files. Do not advertise production support until the release plan and
+production-readiness tracker are complete.
 
 ## Security Posture
 
@@ -21,8 +21,11 @@ startup files. Do not advertise production support until the release gates in
 - 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 security` validates release metadata, required security markers,
+  generated-artifact exclusions, and high-confidence secret scans.
 - `make release-evidence` records unit tests, native FFI audit output, SBOM
-  manifests, generated-source reproducibility, and FFI-shim reproducibility.
+  manifests, generated-source reproducibility, FFI-shim reproducibility, and
+  sanitized host-neutral release evidence.
 
 ## Sensitive Data
 
@@ -34,6 +37,8 @@ logs, or generated compatibility reports from private workloads.
 
 Local release candidates must pass:
 
+- `make security`
+- `make verify`
 - `make test`
 - `make audit`
 - `make timing-evidence`
diff --git a/docs/ffi-boundary.md b/docs/ffi-boundary.md
index c509735..ea6c72f 100644
--- a/docs/ffi-boundary.md
+++ b/docs/ffi-boundary.md
@@ -17,7 +17,7 @@ limit operations.
 - `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.
+  sanitized linkage/load-command output, and repeated FFI shim build hashes.
 
 ## Ownership And Lifetime
 
diff --git a/docs/release-evidence.md b/docs/release-evidence.md
index 1515591..63f1dbc 100644
--- a/docs/release-evidence.md
+++ b/docs/release-evidence.md
@@ -5,6 +5,7 @@
 
 The evidence bundle contains:
 
+- security gate output;
 - unit-test output;
 - deterministic adversarial parser/redirection/job-control corpus output;
 - timing/fd lifecycle target proof status under `timing-evidence/`;
@@ -16,9 +17,13 @@ The evidence bundle contains:
   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.
+`make release-evidence` fails unless the copied reproducibility report records
+`status=match`. 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.
+
+Evidence text is sanitized before archival so private checkout paths, SSH clone
+URLs, and host-identifying names are not preserved in release bundles.
 
 adversarial_corpus_status: documented
 adversarial_corpus_cases_minimum: 512
diff --git a/docs/threat-model.md b/docs/threat-model.md
index b837970..e0cf9dd 100644
--- a/docs/threat-model.md
+++ b/docs/threat-model.md
@@ -34,8 +34,9 @@
   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.
+- Release evidence records the security gate, unit tests, native FFI audit
+  output, SBOM manifests, generated-source reproducibility, FFI-shim
+  reproducibility, and sanitized host-neutral evidence.
 
 ## Non-Goals
 
diff --git a/tools/sanitize-evidence.sh b/tools/sanitize-evidence.sh
new file mode 100755
index 0000000..86e5d74
--- /dev/null
+++ b/tools/sanitize-evidence.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+set -eu
+
+[ "$#" -gt 0 ] || exit 0
+
+for path in "$@"; do
+  [ -e "$path" ] || continue
+  find "$path" -type f -print | while IFS= read -r file; do
+    if grep -Iq . "$file"; then
+      tmp=$file.sanitize.$$
+      sed -E \
+        -e 's#/Users/[^[:space:]"]+#<redacted-path>#g' \
+        -e 's#~/mine(/[^[:space:]"]*)?#<redacted-path>#g' \
+        -e 's#git@[^[:space:]"]+#<redacted-ssh-remote>#g' \
+        -e 's#users-MacBook-Pro#<redacted-host>#g' \
+        -e 's#uname=Darwin [^[:space:]]+ #uname=Darwin #g' \
+        "$file" > "$tmp"
+      mv "$tmp" "$file"
+    fi
+  done
+done
diff --git a/tools/sbom.sh b/tools/sbom.sh
index c0f7730..4c39dde 100644
--- a/tools/sbom.sh
+++ b/tools/sbom.sh
@@ -7,49 +7,53 @@ out_dir=${SBOM_DIR:-"$dist_dir/sbom"}
 ffi_lib=${FFI_LIB:-libjsh-ffi.dylib}
 mkdir -p "$out_dir"
 
+hash_file_with_label() {
+  file=$1
+  label=$2
+  if command -v sha256sum >/dev/null 2>&1; then
+    hash=$(sha256sum "$file" | awk '{print $1}')
+  else
+    hash=$(shasum -a 256 "$file" | awk '{print $1}')
+  fi
+  printf '%s  %s\n' "$hash" "$label"
+}
+
 hash_manifest() {
   dir=$1
   out=$2
   if [ -d "$dir" ]; then
-    find "$dir" -type f \
+    rel_dir=${dir#"$repo_root"/}
+    (cd "$repo_root" && find "$rel_dir" -type f \
       ! -path '*/.git/*' \
       ! -path '*/dist/*' \
       ! -path '*/.jerboa/*' \
       ! -path '*/_vendor/*' \
       -print | LC_ALL=C sort | while IFS= read -r file; do
-        if command -v sha256sum >/dev/null 2>&1; then
-          sha256sum "$file"
-        else
-          shasum -a 256 "$file"
-        fi
-      done > "$out"
+        hash_file_with_label "$file" "$file"
+      done) > "$out"
   else
     : > "$out"
   fi
 }
 
 {
-  echo "repo=$repo_root"
+  echo "repo=jerboa-shell"
   echo "version=$(tr -d '[:space:]' < "$repo_root/VERSION" 2>/dev/null || true)"
   echo "generated_at_utc=$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
-  echo "uname=$(uname -a)"
+  echo "uname=$(uname -srm)"
   echo "cc=$(${CC:-cc} --version 2>/dev/null | sed -n '1p' || true)"
   echo "python=$(python3 --version 2>/dev/null || true)"
-  echo "jerbuild=${JERBUILD:-jerbuild}"
+  echo "jerbuild=$([ -n "${JERBUILD:-}" ] && basename "${JERBUILD:-jerbuild}" || printf '%s' jerbuild)"
   "${JERBUILD:-jerbuild}" --version 2>/dev/null || true
 } > "$out_dir/build-environment.txt"
 
 if [ -f "$repo_root/$ffi_lib" ]; then
-  if command -v sha256sum >/dev/null 2>&1; then
-    sha256sum "$repo_root/$ffi_lib" > "$out_dir/ffi-shim.sha256"
-  else
-    shasum -a 256 "$repo_root/$ffi_lib" > "$out_dir/ffi-shim.sha256"
-  fi
+  hash_file_with_label "$repo_root/$ffi_lib" "$ffi_lib" > "$out_dir/ffi-shim.sha256"
   if command -v otool >/dev/null 2>&1; then
-    otool -L "$repo_root/$ffi_lib" > "$out_dir/ffi-shim-linkage.txt"
-    otool -l "$repo_root/$ffi_lib" > "$out_dir/ffi-shim-load-commands.txt"
+    (cd "$repo_root" && otool -L "$ffi_lib") > "$out_dir/ffi-shim-linkage.txt"
+    (cd "$repo_root" && otool -l "$ffi_lib") > "$out_dir/ffi-shim-load-commands.txt"
   elif command -v ldd >/dev/null 2>&1; then
-    ldd "$repo_root/$ffi_lib" > "$out_dir/ffi-shim-linkage.txt"
+    (cd "$repo_root" && ldd "$ffi_lib") > "$out_dir/ffi-shim-linkage.txt"
   else
     echo "No native dependency inspector available." > "$out_dir/ffi-shim-linkage.txt"
   fi
@@ -63,14 +67,10 @@ hash_manifest "$repo_root/support" "$out_dir/support.sha256"
 hash_manifest "$repo_root/test" "$out_dir/test-harness.sha256"
 hash_manifest "$repo_root/docs" "$out_dir/docs.sha256"
 
-find "$repo_root" -maxdepth 1 -type f \
+(cd "$repo_root" && find . -maxdepth 1 -type f \
   \( -name '*.ss' -o -name 'Makefile' -o -name 'README.md' -o -name 'SECURITY.md' -o -name 'VERSION' \) \
-  -print | LC_ALL=C sort | while IFS= read -r file; do
-    if command -v sha256sum >/dev/null 2>&1; then
-      sha256sum "$file"
-    else
-      shasum -a 256 "$file"
-    fi
-  done > "$out_dir/source-modules.sha256"
+  -print | sed 's#^\./##' | LC_ALL=C sort | while IFS= read -r file; do
+    hash_file_with_label "$file" "$file"
+  done) > "$out_dir/source-modules.sha256"
 
 echo "status=generated"
diff --git a/tools/security-check.sh b/tools/security-check.sh
new file mode 100755
index 0000000..f9b2e50
--- /dev/null
+++ b/tools/security-check.sh
@@ -0,0 +1,69 @@
+#!/bin/sh
+set -eu
+
+repo_root=${REPO_ROOT:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}
+cd "$repo_root"
+
+fail() {
+  echo "ERROR: $*" >&2
+  exit 1
+}
+
+require_file() {
+  [ -f "$1" ] || fail "missing required file: $1"
+}
+
+require_marker() {
+  marker=$1
+  shift
+  found=0
+  for file in "$@"; do
+    if [ -f "$file" ] && grep -Fq "$marker" "$file"; then
+      found=1
+    fi
+  done
+  [ "$found" = 1 ] || fail "missing release marker: $marker"
+}
+
+require_file LICENSE
+require_file README.md
+require_file SECURITY.md
+require_file .gitignore
+require_file .jerboa/security.json
+require_file docs/threat-model.md
+require_file docs/ffi-boundary.md
+require_file docs/release-evidence.md
+require_file tools/timing-evidence.sh
+require_file tools/sbom.sh
+require_file tools/reproducibility-report.sh
+require_file tools/sanitize-evidence.sh
+require_file ffi-shim.c
+require_file support/adversarial-corpus-evidence.ss
+
+python3 -m json.tool .jerboa/security.json >/dev/null
+
+require_marker "not a sandbox" SECURITY.md docs/threat-model.md
+require_marker "adversarial_corpus_status: documented" docs/release-evidence.md
+require_marker "adversarial_corpus_cases_minimum: 512" docs/release-evidence.md
+require_marker "JSH_TARGET_TIMING_PROOF_FILE" SECURITY.md docs/release-evidence.md .jerboa/security.json
+require_marker "sensitive_artifact_policy=no-command-logs-history-env-or-private-paths" docs/release-evidence.md tools/timing-evidence.sh
+require_marker "Full standalone binary reproducibility" docs/release-evidence.md docs/threat-model.md
+require_marker "ffi_ensure_std_fds" ffi-shim.c docs/ffi-boundary.md
+
+tracked_generated=$(git ls-files 'src/**' 'jsh-src/**' 'dist/**' 'jsh' 'jsh-macos' '*.so' '*.dylib' '*.wpo' '*.o' 2>/dev/null || true)
+[ -z "$tracked_generated" ] || fail "generated artifacts are tracked: $tracked_generated"
+
+secret_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}|xox[baprs]-[A-Za-z0-9-]{10,})'
+secret_hits=$(git grep -n -I -E "$secret_pattern" -- . || true)
+[ -z "$secret_hits" ] || {
+  printf '%s\n' "$secret_hits" >&2
+  fail "high-confidence secret pattern found"
+}
+
+doc_private_hits=$(git grep -n -I -E '(/Users/|~/mine|git@|users-MacBook-Pro)' -- README.md SECURITY.md docs .github 2>/dev/null || true)
+[ -z "$doc_private_hits" ] || {
+  printf '%s\n' "$doc_private_hits" >&2
+  fail "public docs or CI contain private path/host/SSH material"
+}
+
+echo "security_status=pass"
diff --git a/tools/timing-evidence.sh b/tools/timing-evidence.sh
index 7b0c4cf..07a84ec 100755
--- a/tools/timing-evidence.sh
+++ b/tools/timing-evidence.sh
@@ -5,6 +5,7 @@ ROOT=${REPO_ROOT:-$(pwd)}
 OUT=${JSH_TIMING_EVIDENCE_DIR:-${DIST_DIR:-$ROOT/dist}/timing-evidence}
 PROOF_FILE=${JSH_TARGET_TIMING_PROOF_FILE:-}
 REQUIRE_PROOF=${JSH_REQUIRE_TARGET_TIMING_PROOF:-0}
+MAX_PROOF_BYTES=${JSH_TARGET_TIMING_PROOF_MAX_BYTES:-65536}
 
 PRIVATE_PATTERN='(/Users/|/home/[^[:space:]\"]+/(mine|src|work)|~/mine|git@|\.local|\.lan|\.corp|\.internal|10\.[0-9]{1,3}\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)'
 SECRET_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})'
@@ -25,7 +26,7 @@ sensitive_artifact_policy=no-command-logs-history-env-or-private-paths
 
 {
   printf 'generated_at_utc=%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
-  printf 'uname=%s\n' "$(uname -a)"
+  printf 'uname=%s\n' "$(uname -srm)"
   printf 'git_status:\n'
   git status --short 2>/dev/null || true
 } > "$OUT/local-host.txt"
@@ -43,6 +44,9 @@ if [ -n "$PROOF_FILE" ] || [ "$REQUIRE_PROOF" = "1" ]; then
   if [ -z "$PROOF_FILE" ] || [ ! -r "$PROOF_FILE" ]; then
     status=blocked-target-timing-proof
     target_timing_proof_status=missing
+  elif [ "$(wc -c < "$PROOF_FILE" | tr -d ' ')" -gt "$MAX_PROOF_BYTES" ]; then
+    status=blocked-target-timing-proof
+    target_timing_proof_status=too-large
   elif grep -Eiq "$PRIVATE_PATTERN|$SECRET_PATTERN" "$PROOF_FILE"; then
     status=blocked-target-timing-proof
     target_timing_proof_status=sensitive-material-rejected