{"id":50372120,"url":"https://github.com/elixir-ai-tools/just_bash","last_synced_at":"2026-06-15T22:00:47.592Z","repository":{"id":338262928,"uuid":"1132383240","full_name":"elixir-ai-tools/just_bash","owner":"elixir-ai-tools","description":"An Elixir bash interpreter with a virtual filesystem ","archived":false,"fork":false,"pushed_at":"2026-06-10T21:56:16.000Z","size":3303,"stargazers_count":75,"open_issues_count":7,"forks_count":5,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-10T23:21:47.705Z","etag":null,"topics":["bash","elxiir"],"latest_commit_sha":null,"homepage":"https://hexdocs.pm/just_bash/readme.html","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/elixir-ai-tools.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-11T21:22:55.000Z","updated_at":"2026-06-10T21:56:06.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/elixir-ai-tools/just_bash","commit_stats":null,"previous_names":["elixir-ai-tools/just_bash"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/elixir-ai-tools/just_bash","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-ai-tools%2Fjust_bash","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-ai-tools%2Fjust_bash/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-ai-tools%2Fjust_bash/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-ai-tools%2Fjust_bash/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/elixir-ai-tools","download_url":"https://codeload.github.com/elixir-ai-tools/just_bash/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-ai-tools%2Fjust_bash/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34381762,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-15T02:00:07.085Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bash","elxiir"],"created_at":"2026-05-30T08:00:19.049Z","updated_at":"2026-06-15T22:00:47.585Z","avatar_url":"https://github.com/elixir-ai-tools.png","language":"Elixir","funding_links":[],"categories":["Ports"],"sub_categories":[],"readme":"# JustBash\n\nA simulated bash environment with an in-memory virtual filesystem, written in Elixir.\n\nDesigned for AI agents that need a secure, sandboxed bash environment.\n\nSupports optional network access via `curl` and `wget` with HTTPS-only enforcement and host allowlists.\n\n\u003e **Note**: This is an Elixir port of [just-bash](https://github.com/vercel-labs/just-bash) by Vercel. The entire codebase was generated through conversational prompting with Claude Opus 4.5 via [OpenCode](https://opencode.ai).\n\n## Security Model\n\nJustBash treats shell code as untrusted and sandboxes it in memory. Custom commands passed via\n`:commands` are trusted host-side extensions supplied by the library caller, and JustBash does not\nsandbox them or provide safety guarantees for them.\n\n- The shell only has access to the provided virtual filesystem\n- No access to the real filesystem by default\n- No network access by default\n- Network access can be enabled with host allowlists — HTTPS-only by default\n- Custom commands are outside the sandbox and can bypass the virtual filesystem and network policy\n\n## Installation\n\n```elixir\ndef deps do\n  [{:just_bash, \"~\u003e 0.1.0\"}]\nend\n```\n\n## Usage\n\n### Basic API\n\n```elixir\nbash = JustBash.new()\n{_result, bash} = JustBash.exec(bash, ~s(echo \"Hello\" \u003e greeting.txt))\n{result, _bash} = JustBash.exec(bash, \"cat greeting.txt\")\nresult.stdout  #=\u003e \"Hello\\n\"\nresult.exit_code  #=\u003e 0\n```\n\n### Configuration\n\n```elixir\nbash = JustBash.new(\n  files: %{\"/data/file.txt\" =\u003e \"content\"},  # Initial files\n  env: %{\"MY_VAR\" =\u003e \"value\"},              # Environment variables\n  cwd: \"/app\"                                # Starting directory\n)\n```\n\n### Network Access\n\nNetwork access is disabled by default. When enabled, only HTTPS is permitted and\nan explicit allowlist is required:\n\n```elixir\n# Allow specific hosts (HTTPS only)\nbash = JustBash.new(\n  network: %{\n    enabled: true,\n    allow_list: [\"api.github.com\", \"*.example.com\"]\n  }\n)\n\n# Allow all hosts\nbash = JustBash.new(\n  network: %{enabled: true, allow_list: :all}\n)\n\n# Also allow plain HTTP (not recommended)\nbash = JustBash.new(\n  network: %{enabled: true, allow_list: :all, allow_insecure: true}\n)\n\n# Custom HTTP client for testing\nbash = JustBash.new(\n  network: %{enabled: true, allow_list: :all},\n  http_client: MyMockHttpClient\n)\n```\n\n### Custom Commands\n\nCustom commands are trusted extensions supplied by the library caller, not untrusted shell input.\nJustBash does not sandbox them and does not provide safety guarantees for them.\n\nRegister trusted host-side commands with `commands:`:\n\n```elixir\ndefmodule MyApp.Commands.Greet do\n  @behaviour JustBash.Commands.Command\n\n  @impl true\n  def names, do: [\"greet\", \"hello\"]\n\n  @impl true\n  def execute(bash, args, _stdin) do\n    name = Enum.join(args, \" \")\n    {%{stdout: \"Hello, #{name}!\\n\", stderr: \"\", exit_code: 0}, bash}\n  end\nend\n\nbash = JustBash.new(commands: %{\"greet\" =\u003e MyApp.Commands.Greet})\n{result, _bash} = JustBash.exec(bash, \"hello world\")\nresult.stdout  #=\u003e \"Hello, world!\\n\"\n```\n\n#### Custom command context\n\nPass caller data into custom commands with the `:context` option. It is stored on the `JustBash`\nstruct as `context` (default `%{}`) and is readable inside any custom command as `bash.context`.\nBuiltins and the interpreter ignore it.\n\n```elixir\ndefmodule MyApp.Commands.Whoami do\n  @behaviour JustBash.Commands.Command\n\n  @impl true\n  def names, do: [\"whoami_ctx\"]\n\n  @impl true\n  def execute(bash, _args, _stdin) do\n    user = Map.get(bash.context, :user, \"anonymous\")\n    {%{stdout: \"#{user}\\n\", stderr: \"\", exit_code: 0}, bash}\n  end\nend\n\nbash =\n  JustBash.new(\n    context: %{user: \"alice\"},\n    commands: %{\"whoami_ctx\" =\u003e MyApp.Commands.Whoami}\n  )\n\n{result, _bash} = JustBash.exec(bash, \"whoami_ctx\")\nresult.stdout  #=\u003e \"alice\\n\"\n```\n\n#### Updating context after construction\n\nThe `:context` option seeds caller data at construction. To add or update entries *afterward*, use\nthe `put_context/3` and `get_context/3` accessors (modeled on `Plug.Conn.put_private/3`). Both target\nthe same `context` map, keys are atoms, and the map is ignored by builtins and the interpreter.\n\n```elixir\ndefmodule MyApp.Commands.Counter do\n  @behaviour JustBash.Commands.Command\n\n  @impl true\n  def names, do: [\"counter_ctx\"]\n\n  @impl true\n  def execute(bash, _args, _stdin) do\n    count = JustBash.get_context(bash, :count, 0)\n    {%{stdout: \"#{count}\\n\", stderr: \"\", exit_code: 0}, bash}\n  end\nend\n\nbash =\n  JustBash.new(commands: %{\"counter_ctx\" =\u003e MyApp.Commands.Counter})\n  |\u003e JustBash.put_context(:count, 41)\n\n{result, _bash} = JustBash.exec(bash, \"counter_ctx\")\nresult.stdout  #=\u003e \"41\\n\"\n```\n\nImportant caveats:\n\n- Custom commands run arbitrary Elixir code in the host BEAM process\n- They are not restricted by the virtual filesystem or `network:` policy\n- Registration keys must appear in `names/0`; aliases from `names/0` are registered automatically\n- Shell functions still win over custom commands at execution time\n- Protected stateful builtins such as `cd`, `export`, `trap`, and `return` cannot be overridden\n\n### Namespaced CLIs (`JustBash.CLI`)\n\nWhen a single tool needs many subcommands — `acme pr review`, `acme product list` — don't\nhand-roll a `case` router, manual `--help`, and ad-hoc error strings in `execute/3`.\n`JustBash.CLI` is a declarative subcommand layer that gives you **routing**, **typed\nargument parsing**, and **auto-generated help, errors, and docs** from a single source of\ntruth. A CLI is plain data (a `%JustBash.CLI{}` tree) that registers like any other\ncommand:\n\n```elixir\nalias JustBash.CLI\nalias JustBash.Commands.Command\n\ncli =\n  CLI.new(\"acme\", doc: \"Acme operations toolkit\", commands: [\n    CLI.command(\"pr\", doc: \"Pull request management\", commands: [\n      CLI.command(\"review\",\n        doc: \"Review a pull request\",\n        flags: [\n          report:  [type: :integer, required: true, doc: \"ID of the report to review\"],\n          format:  [type: :string, default: \"text\", values: ~w(text json), doc: \"Output format\"],\n          verbose: [type: :boolean, short: \"-v\"]\n        ],\n        run: fn inv -\u003e\n          tag = if inv.flags.verbose, do: \"[v] \", else: \"\"\n          {Command.ok(\"#{tag}report #{inv.flags.report} as #{inv.flags.format}\\n\"), inv.bash}\n        end)\n    ])\n  ])\n\nbash = JustBash.new(commands: %{\"acme\" =\u003e cli})\n{result, _} = JustBash.exec(bash, \"acme pr review --report 42 --format json\")\nresult.stdout  #=\u003e \"report 42 as json\\n\"\n```\n\nEach leaf's `:run` handler takes a single `%JustBash.CLI.Invocation{}` (`flags`, `args`,\n`bash`, `stdin`, `path`) and returns `{result, bash}` — the same contract as a plain\ncustom command, so handlers keep full access to `bash.fs`, `bash.context`, etc. Use a\ncapture (`run: \u0026Acme.PR.review/1`) to keep handler logic in named, testable functions.\n\nHelp, `did you mean` suggestions, and usage-bearing errors come for free and are\nconsistent across every CLI — which is exactly what an agent needs to recover from a typo\nin one turn:\n\n```text\n$ acme pr review --help\nacme pr review - Review a pull request\n\nUsage: acme pr review --report \u003cint\u003e [--format text|json] [-v]\n\nOptions:\n  --report \u003cint\u003e       ID of the report to review (required)\n  --format text|json   Output format (values: text, json) (default: text)\n  -v, --verbose\n\n$ acme pr reviw\nacme: unknown command 'pr reviw'\nDid you mean 'pr review'?\nRun 'acme --help' for available commands.      # exit code 2\n\n$ acme pr review\nacme pr review: missing required flag: --report\nUsage: acme pr review --report \u003cint\u003e [--format text|json] [-v]   # exit code 2\n```\n\nBecause the spec is declarative, you can introspect it to generate the tool documentation\nthat goes into an agent's system prompt — from the same source as the runtime behavior:\n\n```elixir\nJustBash.CLI.describe(cli)\n#=\u003e %{name: \"acme\", doc: \"...\", commands: [%{path: [\"pr\", \"review\"], flags: [...], ...}]}\n\nJustBash.CLI.render_docs(cli, format: :markdown)  # a markdown manual\n```\n\nIf you prefer a CLI to live as a module alongside your other command modules, `use\nJustBash.CLI` and define `spec/0` (conventional `use`-wiring, not a DSL):\n\n```elixir\ndefmodule Acme.CLI do\n  use JustBash.CLI\n\n  @impl true\n  def spec, do: JustBash.CLI.new(\"acme\", doc: \"Acme toolkit\", commands: [...])\nend\n\nbash = JustBash.new(commands: %{\"acme\" =\u003e Acme.CLI})\n```\n\nFor a complete before/after, compare [`eval/commands/kv.ex`](eval/commands/kv.ex) (a\nhand-rolled router with help text duplicated by hand) against\n[`eval/commands/kv_cli.ex`](eval/commands/kv_cli.ex) (the same tool on `JustBash.CLI`,\nwhere only the storage logic remains). CLI handlers carry the same trust model and crash\nisolation as any custom command — they are host code and are **not** sandboxed.\n\n### Execute Script Files\n\n```elixir\n# Run a script from the virtual filesystem\nbash = JustBash.new(files: %{\"/script.sh\" =\u003e \"echo hello\"})\n{result, bash} = JustBash.exec_file(bash, \"/script.sh\")\n```\n\n### Sigil\n\n```elixir\nimport JustBash.Sigil\n\nresult = ~b\"echo hello\"\nresult.stdout  #=\u003e \"hello\\n\"\n\n# Modifiers\n~b\"echo hello\"t  # trimmed output\n~b\"echo hello\"s  # stdout only\n~b\"exit 42\"e     # exit code\n```\n\n## Supported Commands\n\n### File Operations\n\n`cat`, `chmod`, `chown`, `cp`, `du`, `file`, `find`, `ln`, `ls`, `mkdir`, `mktemp`, `mv`, `readlink`, `realpath`, `rm`, `stat`, `touch`, `tree`\n\n### Text Processing\n\n`awk`, `base64`, `comm`, `cut`, `diff`, `expand`, `fold`, `grep`, `head`, `md5sum`, `nl`, `paste`, `rev`, `sed`, `sha256sum`, `shasum`, `sort`, `tac`, `tail`, `tr`, `uniq`, `wc`, `xargs`\n\n### Data Processing\n\n`jq` (JSON), `markdown` (Markdown → HTML)\n\n### Network\n\n`curl`, `wget`\n\n### Shell Builtins\n\n`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`\n\n### Utilities\n\n`arch`, `basename`, `date`, `dirname`, `env`, `hostname`, `id`, `nproc`, `printenv`, `seq`, `sleep`, `tee`, `uname`, `which`, `whoami`, `yes`\n\n## Shell Features\n\n- **Pipes**: `cmd1 | cmd2`\n- **Redirections**: `\u003e`, `\u003e\u003e`, `2\u003e`, `\u0026\u003e`, `\u003c`, `\u003c\u003c\u003c`, heredocs\n- **Command chaining**: `\u0026\u0026`, `||`, `;`\n- **Variables**: `$VAR`, `${VAR}`, `${VAR:-default}`, `${VAR:=value}`, `${#VAR}`, `${VAR:start:len}`, `${VAR#pattern}`, `${VAR%pattern}`, `${VAR/old/new}`, `${VAR^^}`, `${VAR,,}`\n- **Brace expansion**: `{a,b,c}`, `{1..10}`, `{a..z}`\n- **Arithmetic**: `$((expr))` with full operators\n- **Glob patterns**: `*`, `?`, `[...]`\n- **Control flow**: `if/elif/else/fi`, `for/while/until`, `case/esac`\n- **Functions**: `function name { ... }` or `name() { ... }`\n- **Indexed arrays**: `arr=(...)`, `${arr[0]}`, `${arr[@]}`, `${#arr[@]}`\n- **Associative arrays**: `declare -A map`, `map[key]=value`, `${map[key]}`\n- **Subshells**: `(cmd)` and command groups `{ cmd; }`\n\n## Default Layout\n\nWhen created without options, JustBash provides a Unix-like directory structure:\n\n- `/home/user` - Default working directory (and `$HOME`)\n- `/bin`, `/usr/bin` - Binary directories\n- `/tmp` - Temporary files\n\n## API Reference\n\n```elixir\n# Create environment\nbash = JustBash.new(opts)\n\n# Execute command\n{result, bash} = JustBash.exec(bash, \"command\")\nresult.stdout      # String\nresult.stderr      # String\nresult.exit_code   # Integer\nresult.env         # Updated environment\n\n# Execute script from virtual filesystem\n{result, bash} = JustBash.exec_file(bash, \"/path/to/script.sh\")\n\n# Parse without executing\n{:ok, ast} = JustBash.parse(\"echo hello\")\n\n# Format script\n{:ok, formatted} = JustBash.format(\"if true;then echo yes;fi\")\n```\n\n## Development\n\n```bash\nmix deps.get\nmix test           # Unit, integration, property-based, and bash-comparison tests\nmix dialyzer       # Type checking\nmix credo --strict # Linting\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felixir-ai-tools%2Fjust_bash","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Felixir-ai-tools%2Fjust_bash","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felixir-ai-tools%2Fjust_bash/lists"}