security: remove core shell status helpers

ober

b8371c26d0c2dc5b93612809fee45f94a7724a91

diff --git a/docs/kimi3-security-recommmendations.md b/docs/kimi3-security-recommmendations.md
index 938e37b..e98c3f9 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -1133,6 +1133,11 @@ fix must add its scanner rule in the same commit (write it into
   `support/pre-commit-security-scan.sh` provides an opt-in staged-diff hook
   for `jerboa_security_scan`; `docs/devex.md`, `docs/security-reference.md`,
   and `AGENTS.md` document the hook and the same-commit scanner-rule policy.
+  The core `create-directory*` compatibility helper now implements recursive
+  directory creation with `mkdir`/`path-directory` instead of shelling out to
+  `mkdir -p`, closing the scanner's critical command-concatenation finding at
+  that language surface. The core `process-status` compatibility helper also
+  no longer constructs a shell `wait` command.
 
 ### K3-P2-06 — Documentation consistency pass
 **Serves:** G5. **Effort:** 2 days.
diff --git a/docs/security-reference.md b/docs/security-reference.md
index 6711a5e..d8cbb30 100644
--- a/docs/security-reference.md
+++ b/docs/security-reference.md
@@ -1001,6 +1001,9 @@ These are known gaps documented as current limitations, not implementation promi
   AEAD, KDF, and password-hashing compatibility APIs now route through the Rust
   native crypto boundary.
 - **FFI audit (Phase 5 of parser hardening) is in progress.** `make ffi-audit-report` inventories Scheme FFI sites, Rust C ABI exports, pointer/width-sensitive bindings, blocking candidates, and Rust unsafe sites. Native unsafe review and selected Scheme FFI slices, including the regex, compression, crypto, OpenSSL TLS, rustls TLS, POSIX, async-process, and nREPL TCP wrappers, are remediated; the remaining Scheme per-binding null-return, bounds, ownership, and GC-safety review is tracked in [ffi-audit.md](ffi-audit.md).
+- **Core compatibility helpers avoid shell construction.** `create-directory*`
+  performs recursive `mkdir` directly, and `process-status` no longer builds a
+  shell `wait` command.
 - **No red team evaluation.** No independent adversarial testing has been performed.
 - **Secure memory still exposes a raw region escape hatch.** The high-level
   `secure-bytevector` API is bounds-checked and integrated with
diff --git a/docs/status.md b/docs/status.md
index 29e428f..b7a65ba 100644
--- a/docs/status.md
+++ b/docs/status.md
@@ -29,7 +29,7 @@ release artifacts are built as Jerboa multicall binaries with `jerboa`,
 | Native Rust exports | The native export review now has 190 exported functions: 183 tracked Scheme references and 7 retained standalone C/binary helpers. The previous 35 no-Scheme-reference removal candidates no longer have C ABI export markers. | Re-run `make native-export-review-check` whenever adding or removing native exports. |
 | Confined worker | `(std security worker)` provides the facade, audit lifecycle, output caps, deadlines, process-group kill, memory rlimit pre-exec setup, Linux syscall/ptrace seccomp pre-exec setup, Linux Landlock filesystem/TCP-connect setup for requested axes, macOS Seatbelt deny-default path/exec/no-network setup for supported axes, standard worker-eval Capsicum entry on FreeBSD, explicit sandbox-axis refusal, egress proxy env wiring, and platform CI smoke for Linux/macOS/FreeBSD sandbox paths. | Keep Linux/macOS/FreeBSD parity tests current; finish arbitrary-command/proxy-aware Capsicum worker paths. |
 | Fuzzing | `tests/fuzz/corpus/` has 15 checked-in seed inputs, `tests/fuzz/regression/` has 11 crash/rejection regressions, `make fuzz-smoke` runs the deterministic regression gate first, and GitHub CI runs smoke fuzzing normally plus deep fuzzing on scheduled daily runs. | Keep adding minimized corpus and regression inputs for every parser/security bug found. |
-| Safe surface | Direct scripts default to the safe prelude; raw access requires `--unsafe-prelude` or `(jerboa prelude unsafe)`. | Continue moving risky APIs behind explicit unsafe imports as new modules land. |
+| Safe surface | Direct scripts default to the safe prelude; raw access requires `--unsafe-prelude` or `(jerboa prelude unsafe)`. Core compatibility helpers avoid shell construction for recursive directory creation and process-status cleanup. | Continue moving risky APIs behind explicit unsafe imports as new modules land. |
 
 ## Compatibility Notes
 
diff --git a/src/jerboa/core.ss b/src/jerboa/core.ss
index 3479b67..d9fbc22 100644
--- a/src/jerboa/core.ss
+++ b/src/jerboa/core.ss
@@ -906,12 +906,21 @@
   ;; create-directory: Gerbil alias for Chez mkdir
   (define create-directory mkdir)
 
-  ;; create-directory*: recursive mkdir -p
-  ;; Uses strict quoting to prevent shell injection via path names.
+  ;; create-directory*: recursive mkdir -p without invoking a shell.
   (define (create-directory* path)
-    (system (string-append "mkdir -p '"
-              (string-replace-simple path "'" "'\"'\"'")
-              "'")))
+    (define (mkdir-if-missing p)
+      (unless (or (string=? p "") (file-directory? p))
+        (mkdir p)))
+    (define (ensure p)
+      (let ([parent (path-directory p)])
+        (when (and parent
+                   (not (string=? parent ""))
+                   (not (string=? parent p))
+                   (not (file-directory? parent)))
+          (ensure parent))
+        (mkdir-if-missing p)))
+    (unless (and (string? path) (= (string-length path) 0))
+      (ensure path)))
 
   ;; file-info record type (using Chez fields syntax)
   (define-record-type (file-info-rec make-file-info-rec file-info-rec?)
@@ -1256,21 +1265,16 @@
   (define (open-input-process plist)
     (open-process plist))
 
-  ;; process-status: Gambit compat — wait for process and return exit code.
-  ;; Drains the port to allow the subprocess to finish, then retrieves the PID
-  ;; from our tracking table and waits for the real exit status.
+  ;; process-status: Gambit compat — drain the process port and return status.
+  ;; The legacy shell-based wait path could not reliably wait on this child from
+  ;; a separate shell, so keep the de facto 0 status without constructing a
+  ;; command string.
   (define (process-status proc)
-    ;; Close port to signal we're done; process will exit
     (when (input-port? proc)
       (let drain ()
         (let ((ch (read-char proc)))
           (unless (eof-object? ch)
             (drain)))))
-    ;; Try to get real exit status via the PID we stored
-    (let ([pid (hashtable-ref *process-pids* proc #f)])
-      (if pid
-        (guard (exn [#t 0])
-          ;; Use waitpid via system call
-          (let ([status (system (string-append "wait " (number->string pid) " 2>/dev/null; echo $?"))])
-            status))
-        0)))
+    (when (hashtable-ref *process-pids* proc #f)
+      (hashtable-delete! *process-pids* proc))
+    0)