JustBash (JustBash v0.3.0)
View SourceA simulated bash environment with virtual filesystem.
JustBash provides a sandboxed bash-like shell environment for Elixir applications. It's designed for AI agents and other use cases that need secure bash execution.
Basic Usage
bash = JustBash.new()
result = JustBash.exec(bash, "echo 'Hello World'")
IO.puts(result.stdout) # "Hello World\n"Features
- In-memory virtual filesystem
- Core bash commands (echo, cat, ls, etc.)
- Shell features: pipes, redirections, variables
- Control flow: if, for, while, case
- Functions and local variables
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.
- All execution happens in memory
- No access to the real filesystem by default
- No network access by default
- Custom commands are outside the sandbox and can bypass filesystem and network restrictions
- Execution limits to prevent infinite loops
Sigil
Use the ~b sigil for inline bash execution:
import JustBash.Sigil
# Execute and get result map
result = ~b"echo hello"
result.stdout # "hello\n"
# With modifiers
~b"echo hello"t # "hello" (trimmed)
~b"echo hello"s # "hello\n" (stdout only)
~b"exit 42"e # 42 (exit code)
~b"echo hi"x # "hi\n" (raises on non-zero exit)
# With interpolation
name = "world"
~b"echo hello #{name}"t # "hello world"
Summary
Functions
Execute a bash command in the environment.
Execute a bash command, raising on parse errors.
Execute a bash script from a path in the virtual filesystem.
Format a bash script into a consistent, readable format.
Format a bash script, raising on parse errors.
Create a new JustBash environment with default configuration.
Parse a bash script and return the AST.
Returns execution statistics from the most recent exec/2 call.
Tokenize a bash script and return the tokens.
Tokenize a bash command string, raising on error.
Types
@type exec_result() :: %{ stdout: String.t(), stderr: String.t(), exit_code: non_neg_integer(), env: map() }
@type stats() :: %{ steps: non_neg_integer(), output_bytes: non_neg_integer(), max_exec_depth: non_neg_integer() }
Execution statistics from the most recent exec/2 call.
@type t() :: %JustBash{ commands: %{required(String.t()) => module()}, context: map(), cwd: String.t(), databases: map(), env: map(), exit_code: non_neg_integer(), fs: JustBash.Fs.InMemoryFs.t(), functions: map(), http_client: term(), interpreter: JustBash.Interpreter.State.t(), jq_module_paths: [String.t()], last_exit_code: non_neg_integer(), limits: JustBash.Limit.t() | nil, max_call_depth: pos_integer(), max_iterations: pos_integer(), network: network_config(), shell_opts: shell_opts() }
Functions
@spec exec(t(), String.t()) :: {exec_result(), t()}
Execute a bash command in the environment.
Returns a tuple of {result, updated_bash} where result contains stdout, stderr, exit_code, and the final env.
Examples
bash = JustBash.new()
{result, _bash} = JustBash.exec(bash, "echo hello")
result.stdout # "hello\n"
result.exit_code # 0
{result, _bash} = JustBash.exec(bash, "cat nonexistent")
result.stderr # "cat: nonexistent: No such file or directory\n"
result.exit_code # 1
@spec exec!(t(), String.t()) :: {exec_result(), t()}
Execute a bash command, raising on parse errors.
Examples
bash = JustBash.new()
{result, _bash} = JustBash.exec!(bash, "echo hello")
@spec exec_file(t(), String.t()) :: {exec_result(), t()}
Execute a bash script from a path in the virtual filesystem.
Reads the script from the sandbox's virtual filesystem and executes it.
The script must exist in bash.fs — no real filesystem access occurs.
Examples
bash = JustBash.new(files: %{"/script.sh" => "echo hello"})
{result, bash} = JustBash.exec_file(bash, "/script.sh")
@spec format( String.t(), keyword() ) :: {:ok, String.t()} | {:error, JustBash.Parser.ParseError.t()}
Format a bash script into a consistent, readable format.
Parses the input script and outputs it with consistent formatting:
- Consistent indentation for control structures
- Normalized whitespace
- Proper line breaks
Options
:indent- Indentation string (default: " " - two spaces)
Examples
JustBash.format("if true;then echo yes;fi")
# {:ok, "if true; then\n echo yes\nfi"}
JustBash.format("echo hello world")
# {:ok, "echo hello world"}
JustBash.format("for i in 1 2 3;do echo $i;done")
# {:ok, "for i in 1 2 3; do\n echo $i\ndone"}
Format a bash script, raising on parse errors.
Examples
JustBash.format!("echo hello")
# "echo hello"
Create a new JustBash environment with default configuration.
Options
:files- Initial files as a map of path => content:env- Initial environment variables:cwd- Starting working directory (default: "/home/user"):commands- Custom commands as a map of name => module implementingJustBash.Commands.Command. Custom commands are trusted host-side extensions supplied by the library caller. They run arbitrary Elixir code, are not constrained by the virtual filesystem or:networksandbox, and are outside JustBash's safety guarantees. Registration keys must be declared in the module'snames/0, and aliases fromnames/0are registered automatically. Custom commands override regular builtins but are overridden by shell functions. Protected stateful builtins such ascdandexportcannot be overridden. Dispatch order: shell functions > custom commands > builtins.:context- Optional map of caller data for custom commands. Stored on theJustBashstruct ascontextand readable inside any custom command asbash.context. Defaults to%{}. Not used by builtins or the interpreter; only host-defined custom commands should read it.:network- Network configuration map with::enabled- Whether network access is allowed (default: false):allow_list- Allowed hosts/patterns. Use:allto allow all hosts, or a list of hostname patterns (e.g.["api.example.com", "*.github.com"]). Empty list[]blocks all requests. (default: [] = all requests blocked when enabled):allow_insecure- Whether plain HTTP is permitted. When false (default), onlyhttps://URLs are allowed. Scripts cannot override this — it is a caller-level control.
:http_client- Module implementing the HTTP client behaviour (default: uses Req):max_iterations- Maximum iterations forwhile/untilloops before they are forcibly stopped. Prevents runaway loops from untrusted scripts (default: 10_000):max_call_depth- Maximum shell function call depth before recursion is forcibly stopped. Prevents unbounded recursion from consuming all available memory (default: 1_000):limits- Resource limits for production safety. Accepts a preset atom (:default,:strict,:relaxed), a keyword list of overrides, orfalseto disable. Default::default. SeeJustBash.Limitfor available keys.:jq_module_paths- List of virtual filesystem paths to search forjqmodules when usingimport/includedirectives (default: [])
Examples
bash = JustBash.new()
bash = JustBash.new(files: %{"/data/file.txt" => "content"})
bash = JustBash.new(env: %{"MY_VAR" => "value"}, cwd: "/app")
bash = JustBash.new(commands: %{"python" => MyPythonCommand})
bash = JustBash.new(network: %{enabled: true})
bash = JustBash.new(network: %{enabled: true, allow_list: ["api.example.com", "*.github.com"]})
# Custom HTTP client for testing:
bash = JustBash.new(network: %{enabled: true}, http_client: MyTestHttpClient)
# Pass data to custom commands via bash.context:
bash = JustBash.new(context: %{user_id: 42}, commands: %{"my_cmd" => MyCommand})
@spec parse(String.t()) :: {:ok, JustBash.AST.Script.t()} | {:error, JustBash.Parser.ParseError.t()}
Parse a bash script and return the AST.
Useful for debugging or analyzing scripts without executing them.
Examples
{:ok, ast} = JustBash.parse("echo hello")
{:error, error} = JustBash.parse("echo 'unterminated")
Returns execution statistics from the most recent exec/2 call.
Useful for observing computational cost without enforcing limits — for example, as a reward signal in reinforcement learning to prefer simpler programs.
Counters reset at the start of each top-level exec/2 call, so
stats always reflect the most recent execution.
Examples
bash = JustBash.new()
{_result, bash} = JustBash.exec(bash, "for i in 1 2 3; do echo $i; done")
JustBash.stats(bash)
#=> %{steps: 12, output_bytes: 6, max_exec_depth: 1}
@spec tokenize(String.t()) :: {:ok, [JustBash.Parser.Lexer.Token.t()]} | {:error, JustBash.Parser.Lexer.Error.t()}
Tokenize a bash script and return the tokens.
Useful for debugging the lexer.
Examples
{:ok, tokens} = JustBash.tokenize("echo hello")
# [%Token{type: :name, value: "echo", ...}, %Token{type: :name, value: "hello", ...}, ...]
@spec tokenize!(String.t()) :: [JustBash.Parser.Lexer.Token.t()]
Tokenize a bash command string, raising on error.