Skip to main content

lib/ex_bashkit.ex

defmodule ExBashkit do
  @moduledoc """
  Run bash scripts in a sandboxed, virtual interpreter — no real processes,
  no host filesystem, no network unless you grant it.

  ExBashkit wraps [bashkit](https://github.com/everruns/bashkit), a pure-Rust
  reimplementation of bash. Scripts execute entirely in-memory: ~150 builtins
  (`echo`, `grep`, `sed`, `awk`, `jq`, `cat`, `find`, …) are reimplemented in
  Rust rather than shelled out, file I/O hits a virtual filesystem, and there is
  no `fork`/`exec` escape hatch. This makes it safe to run untrusted scripts —
  e.g. bash produced by an LLM agent.

  ## Quick start

      iex> ExBashkit.exec("echo hello | tr a-z A-Z")
      {:ok, %ExBashkit.Result{stdout: "HELLO\\n", stderr: "", exit_code: 0}}

  `exec/1` is stateless: each call runs in a fresh interpreter with no host
  filesystem and no network. For state that persists across calls — environment,
  cwd, an in-memory filesystem, shell functions — plus the capability options
  (resource limits, host mounts, a network allowlist, Elixir-defined custom
  builtins, virtual-filesystem backends, and snapshot/restore), use
  `ExBashkit.Session`.

  When running untrusted scripts under concurrent load, see the "Hardening for
  untrusted load" section of `ExBashkit.Session` and the optional
  `ExBashkit.Pool`.
  """

  alias ExBashkit.Result

  @doc """
  Execute a bash `script` in a fresh sandbox and return its result.

  Returns `{:ok, %ExBashkit.Result{}}` on success (note: a non-zero
  `exit_code` is still `{:ok, ...}` — the script ran; it just failed, exactly
  like a real shell). Returns `{:error, message}` if the script could not be
  parsed or the interpreter itself errored.

  Each call runs in an independent sandbox; no state carries across calls.

  ## Examples

      iex> {:ok, result} = ExBashkit.exec("echo hi")
      iex> result.stdout
      "hi\\n"

      iex> {:ok, result} = ExBashkit.exec("false")
      iex> result.exit_code
      1
  """
  @spec exec(String.t()) :: {:ok, Result.t()} | {:error, String.t()}
  def exec(script) when is_binary(script) do
    case ExBashkit.Native.exec(script) do
      {:ok, {stdout, stderr, exit_code}} ->
        {:ok, %Result{stdout: stdout, stderr: stderr, exit_code: exit_code}}

      {:error, message} ->
        {:error, message}
    end
  end
end