You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- THREAT_MODEL.md: model the opt-in js-exec/QuickJS surface (new 4.11),
qualify the child_process claim in 3.5 (virtual shim), and state the
no-bash-to-host-JS architectural invariant guarding 4.2/4.3
- CLAUDE.md: align WASM policy with reality (sql.js, quickjs-emscripten,
vendored CPython as approved exceptions)
- README: document node as a js-exec alias; list tar/yq/xan (and
js-exec/node) as unavailable in browser builds
- CI: add coverage.yml (non-gating coverage report artifact)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CLAUDE.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
5
5
## Project Overview
6
6
7
-
just-bash is a TypeScript implementation of a bash interpreter with an in-memory virtual filesystem. Designed for AI agents needing a secure, sandboxed bash environment. No WASM dependencies allowed.
7
+
just-bash is a TypeScript implementation of a bash interpreter with an in-memory virtual filesystem. Designed for AI agents needing a secure, sandboxed bash environment. No new WASM dependencies without approval; approved exceptions are sql.js (SQLite), quickjs-emscripten (js-exec), and the vendored CPython WASM (python3).
- Always verify with `pnpm typecheck && pnpm lint:fix && pnpm knip && pnpm test:run` before finishing
258
258
- Assert full stdout/stderr in tests, not partial matches
259
259
- Implementation must match real bash behavior, not convenience
260
-
-Dependencies using WASM are not allowed (exception: sql.js for SQLite, approved for security sandboxing)
260
+
-No new WASM dependencies without approval. Approved exceptions: sql.js (SQLite), quickjs-emscripten (js-exec), and the vendored CPython WASM (python3); see THREAT_MODEL.md §4.7
261
261
- We explicitly don't support 64-bit integers
262
262
- All parsing/execution must have reasonable limits to prevent runaway compute
| child_process | spawn/exec/fork | Not imported anywhere; no code path from interpreter| Architecture |
162
+
| child_process | spawn/exec/fork | Not imported anywhere; no code path from interpreter. **With `javascript` enabled**, a *virtual*`child_process` module exists *inside* QuickJS (`js-exec-worker.ts:261–280`) whose `execSync`/`spawnSync` re-enter the sandbox via the SAB bridge, not the host OS — see §4.11 | Architecture (+ virtual shim, §4.11)|
163
163
164
164
### 3.6 Information Disclosure
165
165
@@ -249,6 +249,8 @@ If any code captures a reference to `Function`, `eval`, etc. **before** the defe
249
249
250
250
**Mitigation**: Defense-in-depth is a secondary layer. The primary defense is that no code path exists from bash interpretation to JavaScript execution.
251
251
252
+
**Architectural invariant**: §4.2 and §4.3 both depend on a single invariant — **no bash→host-JS code path**: there must be no route by which an untrusted bash script can reach the host's `eval`, `new Function`, or dynamic `import()`, the only primitives that could turn these defense-in-depth gaps into a real escape. While that invariant holds, §4.2 and §4.3 stay LOW (defense-in-depth is secondary). If the invariant ever breaks, these residuals become **CRITICAL** — a pre-captured `Function` reference (§4.2) or a `globalThis.Function` reassignment (§4.3) would then be a direct JS-execution escape. The invariant is **guarded by the `check-banned-patterns` linter** (`scripts/check-banned-patterns.js`), which bans `eval(`, `new Function(`, and non-literal dynamic `import()` in source, so a path to host-JS code execution cannot be introduced silently. See §4.1 (three-layer `import()` mitigation) and §4.11 (the opt-in js-exec surface, the *only* intentional guest-JS execution path, which keeps the guest off the host's `eval`/`Function`).
253
+
252
254
### 4.3 globalThis Property Reassignment
253
255
254
256
**Risk**: LOW (defense-in-depth is secondary)
@@ -257,6 +259,8 @@ Attackers within the sandbox could overwrite `globalThis.Function` or use `Objec
257
259
258
260
**Mitigation**: Same as §4.2 — relies on no code path existing, not on the monkey-patching being unbypassable.
259
261
262
+
**Architectural invariant**: See §4.2 — relies on the same no-bash→host-JS-code-path invariant, which is guarded by the `check-banned-patterns` linter (`scripts/check-banned-patterns.js` bans `eval(`, `new Function(`, non-literal `import()`). Becomes **CRITICAL** if that invariant ever breaks.
263
+
260
264
### 4.4 Signal/Job Control Not Fully Modeled
261
265
262
266
**Risk**: LOW
@@ -328,6 +332,36 @@ Heredocs with variable expansion are size-limited (10MB) but nested heredocs wit
328
332
329
333
`Reflect` is frozen (not blocked) in the defense-in-depth layer. `Reflect.construct`, `Reflect.apply` etc. remain callable but cannot construct `Function` directly because the `Function` constructor itself is blocked.
**Risk**: MEDIUM (intentional, opt-in, isolation by construction)
338
+
339
+
When `javascript: true` — or an `invokeTool` hook is provided, which implicitly enables js-exec (`src/Bash.ts`) — the `js-exec` and `node` commands execute untrusted JavaScript/TypeScript inside QuickJS (compiled to WASM via Emscripten) in a dedicated Worker thread. The `node` command is a stub that reroutes to js-exec; both surface the same boundary. Like Python (§4.7), this is an opt-in code-execution surface whose safety rests on isolation by construction rather than on a JS-level sandbox over the host.
340
+
341
+
**Enabling the surface**: js-exec is registered only when `options.javascript || jsConfig.invokeTool` (`src/Bash.ts`). It is absent from the command registry and unreachable from any bash script unless the host explicitly turns it on. The `invokeTool` hook enables js-exec implicitly because the hook is meaningless without it.
342
+
343
+
**Host bridge (SharedArrayBuffer)**: User code runs inside QuickJS, which has no Node.js APIs of its own. A synchronous SharedArrayBuffer protocol (`src/commands/worker-bridge/protocol.ts`) bridges selected host capabilities back into the guest:
344
+
-**Virtual filesystem** — file read/write/stat are routed through the bridge to the in-memory OverlayFS, the same VFS the interpreter uses; no host-FS access.
345
+
-**`exec`** — `bridge-handler.ts` calls the host `exec` callback, which is `Bash.exec.bind(this)` (`src/Bash.ts`), i.e. it **re-enters the sandbox interpreter**, not the OS. Output is capped to the remaining execution deadline.
346
+
-**`fetch`** — Web Fetch API, gated by the host `secureFetch` allow-list (off by default).
347
+
-**`invokeTool`** — host-supplied tool hook for agent-driven tool calls; absent unless the host provides it.
348
+
349
+
**Virtual `child_process` shim**: Inside QuickJS, `import "child_process"` resolves to a **virtual module** (`js-exec-worker.ts:261–280`), not Node's `child_process`. Its `execSync`/`exec`/`spawnSync` route through `globalThis[Symbol.for('jb:exec')]` → the bridge → `Bash.exec`, so they re-enter the sandbox (no `spawn`/`fork`/OS process). This is the shim referenced from §3.5. Re-entrant js-exec is detected via AsyncLocalStorage and rejected to prevent deadlock ("recursive invocation is not supported").
350
+
351
+
**Isolation rests on**:
352
+
-**QuickJS WASM** — untrusted JS executes in the QuickJS interpreter within WASM linear memory; the guest has no access to Node.js host objects, only the four explicitly bridged primitives above.
353
+
-**Worker thread** — a dedicated worker per execution; terminated on timeout (`worker.terminate()`) and recycled after idle.
354
+
-**`WorkerDefenseInDepth`** — activated after QuickJS loads with only `shared_array_buffer`, `atomics`, `process_stdout`, `process_stderr` excluded (SAB/Atomics are required by the sync bridge; stdout/stderr because Emscripten routes WASM output through Node's console).
355
+
-**Memory cap** — QuickJS memory limited to 64MB (`MEMORY_LIMIT`, `js-exec-worker.ts`).
356
+
-**Timeout** — 10s default, 60s when network/fetch is enabled (`DEFAULT_JS_TIMEOUT_MS` / `DEFAULT_JS_NETWORK_TIMEOUT_MS`, `js-exec.ts`); enforced by terminating the worker.
357
+
358
+
**Accepted behaviors** (not vulnerabilities):
359
+
-`eval()`/`new Function()` inside QuickJS execute arbitrary *guest* JS — same posture as Python's `eval()`/`exec()` (§4.7); no host-JS escalation path because the guest cannot reach the host's `eval`/`Function`.
360
+
- The bridge `exec` re-enters the sandbox, so any command reachable from js-exec is one the host already permitted in the bash environment; it does not widen the command surface beyond what bash itself can reach.
361
+
- TS source is transpiled to JS before being handed to QuickJS; this is a guest-side convenience, not a host code path.
362
+
363
+
**Residual risk**: The security of this surface depends on the QuickJS/WASM boundary holding — i.e. the guest genuinely cannot reach host-JS primitives except through the four bridged calls above. The escape vectors to watch are a QuickJS/WASM breakout, or a bridge-call handler that acts on attacker-controlled arguments without validation (path validation in the FS bridge is delegated to the OverlayFs gates; `exec` re-enters the interpreter, which applies its own limits). Severity is **MEDIUM**, analogous to Python (§4.7): opt-in, isolated by construction, bounded by memory + timeout, and only as trustworthy as the WASM boundary it rests on. This surface is also the one residual that can weaken the no-bash→host-JS-code-path invariant of §4.2/§4.3 — but only if the WASM boundary is broken, since the guest is intentionally kept off the host's `eval`/`Function`/`import()`.
Copy file name to clipboardExpand all lines: packages/just-bash/README.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -70,7 +70,7 @@ Custom commands receive a `CommandContext` with `fs`, `cwd`, `env`, `stdin`, and
70
70
71
71
### Optional Runtimes
72
72
73
-
`js-exec` (JavaScript/TypeScript via QuickJS; requires `javascript: true`), `python3`/`python` (Python via CPython; requires `python: true`)
73
+
`js-exec` (JavaScript/TypeScript via QuickJS; requires `javascript: true`), `python3`/`python` (Python via CPython; requires `python: true`). The `node` command is an alias of the `js-exec` runtime, not a real Node.js process.
74
74
75
75
### Compression & Archives
76
76
@@ -594,7 +594,7 @@ All limits have defaults. Error messages tell you which limit was hit. Increase
594
594
595
595
## Browser Support
596
596
597
-
The core shell (parsing, execution, filesystem, and all built-in commands) works in browser environments. The following features require Node.js and are unavailable in browsers: `python3`/`python`, `sqlite3`, `js-exec`, and `OverlayFs`/`ReadWriteFs` (which access the real filesystem).
597
+
The core shell (parsing, execution, filesystem, and all built-in commands) works in browser environments. The following features require Node.js and are unavailable in browsers: `python3`/`python`, `sqlite3`, `js-exec`/`node`, `tar`, `yq`, `xan`, and `OverlayFs`/`ReadWriteFs` (which access the real filesystem).
0 commit comments