App mounts let your agent or backend work with Mesa repositories without needing cloning, FUSE, or a sandbox.
MesaFS app mount is available in both the TypeScript and Python SDKs. There are two main ways of interacting the with app mount:
- Basic Filesystem API: call methods like
readFile and writeFile to directly access files
- Emulated Bash: Execute shell commands using tools like
ls, cp, grep. Runs against your Mesa repositories, entirely in-process.
Both read and write operations are supported.
MesaFS can be run as either a POSIX mount or app mount. If you aren’t sure which to use, see Filesystem for an overview and
comparison.
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 Docker
for required system packages.
Install the 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 });
// Mount a Mesa filesystem scoped to your repos
const fs = await mesa.fs.mount({
repos: [{ name: "my-repo", bookmark: "main" }],
});
Run Bash commands or directly read/write files
const fs = await mesa.fs.mount(...);
// Get a bash shell backed by Mesa
const bash = fs.bash();
// Run shell commands
const result = await bash.exec("ls /my-org/my-repo/src");
console.log(result.stdout);
// OR directly read/write
await fs.readFile('/my-org/my-repo/README.md')
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
await fs.change.new({
repo: "my-repo",
bookmark: "main",
});
const current = await fs.change.current({ repo: "my-repo" });
// Switch to an existing change (does not create a new one)
await fs.change.edit({
repo: "my-repo",
changeId: current.changeId,
});
// 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
Mesa’s app mount mode uses the same core read, write, caching, and version control logic as the POSIX mount — just running in-process.
To provide an emulated bash shell, Mesa uses just-bash (TypeScript) or Bashkit (Python) with MesaFS as the backing filesystem.
Integrating with Agents
Mesa’s app mount is designed to be used with any agent framework. By providing the app mount’s bash function
as a tool, you can easily bring MesaFS into an 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.mount({ repos: [{ name: "my-repo" }] });
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 authenticatedBash = 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 unrestrictedBash = 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
| Option | Description |
|---|
env | Environment variables available to commands |
cwd | Starting directory for command execution |
executionLimits | Iteration limits for commands |
fetch | Custom fetch implementation for network access |
network | Enable network access |
python | Enable Python runtime |
javascript | Enable JavaScript runtime |
commands | Which built-in commands to enable |
customCommands | User-defined custom commands |
logger | Logging 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-org/my-repo/src/index.ts");
// Write a file
await fs.writeFile("/my-org/my-repo/src/new-file.ts", "export const x = 1;");
// Check if a file exists
const exists = await fs.exists("/my-org/my-repo/package.json");
// List a directory
const entries = await fs.readdir("/my-org/my-repo/src");
// Copy files
await fs.cp("/my-org/my-repo/src/a.ts", "/my-org/my-repo/src/b.ts");
// Remove files
await fs.rm("/my-org/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.mount({
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.