tiny hardened wrapper around Bun.spawn. allowlist, timeout, output cap. never invokes a shell.
bun add bun-spawn-safeimport { safeSpawn } from "bun-spawn-safe";
const r = await safeSpawn({
cmd: "git",
args: ["log", "--oneline", "-n", "5"],
timeoutMs: 5_000,
maxOutputBytes: 64 * 1024,
allowList: ["git", "ls", "cat"],
});
r.exitCode; // 0
r.stdout; // "abc1234 init\n..."
r.truncated; // false
r.durationMs; // 42one function. takes a cmd plus argv. spawns it directly via Bun.spawn. captures stdout/stderr to a byte cap. kills it on a timeout. returns a flat result object.
never passes through /bin/sh, never builds a string, never expands globs. args is argv, not a command line.
- not a shell. you cannot pipe, redirect, or chain commands. write code for that.
- not a sandbox. it does not jail the filesystem or network. pair with
agent-sandbox,bwrap, or your platform of choice if you need isolation. - not cross-runtime. Bun only. uses
Bun.spawn.
interface SpawnOptions {
cmd: string;
args?: string[];
cwd?: string;
timeoutMs?: number; // default 30_000
maxOutputBytes?: number; // default 65_536, per stream
allowList?: string[]; // exact-match against cmd
env?: string[] | Record<string, string>;
stdin?: string | Uint8Array;
}
interface SpawnResult {
exitCode: number; // 124 on timeout
stdout: string;
stderr: string;
killed?: "timeout";
truncated: boolean; // true if either stream hit the cap
durationMs: number;
}
function safeSpawn(opts: SpawnOptions): Promise<SpawnResult>;
class DisallowedCommandError extends Error { cmd: string; allowList: string[]; }
class InvalidCommandError extends Error { cmd: string; reason: string; }env semantics:
undefined: fullprocess.envis forwarded (Bun default)string[]: only those keys are forwarded, missing keys excludedRecord<string, string>: that exact env, no merge withprocess.env
cmd is rejected with InvalidCommandError if it contains /, \, ;, |, &, backtick, $(, or a null byte. this is a sanity check, not a security boundary. the real protection is that args is a real argv, not a string passed to a shell.
if allowList is set, cmd must match one of its entries by string equality. otherwise DisallowedCommandError is thrown.
small, focused tools that compose. use any one alone, all together if you need:
anchor- local proxy that caches AI API calls and tracks spendllm-pricing- usd list prices for major LLM apisanthropic-sse- streaming sse parser for anthropic messages apiopenai-stream-parse- same, for openai chat completionstg-bot-cf- tiny telegram bot framework for cloudflare workersbun-spawn-safe- hardened wrapper aroundBun.spawn
MIT