
# AGENTS.md - just-bash

Instructions for AI agents using just-bash in projects.

## What is just-bash?

A sandboxed bash interpreter with an in-memory virtual filesystem. Use it when you need to:

- Execute shell commands without real filesystem access
- Run untrusted scripts safely
- Process text with standard Unix tools (grep, sed, awk, jq, etc.)

## For AI Agents

If you're building an AI agent that needs a bash tool, use [`bash-tool`](https://github.com/vercel-labs/bash-tool) which is optimized for just-bash:

```sh
npm install bash-tool
```

```typescript
import { createBashTool } from "bash-tool";
import { generateText } from "ai";

const bashTool = createBashTool({
  files: { "/data/users.json": '[{"name": "Alice"}, {"name": "Bob"}]' },
});

const result = await generateText({
  model: "anthropic/claude-sonnet-4",
  tools: { bash: bashTool },
  prompt: "Count the users in /data/users.json",
});
```

See the [bash-tool documentation](https://github.com/vercel-labs/bash-tool) for more details.

## Quick Reference

```typescript
import { Bash } from "just-bash";

const bash = new Bash({
  files: { "/data/input.txt": "content" }, // Initial files
  cwd: "/data", // Working directory
});

const result = await bash.exec("cat input.txt | grep pattern");
// result.stdout  - command output
// result.stderr  - error output
// result.exitCode - 0 = success, non-zero = failure
```

## Key Behaviors

1. **Isolation**: Each `exec()` call is isolated. Environment variables, functions, and cwd changes don't persist between calls. Only filesystem changes persist.

2. **No real filesystem**: By default, commands only see the virtual filesystem. Use `OverlayFs` to read from a real directory (writes stay in memory).

3. **No network by default**: `curl` doesn't exist unless you configure `network` options with URL allowlists.

4. **No binaries/WASM**: Only built-in commands work. You cannot run node, python, or other binaries.

5. **ReadWriteFs root separation**: If you use `ReadWriteFs`, point it at a workspace directory, not at the installed `just-bash` package or other trusted runtime code.

6. **UTC by default**: `date` always shows UTC (`%Z=UTC`, `%z=+0000`) unless the host opts in by passing `TZ` as an env var (e.g. `new Bash({ env: { TZ: "America/New_York" } })`). `-u` always forces UTC; an invalid `$TZ` falls back to UTC.

## Available Commands

**Text processing**: `awk`, `cat`, `column`, `comm`, `cut`, `egrep`, `expand`, `fgrep`, `fold`, `grep`, `head`, `join`, `nl`, `paste`, `rev`, `rg`, `sed`, `sort`, `strings`, `tac`, `tail`, `tr`, `unexpand`, `uniq`, `wc`, `xargs`

**Data processing**: `jq` (JSON), `js-exec` (JavaScript/TypeScript via QuickJS), `python3`/`python` (Python via WASM/CPython), `sqlite3` (SQLite), `xan` (CSV), `yq` (YAML/XML/TOML/CSV)

**File operations**: `basename`, `chmod`, `cp`, `dirname`, `du`, `file`, `find`, `ln`, `ls`, `mkdir`, `mktemp`, `mv`, `od`, `pwd`, `readlink`, `rm`, `rmdir`, `split`, `stat`, `touch`, `tree`

**Utilities**: `alias`, `base64`, `bash`, `clear`, `curl`, `date`, `diff`, `echo`, `env`, `expr`, `false`, `gzip`, `gunzip`, `help`, `history`, `hostname`, `html-to-markdown`, `md5sum`, `printenv`, `printf`, `seq`, `sh`, `sha1sum`, `sha256sum`, `sleep`, `tar`, `tee`, `time`, `timeout`, `true`, `unalias`, `which`, `whoami`, `yes`, `zcat`

All commands support `--help` for usage details.

## Tools by File Format

### JSON - `jq`

```bash
# Extract field
jq '.name' data.json

# Filter array
jq '.users[] | select(.active == true)' data.json

# Transform structure
jq '[.items[] | {id, name}]' data.json

# From stdin
echo '{"x":1}' | jq '.x'
```

### YAML - `yq`

```bash
# Extract value
yq '.config.database.host' config.yaml

# Output as JSON
yq -o json '.' config.yaml

# Filter with jq syntax
yq '.users[] | select(.role == "admin")' users.yaml

# Modify file in-place
yq -i '.version = "2.0"' config.yaml
```

### TOML - `yq`

```bash
# Read TOML (auto-detected from .toml extension)
yq '.package.name' Cargo.toml
yq '.tool.poetry.version' pyproject.toml

# Convert TOML to JSON
yq -o json '.' config.toml

# Convert YAML to TOML
yq -o toml '.' config.yaml
```

### CSV/TSV - `yq -p csv`

```bash
# Read CSV (auto-detects from .csv/.tsv extension)
yq '.[0].name' data.csv
yq '.[0].name' data.tsv

# Filter rows
yq '[.[] | select(.status == "active")]' data.csv

# Convert CSV to JSON
yq -o json '.' data.csv
```

### Front-matter - `yq --front-matter`

```bash
# Extract YAML front-matter from markdown
yq --front-matter '.title' post.md
yq -f '.tags[]' blog-post.md

# Works with TOML front-matter (+++) too
yq -f '.date' hugo-post.md
```

### XML - `yq -p xml`

```bash
# Extract element
yq '.root.users.user[0].name' data.xml

# Access attributes (use +@ prefix)
yq '.root.item["+@id"]' data.xml

# Convert XML to JSON
yq -p xml -o json '.' data.xml
```

### INI - `yq -p ini`

```bash
# Read INI section value
yq '.database.host' config.ini

# Convert INI to JSON
yq -p ini -o json '.' config.ini
```

### HTML - `html-to-markdown`

```bash
# Convert HTML to markdown
html-to-markdown page.html

# From stdin
echo '<h1>Title</h1><p>Text</p>' | html-to-markdown
```

### Format Conversion with yq

```bash
# JSON to YAML
yq -p json '.' data.json

# YAML to JSON
yq -o json '.' data.yaml

# YAML to TOML
yq -o toml '.' config.yaml

# TOML to JSON
yq -o json '.' Cargo.toml

# CSV to JSON
yq -p csv -o json '.' data.csv

# XML to YAML
yq -p xml '.' data.xml
```

## Common Patterns

### Process JSON with jq

```bash
cat data.json | jq '.items[] | select(.active) | .name'
```

### Find and process files

```bash
find . -name "*.ts" -type f | xargs grep -l "TODO"
```

### Text transformation pipeline

```bash
cat input.txt | grep -v "^#" | sort | uniq -c | sort -rn | head -10
```

### AWK for columnar data

```bash
cat data.csv | awk -F',' '{sum += $3} END {print sum}'
```

## Limitations

- **32-bit integers only**: Arithmetic operations use 32-bit signed integers
- **No job control**: No `&`, `bg`, `fg`, or process suspension
- **No external binaries**: Only built-in commands are available
- **Execution limits**: Loops, recursion, command counts, and output sizes have configurable limits to prevent runaway execution (exit code 126 when exceeded)

## Error Handling

Always check `exitCode`:

```typescript
import { Bash } from "just-bash";

const bash = new Bash({ files: { "/file.txt": "some content" } });
const result = await bash.exec("grep pattern file.txt");
if (result.exitCode !== 0) {
  // Command failed - check result.stderr for details
}
```

Common exit codes:

- `0` - Success
- `1` - General error or no matches (grep)
- `2` - Misuse of command (invalid options)
- `126` - Execution limit exceeded (loops, output size, string length)
- `127` - Command not found

## Debugging Tips

1. **Check stderr**: Error messages go to `result.stderr`
2. **Use --help**: All commands support `--help` for usage
3. **Test incrementally**: Build pipelines step by step
4. **Quote variables**: Use `"$var"` to handle spaces in values

## Security Model

- Virtual filesystem is isolated from the real system
- Network access requires explicit URL allowlists
- Execution limits prevent infinite loops and resource exhaustion
- No shell injection possible (commands are parsed, not eval'd)

### Execution Limits

All limits are configurable via `executionLimits` in `BashOptions`:

```typescript
const bash = new Bash({
  executionLimits: {
    maxCommandCount: 10000,      // Max commands per exec()
    maxLoopIterations: 10000,    // Max bash loop iterations
    maxCallDepth: 100,           // Max function recursion depth
    maxStringLength: 10485760,   // Max string size (10MB)
    maxArrayElements: 100000,    // Max array elements
    maxGlobOperations: 100000,   // Max glob filesystem operations
    maxAwkIterations: 10000,     // Max AWK loop iterations
    maxSedIterations: 10000,     // Max sed branch loop iterations
    maxJqIterations: 10000,      // Max jq loop iterations
    maxSubstitutionDepth: 50,    // Max $() nesting depth
    maxHeredocSize: 10485760,    // Max heredoc size (10MB)
  },
});
```

**Output size limits**: AWK, sed, jq, and printf enforce `maxStringLength` on their output buffers. Commands that exceed the limit exit with code 126.

**File read size limits**: `OverlayFs` and `ReadWriteFs` default to a 10MB max file read size. Override with `maxFileReadSize` in filesystem options (set to `0` to disable).

**Network response size**: `maxResponseSize` in `NetworkConfig` caps HTTP response bodies (default: 10MB).

## Discovering Types

TypeScript types are available in the `.d.ts` files. Use JSDoc-style exploration to understand the API:

```bash
# Find all type definition files
find node_modules/just-bash/dist -name "*.d.ts" | head -20

# View main exports and their types
cat node_modules/just-bash/dist/index.d.ts

# View Bash class options
grep -A 30 "interface BashOptions" node_modules/just-bash/dist/Bash.d.ts

# Search for specific types
grep -r "interface.*Options" node_modules/just-bash/dist/*.d.ts
```

Key types to explore:
- `BashOptions` - Constructor options for `new Bash()`
- `ExecResult` - Return type of `bash.exec()`
- `InitialFiles` - File specification format

## Bundling just-bash

just-bash ships pre-bundled (`dist/bundle/index.js`, `index.cjs`), but a handful
of dependencies are deliberately left **external** — the bundle imports them by
name instead of inlining them. Under plain Node this is invisible: they are
declared dependencies, so the package manager installs them and the imports
resolve at runtime.

It matters when you run just-bash through **another** bundler (Next.js,
webpack, esbuild, rollup) for a serverless or edge target. That bundler will try
to inline these, and some of them cannot be inlined. Mark all six as external:

| Package | What it is |
| --- | --- |
| `@mongodb-js/zstd` | native binding (`optionalDependencies`) |
| `node-liblzma` | native binding (`optionalDependencies`) |
| `sql.js` | ships a `.wasm` asset |
| `run` | embeds the QuickJS WASM runtime |
| `seek-bzip` | CommonJS-only bzip2 decoder |
| `guarded-fetch` | imports `node:dns/promises`, `node:net`, `node:dns` |

`guarded-fetch` is the one that bites bundlers targeting a non-Node context:
inlining it pulls in Node builtins, and a bundler whose chunking context has no
concept of them fails the build (Turbopack reports `the chunking context
(unknown) does not support external modules (request: node:dns/promises)`).

Next.js 15+:

```ts
// next.config.ts
const nextConfig = {
  serverExternalPackages: [
    "just-bash",
    "@mongodb-js/zstd",
    "node-liblzma",
    "seek-bzip",
    "sql.js",
    "run",
    "guarded-fetch",
  ],
};
```

Older Next.js uses `experimental.serverComponentsExternalPackages`. webpack:
add them to `externals` (or use `webpack-node-externals`). esbuild/rollup:
`--external:<name>` / `external: [...]`.

**Node version**: just-bash requires Node `>=20.19` (`guarded-fetch`'s floor).

**Optional dependencies**: `@mongodb-js/zstd` and `node-liblzma` are declared
in `optionalDependencies`, so an install that cannot build them still succeeds.
Only the compression they provide is lost: `tar -J` then exits non-zero with
`xz compression requires node-liblzma which failed to load`, rather than
crashing the interpreter. The other four are regular dependencies and are
required.
