@everruns/bashkit

中文文档
0.21.0发布于 2026/11/11MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 3547

Sandboxed bash interpreter for JavaScript/TypeScript

原始使用文档
文档目录42 节
  1. Features
  2. Install
  3. Quick Start
  4. Sync Execution
  5. Async Execution
  6. Live Output
  7. Configuration
  8. BashOptions
  9. Virtual Filesystem
  10. Direct Methods on Bash and BashTool
  11. FileSystem Accessor
  12. Native Addon Interop
  13. Pre-Initialized Files
  14. Real Filesystem Mounts
  15. Live Mounts
  16. Error Handling
  17. Cancellation
  18. BashTool
  19. ScriptedTool
  20. Custom Builtins
  21. Script Analysis
  22. Snapshot / Restore
  23. Network
  24. Credential Injection
  25. Framework Integrations
  26. OpenAI
  27. Anthropic
  28. Vercel AI SDK
  29. LangChain
  30. API Reference
  31. Bash
  32. BashTool
  33. ScriptedTool
  34. BashOptions
  35. ScriptAnalysis
  36. BuiltinContext
  37. ExecuteOptions
  38. ExecResult and BashError
  39. Platform Support
  40. How It Works
  41. Part of Everruns
  42. License

@everruns/bashkit

Sandboxed bash interpreter for JavaScript and TypeScript. Native NAPI-RS bindings to the bashkit Rust core for Node.js, Bun, and Deno.

Homepage: bashkit.sh

Features

  • Sandboxed, in-process execution with a virtual filesystem
  • Full bash syntax: variables, pipelines, redirects, loops, functions, and arrays
  • 164 built-in commands including grep, sed, awk, jq, curl, and find
  • Sync and async execution APIs
  • Direct VFS helpers, standalone FileSystem handles, and live mounts
  • Cancellation support via cancel()
  • Sticky cancellation recovery via clearCancel()
  • Snapshot and restore support on Bash
  • Outbound HTTP (curl/wget) behind a URL allowlist with transparent credential injection
  • AI framework adapters for OpenAI, Anthropic, Vercel AI SDK, and LangChain

Install

npm install @everruns/bashkit   # Node.js
bun add @everruns/bashkit       # Bun
deno add npm:@everruns/bashkit  # Deno

Use a patched, supported Node.js release. The October 2026 security minimums are 22.23.4, 24.22.1, and 26.12.1 in their respective release lines; keep taking subsequent upstream security updates. The package's engines >=18 is an ABI compatibility floor, not security support for EOL Node 18/20. The Node 20 CI lane checks legacy compatibility only. Bashkit does not bundle Node.

Quick Start

Sync Execution

import { Bash } from "@everruns/bashkit";

const bash = new Bash();

const result = bash.executeSync('echo "Hello, World!"');
console.log(result.stdout); // Hello, World!\n

bash.executeSync("X=42");
console.log(bash.executeSync("echo $X").stdout); // 42\n

Async Execution

import { Bash } from "@everruns/bashkit";

const bash = new Bash();

const result = await bash.execute('echo -e "banana\\napple\\ncherry" | sort');
console.log(result.stdout); // apple\nbanana\ncherry\n

await bash.execute('printf "data\\n" > /tmp/file.txt');
console.log((await bash.execute("cat /tmp/file.txt")).stdout); // data\n

Live Output

const bash = new Bash();

const result = await bash.execute(
  "for i in 1 2 3; do echo out-$i; echo err-$i >&2; done",
  {
    onOutput({ stdout, stderr }) {
      if (stdout) process.stdout.write(stdout);
      if (stderr) process.stderr.write(stderr);
    },
  },
);

onOutput is optional and fires during execution with chunk objects shaped like { stdout, stderr }. Chunks are not line-aligned or exact terminal interleaving, but concatenating all callback chunks matches the final ExecResult.stdout and ExecResult.stderr. The handler must be synchronous; Promise-returning handlers are rejected. Do not call back into the same Bash / BashTool instance from onOutput via execute*, readFile, fs(), or similar same-instance APIs.

Configuration

BashOptions

import { Bash } from "@everruns/bashkit";

const bash = new Bash({
  username: "agent",
  hostname: "sandbox",
  cwd: "/home/agent",
  env: { PROJECT: "bashkit" },
  maxCommands: 1000,
  maxLoopIterations: 10000,
  maxMemory: 10 * 1024 * 1024,
  timeoutMs: 30_000,
  mounts: [{ path: "/workspace", root: "./src", writable: true }],
  python: false,
});

Virtual Filesystem

Direct Methods on Bash and BashTool

import { Bash } from "@everruns/bashkit";

const bash = new Bash();

bash.mkdir("/data", true);
bash.writeFile("/data/config.json", '{"debug":true}');
bash.appendFile("/data/config.json", "\n");

console.log(bash.readFile("/data/config.json"));
console.log(bash.exists("/data/config.json"));
console.log(bash.ls("/data"));
console.log(bash.glob("/data/*.json"));

BashTool exposes the same direct filesystem helpers.

FileSystem Accessor

import { Bash, FileSystem } from "@everruns/bashkit";

const source = new FileSystem();
source.writeFile("/org/repo/README.md", "hello\n");

const bash = new Bash();
bash.mount("/workspace", source);

console.log(bash.executeSync("cat /workspace/org/repo/README.md").stdout);

Call bash.fs() or tool.fs() when you need a live handle to the current interpreter filesystem. Use new FileSystem() or FileSystem.real(...) when you need a standalone mountable filesystem.

Native Addon Interop

import { Bash, FileSystem } from "@everruns/bashkit";
import { createFilesystem } from "filesystem-addon";

const imported = FileSystem.fromExternal(createFilesystem());

const bash = new Bash();
bash.mount("/workspace", imported);
console.log(bash.executeSync("ls /workspace").stdout);

toExternal() / fromExternal() exchange an opaque native External token for the versioned stable ABI handle, so JavaScript cannot inspect or mutate native handle bytes. Native addons should depend on bashkit with the interop feature and use bashkit::interop::fs.

Pre-Initialized Files

import { Bash } from "@everruns/bashkit";

const bash = new Bash({
  files: {
    "/config.json": '{"key":"value"}',
    "/lazy.txt": () => "computed on first read",
  },
});

console.log(bash.readFile("/config.json"));

const asyncBash = await Bash.create({
  files: {
    "/async.txt": async () => "loaded asynchronously",
  },
});

Real Filesystem Mounts

import { Bash } from "@everruns/bashkit";

const bash = new Bash({
  mounts: [
    { path: "/docs", root: "./docs" },
    { path: "/workspace", root: "./src", writable: true },
  ],
});

console.log(bash.executeSync("ls /workspace").stdout);

Live Mounts

import { Bash, FileSystem } from "@everruns/bashkit";

const bash = new Bash();
const workspace = FileSystem.real("./src", {
  writable: true,
  allowedMountPaths: ["./src"],
});

bash.mount("/workspace", workspace);
console.log(bash.executeSync("ls /workspace").stdout);
bash.unmount("/workspace");

Error Handling

import { Bash, BashError } from "@everruns/bashkit";

const bash = new Bash();

try {
  bash.executeSyncOrThrow("exit 1");
} catch (err) {
  if (err instanceof BashError) {
    console.log(err.exitCode);
    console.log(err.stderr);
  }
}

Cancellation

import { Bash } from "@everruns/bashkit";

const bash = new Bash();

const running = bash.execute("sleep 60");
bash.cancel();
await running;

bash.clearCancel(); // preserve session/VFS state before reusing the instance

cancel() sets a sticky flag that causes future executions to fail with "execution cancelled". Call clearCancel() after the cancelled execution has finished to reuse the same instance without losing shell or VFS state. Use reset() only when you want to discard state entirely.

BashTool exposes the same cancel(), clearCancel(), and reset() methods. For synchronous execution, executeSync(...) and executeSyncOrThrow(...) also accept { signal }.

Async execute(...) calls are serialized per instance and keep a bounded backlog. If too many executions are already pending on one Bash, BashTool, or ScriptedTool, the next async call rejects instead of retaining another queued command string. Commands larger than maxInputBytes are rejected before entering the async queue.

BashTool

BashTool wraps the interpreter with tool-contract metadata for agent frameworks:

  • name
  • version
  • shortDescription
  • description()
  • help()
  • systemPrompt()
  • inputSchema()
  • outputSchema()
import { BashTool } from "@everruns/bashkit";

const tool = new BashTool();

console.log(tool.name);
console.log(tool.inputSchema());

const result = tool.executeSync("echo hello");
console.log(result.stdout);

ScriptedTool

Use ScriptedTool to register JavaScript callbacks as bash-callable tools:

import { ScriptedTool } from "@everruns/bashkit/scripted";

const tool = new ScriptedTool({ name: "api" });
tool.addTool("get_user", "Fetch user by ID", (params) => {
  return JSON.stringify({ id: params.id, name: "Alice" });
});

const result = tool.executeSync("get_user --id 1 | jq -r '.name'");
console.log(result.stdout); // Alice

Custom Builtins

Register JS callbacks as bash builtins that share the Bash / BashTool instance's VFS, files created in one call persist across execute() calls.

import { Bash } from "@everruns/bashkit";

// Constructor-time registration
const bash = new Bash({
  customBuiltins: {
    "get-order": (ctx) =>
      JSON.stringify({ id: ctx.argv[0], status: "shipped" }) + "\n",
  },
});

// Or post-construction (safe to call at any time — no interpreter rebuild)
bash.addBuiltin("greet", (ctx) => `hello ${ctx.argv[0] ?? "world"}\n`);

// Tool output flows through the shared VFS
await bash.execute("mkdir -p /scratch");
await bash.execute("get-order 42 > /scratch/order.json");
console.log((await bash.execute("cat /scratch/order.json")).stdout);
// {"id":"42","status":"shipped"}

bash.removeBuiltin("greet"); // when you no longer need it

Callbacks can be sync (string return) or async (Promise<string>). Both are awaited uniformly by the Rust side. Exceptions / rejections become stderr with exit code 1.

const bash = new Bash({
  customBuiltins: {
    fetch: async (ctx) => {
      const data = await fetchSomething(ctx.argv[0]);
      return JSON.stringify(data) + "\n";
    },
    fail: () => {
      throw new Error("nope"); // → stderr, exit 1
    },
  },
});

The callback receives a BuiltinContext:

  • name: string, command name as invoked
  • argv: string[], arguments (not including the command name)
  • stdin: string | null, piped input, or null if no pipe
  • env: Record<string, string>, environment variables (only exported names)
  • cwd: string, current working directory
  • fs: FileSystem, live handle to the instance's virtual filesystem

ctx.fs is the same VFS the executing script sees: reads observe earlier script writes, and writes are visible to subsequent commands. It inherits mounts and read-only configuration.

const bash = new Bash({
  customBuiltins: {
    head10: (ctx) =>
      ctx.fs.readFile(ctx.argv[0]).split("\n").slice(0, 10).join("\n") + "\n",
  },
});
await bash.execute("head10 /var/log/app.log");

Override precedence: shell function > POSIX special builtin > custom builtin

baked-in builtin > PATH. Custom builtins can override baked-in commands (e.g. wrap cat), but shell functions defined in the script still win.

Custom builtins survive reset(). They are host-side configuration and are not preserved by snapshot() / restoreSnapshot(), pass customBuiltins again or call addBuiltin after restoring.

Use execute() (async). If the script invokes a custom builtin under executeSync() the builtin fails fast with exit code 1 and stderr "<name>: custom builtins require execute() (async). ..." instead of deadlocking, the JS event loop is blocked while the synchronous call is in flight, so the underlying Promise<string> callback could never run.

Script Analysis

analyze() reports what a script statically refers to, commands, arguments, redirect targets, function definitions, without running it. Use it to decide whether a model-produced command needs user approval.

const analysis = bash.analyze("cat notes.txt | grep -i todo > out.txt");

analysis.commandNames;          // ["cat", "grep"]
analysis.commands[1].args;      // ["-i", "todo"]
analysis.redirects[0].path;     // "out.txt"
analysis.redirects[0].isWrite;  // true
analysis.isOpaque;              // false

Words that are not fully literal report null, never a partial string:

const a = bash.analyze('rm "$target" /tmp/fixed');
a.commands[0].args; // [null, "/tmp/fixed"]

Advisory only. Static analysis cannot see through dynamic dispatch, eval, functions, or aliases. Those set isOpaque, and an allowlist check must consult it, c=rm; $c -rf /data contains no disallowed command name:

const READ_ONLY = new Set(["ls", "cat", "head", "grep", "wc", "echo"]);

function safeToRun(script: string): boolean {
  let analysis;
  try {
    analysis = bash.analyze(script);   // throws if the script does not parse
  } catch {
    return false;
  }
  if (analysis.isOpaque) return false;
  return analysis.commands.every(
    (c) => c.isAssignmentOnly || (c.name != null && READ_ONLY.has(c.name)),
  );
}

Commands inside $(…), loops, branches, and function bodies are reported too, each tagged with context ("direct", "substitution", "function_body"), so echo $(rm -rf /) never looks like a bare echo. Enforcement stays with the builtin registry, the network allowlist, and the mount policy.

Snapshot / Restore

State snapshots are available on both Bash and BashTool instances.

Security: unkeyed Bash snapshots use a public corruption-detection digest and are forgeable. Use hmacKey whenever snapshot bytes cross a trust boundary (network, user uploads, shared storage). BashTool snapshots require hmacKey because they include tool session state, VFS contents, and counters that may be restored in multi-tenant agent services.

import { Bash, BashTool } from "@everruns/bashkit";

const bash = new Bash({ username: "agent", maxCommands: 100 });
await bash.execute(
  "export BUILD_ID=42; mkdir -p /workspace && cd /workspace && echo ready > state.txt",
);

const snapshot = bash.snapshot();
const shellOnly = bash.snapshot({ excludeFilesystem: true });
const promptOnly = bash.snapshot({
  excludeFilesystem: true,
  excludeFunctions: true,
});

const secretKey = Buffer.from(process.env.BASHKIT_SNAPSHOT_KEY!, "hex");
const keyedSnapshot = bash.snapshotKeyed(secretKey);
const keyedRestored = Bash.fromSnapshotKeyed(keyedSnapshot, secretKey);

const restored = Bash.fromSnapshot(snapshot);
console.log((await restored.execute("echo $BUILD_ID")).stdout); // 42\n

restored.reset();
restored.restoreSnapshot(snapshot);
restored.restoreSnapshot(shellOnly);
restored.restoreSnapshotKeyed(keyedSnapshot, secretKey);
console.log(restored.executeSync("pwd").stdout); // /workspace\n

const tool = new BashTool({ username: "agent", maxCommands: 5 });
tool.executeSync("export TOOL_STATE=ready");

const hmacKey = new TextEncoder().encode(process.env.SNAPSHOT_SECRET!);
const toolSnapshot = tool.snapshot({ hmacKey });
const toolShellOnly = tool.snapshot({ excludeFilesystem: true, hmacKey });
const restoredTool = BashTool.fromSnapshot(
  toolSnapshot,
  {
    username: "agent",
    maxCommands: 5,
  },
  { hmacKey },
);

console.log(restoredTool.executeSync("echo $TOOL_STATE").stdout); // ready\n
restoredTool.restoreSnapshot(toolShellOnly, { hmacKey });

Network

Outbound HTTP (curl / wget) is disabled by default. Enable it with the network option, which requires either an explicit URL allowlist or allowAll: true:

const bash = new Bash({
  network: {
    allow: ["https://api.example.com/**"],
    blockPrivateIps: true, // default
  },
});

await bash.execute("curl https://api.example.com/users"); // allowed
await bash.execute("curl https://evil.example.org"); // denied: not in allowlist

Credential Injection

Attach credentials to matching requests without exposing them to scripts:

const bash = new Bash({
  network: {
    allow: ["https://api.example.com/**"],
    // Direct injection — the script never sees the secret.
    credentials: [
      {
        pattern: "https://api.example.com/**",
        kind: "bearer",
        token: process.env.API_TOKEN!,
      },
    ],
    // Placeholder mode — the script sees an opaque `bk_placeholder_...`
    // value in $MY_TOKEN; it is swapped for the real secret on the wire.
    credentialPlaceholders: [
      {
        env: "MY_TOKEN",
        pattern: "https://api.example.com/**",
        kind: "header",
        name: "X-Api-Key",
        value: process.env.API_KEY!,
      },
    ],
  },
});

Credential kinds: "bearer" (requires token), "header" (requires name + value), "headers" (requires headers: Array<{ name, value }>).

Framework Integrations

OpenAI

import { bashTool } from "@everruns/bashkit/openai";

const bash = bashTool();

Anthropic

import { bashTool } from "@everruns/bashkit/anthropic";

const bash = bashTool();

Vercel AI SDK

import { bashTool } from "@everruns/bashkit/ai";

const bash = bashTool();

LangChain

import {
  createBashTool,
  createScriptedTool,
} from "@everruns/bashkit/langchain";

API Reference

Bash

  • new Bash(options?)
  • Bash.create(options?)
  • executeSync(commands, options?)
  • execute(commands, options?)
  • executeSyncOrThrow(commands, options?)
  • executeOrThrow(commands, options?)
  • cancel()
  • clearCancel()
  • reset()
  • addBuiltin(name, callback) / removeBuiltin(name), register/unregister persistent JS builtins
  • snapshot(options?) / snapshotKeyed(key, options?)
  • restoreSnapshot(data, options?) / restoreSnapshotKeyed(data, key)
  • Bash.fromSnapshot(data, options?) / Bash.fromSnapshotKeyed(data, key)
  • Direct VFS helpers: readFile, writeFile, appendFile, mkdir, remove, exists, stat, readDir, ls, glob, mount, unmount, fs
  • shellState(), lightweight inspection snapshot (variables, env, cwd, arrays, aliases, traps)
  • analyze(script), static, pre-execution introspection (see Script Analysis)

BashTool

  • All execution, cancellation (cancel(), clearCancel()), reset, custom builtins, snapshot, restore, and direct VFS helpers from Bash
  • Tool metadata: name, version, shortDescription
  • snapshot({ hmacKey, ...options })
  • restoreSnapshot(data, { hmacKey })
  • BashTool.fromSnapshot(data, options?, { hmacKey })
  • description()
  • help()
  • systemPrompt()
  • inputSchema()
  • outputSchema()

ScriptedTool

  • new ScriptedTool(options)
  • addTool(name, description, callback, schema?)
  • executeSync(script)
  • execute(script)
  • executeSyncOrThrow(script)
  • executeOrThrow(script)
  • env(key, value)
  • toolCount()

BashOptions

  • username?: string

  • hostname?: string

  • cwd?: string, initial working directory (avoids a leading cd)

  • env?: Record<string, string>, initial environment variables (avoids an export prelude)

  • maxCommands?: number

  • maxLoopIterations?: number

  • maxMemory?: number

  • maxInputBytes?: number

  • timeoutMs?: number

  • files?: Record<string, string | (() => string) | (() => Promise<string>)>

  • mounts?: Array<{ path: string; root: string; writable?: boolean }>

  • python?: boolean

  • externalFunctions?: string[], Python host function names

  • externalHandler?: ExternalHandler, sync/async Python dispatcher (name, args, kwargs, { signal })

  • typescript?: boolean, opt into embedded TypeScript (ts/node/deno/bun)

  • typescriptExternalFunctions?: string[]

  • typescriptExternalHandler?: TypeScriptExternalHandler, dispatcher (name, args, { signal })

  • customBuiltins?: Record<string, (ctx: BuiltinContext) => string | Promise<string>>, JS callbacks registered as bash builtins (see Custom Builtins)

  • network?: NetworkOptions, outbound HTTP configuration (see Network)

Host calls require asynchronous execute(). See the host function guide for JSON values, caller context, cancellation and reentry rules.

ScriptAnalysis

Returned by analyze(script).

  • commands: AnalyzedCommand[], every simple command, in source order
  • redirects: AnalyzedRedirect[], { path: string | null, mode: "read" | "write" | "append", isWrite: boolean }
  • functions: string[], function names the script defines
  • commandNames: string[], distinct statically known names, first-seen order
  • hasDynamicCommands: boolean / hasInterpreterReentry: boolean / hasCommandSubstitution: boolean / truncated: boolean
  • isOpaque: boolean, the script hides work; allowlist checks must treat this as "ask"

AnalyzedCommand: { name: string | null, args: Array<string | null>, context: "direct" | "substitution" | "function_body", assignments: string[], isAssignmentOnly: boolean }

BuiltinContext

  • name: string, command name as invoked
  • argv: string[], arguments (not including the command name)
  • stdin: string | null, piped input, null if no pipe
  • env: Record<string, string>, exported environment variables
  • cwd: string, current working directory
  • fs: FileSystem, live handle to the instance's virtual filesystem

ExecuteOptions

  • signal?: AbortSignal
  • onOutput?: (chunk: { stdout: string; stderr: string }) => void

ExecResult and BashError

  • ExecResult.stdout
  • ExecResult.stderr
  • ExecResult.exitCode
  • ExecResult.error
  • ExecResult.success
  • ExecResult.stdoutTruncated
  • ExecResult.stderrTruncated
  • BashError.exitCode
  • BashError.stderr

Platform Support

OSArchitecture
macOSx86_64, aarch64
Linuxx86_64, aarch64
Windowsx86_64
WASMwasm32-wasip1-threads

How It Works

The JavaScript package wraps the Rust bashkit interpreter through NAPI-RS bindings. Commands execute in-process against a virtual filesystem, with the Rust core enforcing parsing, execution, and resource limits while the JS wrapper exposes a TypeScript-friendly API and framework adapters.

Part of Everruns

Bashkit is part of the Everruns ecosystem. See the bashkit monorepo for the Rust core, the Python package (bashkit), and related tooling.

License

MIT

版本与发布记录

@everruns/bashkit 共发布 42 个版本,首次发布于 2026/03/15;过去 12 个月发布了 42 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.21.0latest2026/11/113 天前
0.20.32026/11/059 天前
0.20.22026/10/3114 天前
0.20.12026/10/3015 天前
0.19.02026/10/1629 天前
0.18.22026/09/231 个月前

其他发布通道:next → 0.17.0。

关于 @everruns/bashkit 的常见问题

@everruns/bashkit 是什么?

Sandboxed bash interpreter for JavaScript/TypeScript

如何安装 @everruns/bashkit?

在项目目录运行 npm install @everruns/bashkit,或使用 pnpm add / yarn add / bun add。

@everruns/bashkit 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@everruns/bashkit 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@everruns/bashkit 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >= 18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@everruns/bashkit 可以商用吗?

@everruns/bashkit 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@everruns/bashkit 的下载量有多少?

根据 npm 官方下载统计,@everruns/bashkit 在 2026/10/14 – 2026/11/12 这 30 天内被下载 3,547 次,日均约 118 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@everruns/bashkit 还在维护吗?

最新版本 0.21.0 发布于 2026/11/11(3 天前);至今共发布 42 个版本,其中过去 12 个月发布了 42 个;首次发布于 2026/03/15。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。