Experimental. ZapCode is an early-stage TypeScript interpreter. Resource limits are enforced by ZapCode's VM. Do not rely on it for untrusted-input safety without additional hardening.
Implemented (experimental)
BashKit provides sandboxed TypeScript/JavaScript execution via typescript,
ts, node, deno, and bun builtins, powered by the
ZapCode embedded TypeScript
interpreter written in Rust.
Enable with:
[dependencies]
bashkit = { version = "0.1", features = ["typescript"] }TypeScript builtins are not auto-registered. Enable via builder:
use bashkit::Bash;
// Default limits
let bash = Bash::builder().typescript().build();
// Custom limits
use bashkit::TypeScriptLimits;
use std::time::Duration;
let bash = Bash::builder()
.typescript_with_limits(
TypeScriptLimits::default()
.max_duration(Duration::from_secs(5))
.max_memory(16 * 1024 * 1024)
)
.build();The typescript feature flag enables compilation; .typescript() on the builder
enables registration. This matches the python pattern
(Bash::builder().python().build()).
- Pure Rust, no V8 or Node.js dependency
- Microsecond cold starts (~2 µs)
- Built-in resource limits (memory, time, stack depth)
- No filesystem/network/eval access by design (sandbox-safe)
- Snapshotable execution state (<2 KB)
- External function suspend/resume for VFS bridging
- Published on crates.io (
zapcode-core)
# Inline code
ts -c "console.log('hello')"
node -e "console.log('hello')"
# Expression evaluation (last expression printed)
ts -c "1 + 2 * 3"
# Script file (from VFS)
ts script.ts
node script.js
# Stdin
echo "console.log('hello')" | ts
ts - <<< "console.log('hi')"
# Version
ts --version
node --version
# All aliases
typescript -c "console.log('hello')"
deno -e "console.log('hello')"
bun -e "console.log('hello')"All aliases map to the same ZapCode interpreter:
| Command | Flag for inline code | Rationale |
|---|---|---|
ts |
-c |
Short alias for TypeScript |
typescript |
-c |
Full name |
node |
-e |
Node.js compatibility |
deno |
-e |
Deno compatibility (eval flag) |
bun |
-e |
Bun compatibility (eval flag) |
The -c and -e flags are both accepted by all aliases for convenience.
ZapCode enforces its own resource limits independent of BashKit's shell limits.
All limits are configurable via TypeScriptLimits:
| Limit | Default | Builder Method | Purpose |
|---|---|---|---|
| Max duration | 30 seconds | .max_duration(d) |
Prevent infinite loops |
| Max memory | 64 MB | .max_memory(bytes) |
Prevent memory exhaustion |
| Max stack depth | 512 | .max_stack_depth(n) |
Prevent stack overflow |
use bashkit::TypeScriptLimits;
use std::time::Duration;
// Tighter limits for untrusted code
let limits = TypeScriptLimits::default()
.max_duration(Duration::from_secs(5))
.max_memory(16 * 1024 * 1024) // 16 MB
.max_stack_depth(100);ZapCode implements a TypeScript/JavaScript subset:
Supported:
- Variables: let, const, var
- Arithmetic, comparison, logical operators
- String operations, template literals
- Arrays, objects, destructuring
- Functions, arrow functions, default parameters
- Async/await, Promises
- Array methods: map, reduce, filter, forEach, find, etc.
- Loops: for, for...of, for...in, while, do...while
- Conditionals: if/else, ternary, switch/case
- Type annotations (parsed but not enforced at runtime)
- Closures, generators
Not supported (ZapCode limitations):
import/require(no module system)eval()/Function()constructor- Filesystem access (use external functions)
- Network access (no fetch/XMLHttpRequest)
process,Deno,Bunglobal objects- DOM APIs
- Most Node.js/Deno/Bun standard library APIs
TypeScript code can access BashKit's virtual filesystem through external functions registered by the builtin. These functions are available as globals in the TypeScript environment:
# Write from bash, read from TypeScript
echo "data" > /tmp/shared.txt
ts -c "const content = await readFile('/tmp/shared.txt'); console.log(content)"
# Write from TypeScript, read from bash
ts -c "await writeFile('/tmp/out.txt', 'hello\n')"
cat /tmp/out.txt
# Check file existence
ts -c "console.log(await exists('/tmp/shared.txt'))"
# List directory
ts -c "const entries = await readDir('/tmp'); console.log(entries)"Bridged operations:
readFile(path: string): Promise<string>— read text from VFSwriteFile(path: string, content: string): Promise<void>— write to VFSexists(path: string): Promise<boolean>— check existencereadDir(path: string): Promise<string[]>— list directorymkdir(path: string): Promise<void>— create directoryremove(path: string): Promise<void>— delete file/directorystat(path: string): Promise<{size: number, isFile: boolean, isDir: boolean}>— metadata
Architecture:
TS code → ZapCode VM → ExternalFn("readFile", [path]) → BashKit VFS → resume
ZapCode suspends execution at external function calls, BashKit bridges the call to the VFS, and resumes execution with the result.
Limitation: stdout after VFS calls. ZapcodeSnapshot::resume() returns
VmState but does not expose the VM's accumulated stdout. This means
console.log() output produced after a VFS call is not captured. Use the
return-value pattern instead — the last expression's value is printed:
# ✓ Works: return value pattern
ts -c "await readFile('/tmp/f.txt')"
# ✓ Works: console.log before VFS call
ts -c "console.log('loading...'); await readFile('/tmp/f.txt')"
# ✗ Lost output: console.log after VFS call
ts -c "const data = await readFile('/tmp/f.txt'); console.log(data)"This is a zapcode-core API limitation. Upstream fix tracked.
Host applications can register custom external functions that TypeScript code can call by name. This enables TypeScript scripts to invoke host-provided capabilities (e.g., tool calls, API requests).
Builder API:
use bashkit::{Bash, TypeScriptLimits, TypeScriptExternalFnHandler};
use serde_json::Value;
use std::sync::Arc;
let handler: TypeScriptExternalFnHandler = Arc::new(|name, args| {
Box::pin(async move {
Ok(Value::Number(42.into()))
})
});
let bash = Bash::builder()
.typescript_with_external_handler(
TypeScriptLimits::default(),
vec!["getAnswer".into()],
handler,
)
.build();ZapCode runs directly in the host process via zapcode-core. No subprocess,
no IPC. Resource limits are enforced by ZapCode's own VM.
use bashkit::{Bash, TypeScriptLimits};
// Default limits
let bash = Bash::builder().typescript().build();
// Custom limits
let bash = Bash::builder()
.typescript_with_limits(TypeScriptLimits::default().max_duration(Duration::from_secs(5)))
.build();See specs/threat-model.md section "TypeScript / ZapCode Security (TM-TS)"
for the full threat analysis.
Bash variables are expanded before reaching the TypeScript builtin. This is
by-design consistent with all other builtins. Use single quotes to prevent
expansion: ts -c 'console.log("hello")'.
ZapCode enforces independent resource limits. Even if BashKit's shell limits are generous, TypeScript code cannot exceed ZapCode's time/memory/stack caps.
TypeScript code has no direct filesystem access. VFS-bridged functions go
through BashKit's virtual filesystem. /etc/passwd reads from VFS, not host.
ZapCode blocks eval(), Function(), import, and require at the language
level. These are not implemented in the interpreter.
TypeScript console.log output is captured in memory. The memory limit on ZapCode prevents unbounded output generation.
- Syntax errors: Exit code 1, error message on stderr
- Runtime errors: Exit code 1, error on stderr, any stdout preserved
- File not found: Exit code 2, error on stderr
- Missing
-c/-eargument: Exit code 2, error on stderr - Unknown option: Exit code 2, error on stderr
When TypeScript is registered via BashToolBuilder::typescript(), the builtin
contributes a hint to help() and system_prompt():
ts/node/deno/bun: Embedded TypeScript (ZapCode). Supports ES2024 subset. File I/O via readFile()/writeFile() async functions. No npm/import/require. No HTTP/network. No eval().
ts/typescript/node/deno/bunall map to the same builtin- Works in pipelines:
echo "data" | ts -c "..." - Works in command substitution:
result=$(ts -c "console.log(42)") - Works in conditionals:
if ts -c "throw new Error()"; then ... else ... fi - Shebang lines (
#!/usr/bin/env ts) are stripped automatically
# Build with typescript feature
cargo build --features typescript
# Run unit tests
cargo test --features typescript --lib -- typescript
# Run spec tests
cargo test --features typescript --test spec_tests -- typescript