Skip to main content
The Mesa SDK integrates with Vercel’s just-bash library. This means your agent can execute regular bash commands — using tools like ls, cat, cp, grep, and more — directly against Mesa repositories without cloning or mounting anything, or even needing a sandbox. Both read and write operations are supported. The integration lives in the @mesadev/sdk package. You create a Mesa filesystem, call .bash() to get a bash executor, and start executing commands.

Quick Start

Prerequisites

  • A Mesa account. If you haven’t signed up yet, you can do so here.
  • An API key with admin scope. You can create one at https://app.mesa.dev/<your-org>/tokens.
  • A repository to access. You can create one at https://app.mesa.dev/<your-org>/repositories.
Running in Docker with a slim base image? See Running in Docker for required system packages.

Install the SDK

npm install @mesadev/sdk

Create a Mesa Filesystem

Initialize a Mesa client and a filesystem handle scoped to the repos you want to access. You can create as many filesystem handles as you need.
import { Mesa } from "@mesadev/sdk";

const mesa = new Mesa({ apiKey: process.env.MESA_API_KEY });

// Create a Mesa filesystem scoped to your repos
const fs = await mesa.fs.create({
  repos: [{ name: "my-repo", bookmark: "main" }],
  mode: "rw", // or "ro" for read-only access
});

// Get a bash shell backed by Mesa
const bash = fs.bash();

// Run commands
const result = await bash.exec("ls /my-org/my-repo/src");
console.log(result.stdout);

Managing Changes and Bookmarks

Beyond file operations, the Mesa filesystem exposes APIs to create/switch changes and manage bookmarks (analogous to branches in git) directly from TypeScript.
// Create a new change from a bookmark and switch to it
const created = await fs.change.new({
  repo: "my-repo",
  bookmark: "main",
});

// Switch to an existing change (does not create a new one)
await fs.change.edit({
  repo: "my-repo",
  changeId: created.changeOid,
});

// Create a bookmark at the current commit
await fs.bookmark.create({
  repo: "my-repo",
  name: "feature/agent-output",
});

// List bookmark names
const bookmarks = await fs.bookmark.list({ repo: "my-repo" });
console.log(bookmarks);
fs.change.new(...) always creates a new change. fs.change.edit(...) never creates a new change, it only switches to an existing one.

How It Works

When you run a command like ls /my-repo/src, just-bash calls into Mesa which fetches the directory listing from Mesa’s API. Writes work the same way — file changes are persisted back to Mesa automatically. Our just-bash implementation takes advantage of the same caching and prefetching mechanisms as Mesa FUSE, which means you get the same performance benefits without the need for a sandbox.

When to use Mesa just-bash

If you have an existing TypeScript-defined agent that runs in your multi-tenant backend, Mesa just-bash enables you to get the full benefits of a filesystem without needing a sandbox. For example, agents written using Vercel’s AI SDK, Mastra, or Langchain can now manipulate files using the full power of bash and common Unix tools, rather than just basic readFile/writeFile tools.

When not to use just-bash

If your agent needs to install dependencies or run arbitrary code, you should instead mount Mesa FUSE in a sandboxed environment like an ECS container or lightweight VM from a provider like Daytona, Vercel, or Modal. Just-bash does allow you to use sqlite databases and execute small scripts in Python and JavaScript, but it is not a general-purpose VM.

Integrating with Agents

Mesa just-bash is designed to be used with any agent framework that supports tool calling. By wrapping the bash execution function in a tool, you can use it in your agent’s workflow. For end-to-end examples, see the Mesa examples repo.

Vercel AI SDK

To define a tool in the Vercel AI SDK, you can use the tool function.
import { tool } from 'ai';
import { z } from 'zod';

const bashTool = tool({
  description: 'Execute bash commands',
  inputSchema: z.object({
    command: z.string(),
  }),
  execute: async ({ command }) => {
    return await bash.exec(command);
  },
});

Langchain

To define a tool in Langchain, you can use the tool function.

import { tool } from 'langchain'
import { z } from 'zod';

const bashTool = tool(
  ({ command }) => bash.exec(command),
  {
    name: 'bash',
    description: 'Execute bash commands',
    schema: z.object({
      command: z.string(),
    }),
  }
);

Mastra

To define a tool in Mastra, you can use the createTool function.
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'

const bashTool = createTool({
  id: 'bash-tool',
  description: 'Execute bash commands',
  inputSchema: z.object({
    command: z.string(),
  }),
  outputSchema: z.object({
    stdout: z.string(),
    stderr: z.string(),
    exitCode: z.number(),
  }),
  execute: async ({ command }) => {
    return await bash.exec(command);
  },
});

Configuring the Shell

The .bash() method accepts options to configure the shell environment. Mesa’s Bash implementation is is a thin wrapper over the underlying just-bash library, so all of the exposed options are pure passthrough to the underlying just-bash Bash instance. We omit some options that are not relevant to a typical Mesa bash() setup. For more details, you can refer to the just-bash documentation.
Options like fs and files from BashOptions are intentionally omitted — the filesystem is always the MesaFileSystem instance, and files are populated from your Mesa repositories.
If you need to use advanced just-bash features such as overlay filesystems, you should separately install the just-bash package and use the MesaFileSystem instance directly to construct a Bash instance.
import { Mesa } from "@mesadev/sdk";
import { Bash } from "just-bash";

const mesa = new Mesa();
const fs = await mesa.fs.create({...});
const bash = new Bash({ fs, ...otherOptions });
For convenience, here is a summary of the options exposed by Mesa’s Bash instance.

cwd (Current Working Directory)

The cwd option sets the starting directory for the shell. The default is /home/user, and Mesa mounts your repos at /<org>/<repo>. In most cases, you will want to set cwd to either /<org>/<repo> or, for multi-repo filesystems, /<org>.
const bash = fs.bash({
  cwd: "/my-org/my-repo",
});

// Now commands run relative to /my-org/my-repo
await bash.exec("ls src");

env (Environment Variables)

The env option sets the environment variables available to commands. For compatibility, the defaults simulate a typical Linux/GNU environment, ex. PATH, HOME, PWD, and OLDPWD.
const bash = fs.bash({
  env: { NODE_ENV: "production" },
});

// Now commands run with NODE_ENV=production
await bash.exec("echo $NODE_ENV");
// Output: production

executionLimits (Resource Limits)

The executionLimits option sets iteration limits for commands, which protects against infinite loops and deep recursion. All of these limits are optional and have reasonable defaults. You can override them to suit your needs.
const bash = fs.bash({
  executionLimits: {
    maxCallDepth: 100, // Max function recursion depth
    maxCommandCount: 10000, // Max total commands executed
    maxLoopIterations: 10000, // Max iterations per loop
    maxAwkIterations: 10000, // Max iterations in awk programs
    maxSedIterations: 10000, // Max iterations in sed scripts
  },
});

fetch (Custom Fetch)

The fetch option allows you to use a custom fetch implementation for network access. This is useful if you want to use a different HTTP client or proxy.
const bash = fs.bash({
  fetch: customFetch,
});

network (Network Access)

Network access is disabled by default. You can enable it with the network option.
// Allow specific URLs with additional methods
const bash = fs.bash({
  network: {
    allowedUrlPrefixes: ["https://api.example.com"],
    allowedMethods: ["GET", "HEAD", "POST"], // Default: ["GET", "HEAD"]
  },
});

// Inject credentials via header transforms (secrets never enter the sandbox)
const bash = fs.bash({
  network: {
    allowedUrlPrefixes: [
      "https://public-api.com", // plain string — no transforms
      {
        url: "https://ai-gateway.vercel.sh",
        transform: [{ headers: { Authorization: "Bearer secret" } }],
      },
    ],
  },
});

// Allow all URLs and methods (use with caution)
const bash = fs.bash({
  network: { dangerouslyAllowFullInternetAccess: true },
});

python (Python Runtime)

Python (CPython compiled to WASM) is opt-in due to additional security surface. Enable with python: true:
const bash = fs.bash({
  python: true,
});

// Execute Python code
await bash.exec('python3 -c "print(1 + 2)"');

// Run Python scripts
await bash.exec('python3 script.py');

javascript (JavaScript Runtime)

JavaScript and TypeScript execution via QuickJS is opt-in due to additional security surface. Enable with javascript: true:
const bash = fs.bash({
  javascript: true,
});

// Execute JavaScript code
await bash.exec('js-exec -c "console.log(1 + 2)"');

// Run script files (.js, .mjs, .ts, .mts)
await bash.exec('js-exec script.js');

// ES module mode with imports
await bash.exec('js-exec -m -c "import fs from \'fs\'; console.log(fs.readFileSync(\'/data/file.txt\', \'utf8\'))"');

commands (Built-in Commands)

The commands option allows you restrict the built-in commands that are available. This is useful if you want to limit what your agents can do. By default, all 90+ built-in commands are enabled.
const bash = fs.bash({
  commands: ["ls", "cd", "pwd"], // Enable only the specified commands.
});

customCommands (User-Defined Commands)

The customCommands option allows you to provide your own custom commands to the shell.
import { defineCommand } from "just-bash";

const hello = defineCommand("hello", async (args, ctx) => {
  const name = args[0] || "world";
  return { stdout: `Hello, ${name}!\n`, stderr: "", exitCode: 0 };
});

const upper = defineCommand("upper", async (args, ctx) => {
  return { stdout: ctx.stdin.toUpperCase(), stderr: "", exitCode: 0 };
});

const bash = fs.bash({ customCommands: [hello, upper] });
const result = await bash.exec("upper \"Mesa is great!\"");
console.log(result.stdout);
// Output: MESA IS GREAT!

logger (Logging Hooks)

The logger option allows you to hook into the internal logging of the shell. This is useful if you want to trace the execution of commands for debugging or monitoring.
const bash = fs.bash({   
  logger: {  
    info(message, data) {  
      console.log(`[INFO] ${message}:`, data);  
    },  
    debug(message, data) {  
      console.log(`[DEBUG] ${message}:`, data);  
    }  
  }  
});

await bash.exec("echo hello");  
// Logs:  
// [INFO] exec: { command: "echo hello" }  
// [DEBUG] stdout: { output: "hello\n" }  
// [INFO] exit: { exitCode: 0 }

Available options

OptionDescription
envEnvironment variables available to commands
cwdStarting directory for command execution
executionLimitsIteration limits for commands
fetchCustom fetch implementation for network access
networkEnable network access
pythonEnable Python runtime
javascriptEnable JavaScript runtime
commandsWhich built-in commands to enable
customCommandsUser-defined custom commands
loggerLogging interface

Direct Filesystem Operations

You can also use the Mesa filesystem directly without going through bash. The filesystem implements the full just-bash IFileSystem interface:
// Read a file
const content = await fs.readFile("/my-repo/src/index.ts");

// Write a file
await fs.writeFile("/my-repo/src/new-file.ts", "export const x = 1;");

// Check if a file exists
const exists = await fs.exists("/my-repo/package.json");

// List a directory
const entries = await fs.readdir("/my-repo/src");

// Copy files
await fs.cp("/my-repo/src/a.ts", "/my-repo/src/b.ts");

// Remove files
await fs.rm("/my-repo/src/old-file.ts");
The filesystem supports symlinks, permissions (chmod), timestamps (utimes), and recursive directory operations. Hard links are not supported and will return ENOTSUP.

Caching

For better performance on repeated reads, configure a disk cache:
const fs = await mesa.fs.create({
  repos: [{ name: "my-repo", bookmark: "main" }],
  mode: "rw",
  cache: {
    diskCache: {
      path: `/tmp/mesa-cache/my-org/my-repo`, // Provide a unique path for each filesystem to avoid conflicts.
      maxSizeBytes: 1024 * 1024 * 1024, // 1 GB
    },
  },
});

Limitations

  • By default, just-bash does not support installing dependencies (ex. running npm install) or executing arbitrary code (although you can enable Python and JavaScript execution via the python and javascript options).
  • Mesa just-bash is not currently supported in the browser or browser-like runtimes like Cloudflare Workers or Vercel Edge Functions. NOTE: this limitation is due to the use of NAPI — we will likely support a browser-runtime version in the future via a WASM build.
  • Bun support is experimental and may not work in all cases. This is due to Bun’s incomplete support for NAPI.