# Embedded TypeScript (ZapCode)
> **Experimental.** ZapCode is an early-stage TypeScript interpreter that may
> have undiscovered crash or security bugs. Resource limits are enforced by
> ZapCode's VM. The integration should be treated as experimental.
Bashkit embeds the [ZapCode](https://github.com/TheUncharted/zapcode) TypeScript
interpreter, a pure-Rust implementation with ~2µs cold start, no V8 dependency,
and built-in sandboxing. TypeScript runs entirely in-memory with configurable
resource limits and no host access.
**See also:**
- [Threat Model](./threat-model.md) - Security considerations (TM-TS-*)
- [Custom Builtins](./custom_builtins.md) - Writing your own builtins
- [Compatibility Reference](./compatibility.md) - Bash feature support
- [`specs/zapcode-runtime.md`][spec] - Full specification
## Quick Start
Enable the `typescript` feature and register via builder:
```rust
use bashkit::Bash;
# #[tokio::main]
# async fn main() -> bashkit::Result<()> {
let mut bash = Bash::builder().typescript().build();
let result = bash.exec("ts -c \"console.log('hello from ZapCode')\"").await?;
assert_eq!(result.stdout, "hello from ZapCode\n");
# Ok(())
# }
```
You can also register the same builtins through the extension model:
```rust
use bashkit::{Bash, TypeScriptExtension};
let bash = Bash::builder()
.extension(TypeScriptExtension::default())
.build();
```
## Usage Patterns
### Inline Code
```bash
ts -c "console.log(2 ** 10)"
# Output: 1024
# Node.js, Deno, and Bun aliases also work
node -e "console.log('hello')"
deno -e "console.log('hello')"
bun -e "console.log('hello')"
```
### Expression Evaluation
When no `console.log()` is called, the last expression is displayed (REPL behavior):
```bash
ts -c "1 + 2 * 3"
# Output: 7
```
### Script Files (from VFS)
```bash
cat > /tmp/script.ts << 'EOF'
const data = [1, 2, 3, 4, 5];
const sum = data.reduce((a, b) => a + b, 0);
console.log(`sum=${sum}, avg=${sum / data.length}`);
EOF
ts /tmp/script.ts
```
### Pipelines and Command Substitution
```bash
result=$(ts -c "console.log(42 * 3)")
echo "Result: $result"
## Virtual Filesystem (VFS) Bridging
VFS operations are available as async global functions in the TypeScript
environment. Files created by bash are readable from TypeScript and vice versa.
### Bash → TypeScript
```bash
echo "important data" > /tmp/shared.txt
ts -c "await readFile('/tmp/shared.txt')"
# Output: important data
```
### TypeScript → Bash
```bash
ts -c "await writeFile('/tmp/result.txt', 'computed by ts\n')"
cat /tmp/result.txt
# Output: computed by ts
```
### Supported VFS Operations
| Read file | `readFile(path)` | `string` |
| Write file | `writeFile(path, content)` | `void` |
| Check exists | `exists(path)` | `boolean` |
| List directory | `readDir(path)` | `string[]` |
| Create directory | `mkdir(path)` | `void` |
| Delete | `remove(path)` | `void` |
| File metadata | `stat(path)` | JSON string |
### Architecture
```text
TS code → ZapCode VM → ExternalFn("readFile", [path]) → Bashkit VFS → resume
```
ZapCode suspends at external function calls, Bashkit bridges them to the VFS,
then resumes execution with the return value.
**Note:** `console.log()` output produced *after* a VFS call is not captured
due to a `zapcode-core` API limitation. Use the return-value pattern instead —
the last expression's value is printed automatically.
## Resource Limits
Default limits prevent runaway TypeScript code. Customize via `TypeScriptLimits`:
```rust,no_run
use bashkit::{Bash, TypeScriptLimits};
use std::time::Duration;
# fn main() {
let bash = Bash::builder()
.typescript_with_limits(
TypeScriptLimits::default()
.max_duration(Duration::from_secs(5))
.max_memory(16 * 1024 * 1024) // 16 MB
.max_allocations(100_000)
.max_stack_depth(100)
)
.build();
# }
```
| Duration | 30 seconds | Execution timeout |
| Memory | 64 MB | Heap memory cap |
| Stack depth | 512 | Call stack depth |
| Allocations | 1,000,000 | Heap allocation cap |
## Configuration
Use `TypeScriptConfig` for full control over aliases and hint behavior:
```rust,no_run
use bashkit::{Bash, TypeScriptConfig, TypeScriptLimits};
use std::time::Duration;
# fn main() {
// Default: ts, typescript, node, deno, bun + unsupported-mode hints
let bash = Bash::builder().typescript().build();
// Only ts/typescript commands, no node/deno/bun aliases
let bash = Bash::builder()
.typescript_with_config(TypeScriptConfig::default().compat_aliases(false))
.build();
// Disable unsupported-mode hints (plain errors only)
let bash = Bash::builder()
.typescript_with_config(TypeScriptConfig::default().unsupported_mode_hint(false))
.build();
// Custom limits + selective config
let bash = Bash::builder()
.typescript_with_config(
TypeScriptConfig::default()
.limits(TypeScriptLimits::default().max_duration(Duration::from_secs(5)))
.compat_aliases(false)
)
.build();
# }
```
### Unsupported Mode Hints
When enabled (default), using unsupported Node/Deno/Bun flags or subcommands
produces helpful guidance:
```text
$ node --inspect app.js
node: unsupported option or subcommand: --inspect
hint: This is an embedded TypeScript interpreter (ZapCode), not Node.js.
hint: Only inline execution is supported:
hint: node -e "console.log('hello')" # run inline code
hint: node script.js # run file from VFS
## LLM Tool Integration
When using `BashTool` for AI agents, call `.typescript()` on the tool builder:
```rust,ignore
use bashkit::{BashTool, Tool};
let tool = BashTool::builder()
.typescript()
.build();
// help() and system_prompt() automatically document TypeScript limitations
let help = tool.help();
```
The builtin's `llm_hint()` is automatically included in the tool's documentation,
so LLMs know not to generate code using `import`, `eval()`, or HTTP.
## Limitations
**No `import`/`require`.** ZapCode has no module system. All code runs in a
single scope.
**No `eval()`/`Function()`.** Dynamic code generation is blocked at the
language level.
**No HTTP/network.** No `fetch`, `XMLHttpRequest`, or network APIs. ZapCode
has no network primitives.
**No `process`/`Deno`/`Bun` globals.** Runtime-specific APIs are not available.
Only standard TypeScript/JavaScript language features work.
**No npm packages.** Only built-in language features and registered external
functions are available.
**stdout after VFS calls.** `console.log()` output after an `await readFile()`
or similar VFS call is not captured. Use the return-value pattern: make the
last expression the value you want printed.
## Security
All TypeScript execution runs in a virtual environment:
- **No host filesystem access** — all paths resolve through the VFS
- **No network access** — no sockets, HTTP, or DNS
- **No dynamic code execution** — `eval()`, `Function()`, `import` blocked
- **Resource limited** — time, memory, stack depth, and allocation caps
- **Path traversal safe** — `../..` is resolved by VFS path normalization
- **Opt-in only** — requires both `typescript` feature AND `.typescript()` builder call
See threat IDs TM-TS-001 through TM-TS-023 in the [threat model](./threat-model.md).
[spec]: https://github.com/everruns/bashkit/blob/main/specs/zapcode-runtime.md