JustBash

A simulated bash environment with an in-memory virtual filesystem, written in Elixir.

Designed for AI agents that need a secure, sandboxed bash environment.

Supports optional network access via curl and wget with HTTPS-only enforcement and host allowlists.

Note: This is an Elixir port of just-bash by Vercel. The entire codebase was generated through conversational prompting with Claude Opus 4.5 via OpenCode.

Security Model

JustBash treats shell code as untrusted and sandboxes it in memory. Custom commands passed via :commands are trusted host-side extensions supplied by the library caller, and JustBash does not sandbox them or provide safety guarantees for them.

Installation

def deps do
  [{:just_bash, "~> 0.1.0"}]
end

Usage

Basic API

bash = JustBash.new()
{_result, bash} = JustBash.exec(bash, ~s(echo "Hello" > greeting.txt))
{result, _bash} = JustBash.exec(bash, "cat greeting.txt")
result.stdout  #=> "Hello\n"
result.exit_code  #=> 0

Configuration

bash = JustBash.new(
  files: %{"/data/file.txt" => "content"},  # Initial files
  env: %{"MY_VAR" => "value"},              # Environment variables
  cwd: "/app"                                # Starting directory
)

Network Access

Network access is disabled by default. When enabled, only HTTPS is permitted and an explicit allowlist is required:

# Allow specific hosts (HTTPS only)
bash = JustBash.new(
  network: %{
    enabled: true,
    allow_list: ["api.github.com", "*.example.com"]
  }
)

# Allow all hosts
bash = JustBash.new(
  network: %{enabled: true, allow_list: :all}
)

# Also allow plain HTTP (not recommended)
bash = JustBash.new(
  network: %{enabled: true, allow_list: :all, allow_insecure: true}
)

# Custom HTTP client for testing
bash = JustBash.new(
  network: %{enabled: true, allow_list: :all},
  http_client: MyMockHttpClient
)

Custom Commands

Custom commands are trusted extensions supplied by the library caller, not untrusted shell input. JustBash does not sandbox them and does not provide safety guarantees for them.

Register trusted host-side commands with commands::

defmodule MyApp.Commands.Greet do
  @behaviour JustBash.Commands.Command

  @impl true
  def names, do: ["greet", "hello"]

  @impl true
  def execute(bash, args, _stdin) do
    name = Enum.join(args, " ")
    {%{stdout: "Hello, #{name}!\n", stderr: "", exit_code: 0}, bash}
  end
end

bash = JustBash.new(commands: %{"greet" => MyApp.Commands.Greet})
{result, _bash} = JustBash.exec(bash, "hello world")
result.stdout  #=> "Hello, world!\n"

Important caveats:

Execute Script Files

# Run a script from the virtual filesystem
bash = JustBash.new(files: %{"/script.sh" => "echo hello"})
{result, bash} = JustBash.exec_file(bash, "/script.sh")

Sigil

import JustBash.Sigil

result = ~b"echo hello"
result.stdout  #=> "hello\n"

# Modifiers
~b"echo hello"t  # trimmed output
~b"echo hello"s  # stdout only
~b"exit 42"e     # exit code

Supported Commands

File Operations

cat, chmod, chown, cp, du, file, find, ln, ls, mkdir, mktemp, mv, readlink, realpath, rm, stat, touch, tree

Text Processing

awk, base64, comm, cut, diff, expand, fold, grep, head, md5sum, nl, paste, rev, sed, sha256sum, shasum, sort, tac, tail, tr, uniq, wc, xargs

Data Processing

jq (JSON), markdown (Markdown → HTML)

Network

curl, wget

Shell Builtins

echo, printf, cd, pwd, eval, export, unset, set, test, [, [[, true, false, :, command, source, ., read, exit, return, local, declare, typeset, break, continue, shift, getopts, trap, type

Utilities

arch, basename, date, dirname, env, hostname, id, nproc, printenv, seq, sleep, tee, uname, which, whoami, yes

Shell Features

Default Layout

When created without options, JustBash provides a Unix-like directory structure:

API Reference

# Create environment
bash = JustBash.new(opts)

# Execute command
{result, bash} = JustBash.exec(bash, "command")
result.stdout      # String
result.stderr      # String
result.exit_code   # Integer
result.env         # Updated environment

# Execute script from virtual filesystem
{result, bash} = JustBash.exec_file(bash, "/path/to/script.sh")

# Parse without executing
{:ok, ast} = JustBash.parse("echo hello")

# Format script
{:ok, formatted} = JustBash.format("if true;then echo yes;fi")

Development

mix deps.get
mix test           # Unit, integration, property-based, and bash-comparison tests
mix dialyzer       # Type checking
mix credo --strict # Linting

License

MIT