MCPcopy Index your code
hub / github.com/ewhauser/gbash

github.com/ewhauser/gbash @v0.0.38

Chat with this repo
repository ↗ · DeepWiki ↗ · release v0.0.38 ↗ · + Follow
13,743 symbols 48,640 edges 924 files 1,017 documented · 7% updated 16d agov0.0.38 · 2026-05-04★ 387 open issues
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

gbash

A deterministic, sandbox-only, bash-like runtime for AI agents, implemented in Go.

Shell parsing and execution are owned in-tree under internal/shell, with a project-owned virtual filesystem, registry-backed command execution, policy enforcement, and structured tracing around that shell core. Commands never fall through to host binaries, and network access is off by default. Originally inspired by Vercel's just-bash.

[!WARNING] This is alpha software. It is likely that additional security hardening is needed. Use with care.

Table of Contents

Features

  • Virtual in-memory filesystem — no host access by default
  • Registry-backed command execution — unknown commands never run host binaries
  • 90+ built-in commands with GNU coreutils compatibility coverage (compatibility report)
  • Optional allowlisted network access via curl
  • Persistent sessions with shared filesystem state across executions
  • Shared JSON-RPC server mode for session-oriented hosts and wrapper binaries
  • Host directory mounting with read-only overlay for real project workspaces
  • Execution budgets — command count, loop iterations, glob expansion, stdout/stderr limits
  • Opt-in structured trace events and lifecycle logs for debugging and agent orchestration
  • WebAssembly support — runs in the browser (demo)

Public Packages

  • github.com/ewhauser/gbash: the core Go runtime and embedding API
  • github.com/ewhauser/gbash/host: the public host adapter boundary for platform and process behavior
  • github.com/ewhauser/gbash/server: shared JSON-RPC server mode for hosting persistent gbash sessions
  • github.com/ewhauser/gbash/contrib/...: optional Go command and tool modules
  • @ewhauser/gbash-wasm/browser: the explicit browser entrypoint for the js/wasm package. It is versioned in-repo today; npm publishing remains disabled in the release workflow for now.
  • @ewhauser/gbash-wasm/node: the explicit Node entrypoint for the same js/wasm package.

Installation

Library:

go get github.com/ewhauser/gbash

CLI:

go install github.com/ewhauser/gbash/cmd/gbash@latest

Extras CLI:

go install github.com/ewhauser/gbash/contrib/extras/cmd/gbash-extras@latest

Prebuilt gbash and gbash-extras archives are also available on the GitHub Releases page. Released Go modules are also requested from the public Go proxy during the release workflow so their API docs stay current on pkg.go.dev. The coordinated release workflow also exports the website and deploys it to GitHub Pages, preserving raw compatibility assets under /compat/latest/ for the published compatibility report.

Quick Start

Try it with go run — no install required:

go run github.com/ewhauser/gbash/cmd/gbash@latest -c 'echo hello; pwd; ls -la'

You should see hello, the default working directory /home/agent, and the initial listing for the empty sandbox home directory.

Everything runs inside a virtual filesystem — nothing touches your host.

Usage

Go API

Use gbash.New to configure a runtime and Runtime.Run for one-shot execution.

package main

import (
    "context"
    "fmt"

    "github.com/ewhauser/gbash"
)

func main() {
    gb, err := gbash.New()
    if err != nil {
        panic(err)
    }

    result, err := gb.Run(context.Background(), &gbash.ExecutionRequest{
        Script: "echo hello\npwd\n",
    })
    if err != nil {
        panic(err)
    }

    fmt.Printf("exit=%d\n", result.ExitCode)
    fmt.Print(result.Stdout)
}
exit=0
hello
/home/agent

Persistent Sessions

Use Session.Exec when you want multiple shell executions to share one sandbox filesystem.

package main

import (
    "context"
    "fmt"

    "github.com/ewhauser/gbash"
)

func main() {
    ctx := context.Background()

    gb, err := gbash.New()
    if err != nil {
        panic(err)
    }

    session, err := gb.NewSession(ctx)
    if err != nil {
        panic(err)
    }

    if _, err := session.Exec(ctx, &gbash.ExecutionRequest{
        Script: "echo hello > /shared.txt\n",
    }); err != nil {
        panic(err)
    }

    result, err := session.Exec(ctx, &gbash.ExecutionRequest{
        Script: "cat /shared.txt\npwd\n",
    })
    if err != nil {
        panic(err)
    }

    fmt.Print(result.Stdout)
}
hello
/home/agent

CLI

Pipe a script to the CLI to execute it inside the sandbox:

printf 'echo hi\npwd\n' | gbash
hi
/home/agent

When stdin is a terminal, the CLI starts an interactive shell automatically:

gbash

You can also force interactive mode explicitly:

printf 'pwd\ncd /tmp\npwd\nexit\n' | gbash -i

The interactive shell reuses one sandbox session and carries forward filesystem and environment state. It exposes a session-local history command, but it does not provide readline-style line editing or job control.

For host-backed CLI runs, you can switch the filesystem mode explicitly:

gbash --root /path/to/project --cwd /home/agent/project -c 'pwd; ls'

--root mounts a host directory read-only at /home/agent/project under an in-memory writable overlay. --cwd sets the initial sandbox working directory.

For programmatic wrappers and harnesses, non-interactive runs can emit one structured JSON object instead of streaming stdout and stderr directly:

gbash -c 'echo hello' --json

The JSON payload includes stdout, stderr, exitCode, truncation flags, timing metadata, and trace metadata when the wrapper enables tracing on the underlying runtime.

For parser and tooling workflows, you can dump the parsed shell AST without executing the script:

gbash --dump-ast -c 'echo hello'

--dump-ast writes pretty-printed typed JSON for the parsed shell/syntax tree. Add --detect to choose the parser variant from a leading shebang first, then a positional file extension for file-backed inputs, and otherwise fall back to bash.

For long-lived agent or editor integrations, the same shared CLI frontend can serve a JSON-RPC protocol instead of executing one script:

gbash --server --socket /tmp/gbash.sock --session-ttl 30m
gbash --server --listen 127.0.0.1:8080 --session-ttl 30m

The server speaks JSON-RPC 2.0 over either a Unix socket or an explicit loopback TCP listener. session_id maps 1:1 to a persistent sandbox session, and session.exec runs one non-interactive shell execution inside that session and returns the full result in one response. Filesystem shape is still chosen at server startup through the normal CLI/runtime options such as --root, --readwrite-root, and --cwd; it is not configured over the wire. The shared CLI requires exactly one transport flag, --socket PATH or --listen HOST:PORT, and --listen is restricted to loopback hosts because the protocol has no built-in authentication.

Install gbash-extras when you want the same CLI surface with the stable official contrib commands (awk, html-to-markdown, jq, sqlite3, and yq) pre-registered:

gbash-extras -c 'jq -r .name data.json'

gbash-extras --server --socket /tmp/gbash-extras.sock and gbash-extras --server --listen 127.0.0.1:8081 expose the same JSON-RPC protocol with the stable extras registry already installed.

The shared frontend is also exposed as the public github.com/ewhauser/gbash/cli package. Call cli.Run with a cli.Config to reuse the stock flag parsing, interactive mode, server mode, and runtime setup from your own wrapper binary. For direct embedding without going through the CLI package, use github.com/ewhauser/gbash/server.

Configuration

Filesystem

Most callers should use one of these entry points:

  • gbash.New() — default mutable in-memory sandbox
  • gbash.WithFileSystem(gbash.SeededInMemoryFileSystem(...)) — in-memory sandbox preloaded with eager or lazy files
  • gbash.WithWorkspace(root) — real host directory mounted read-only under an in-memory overlay
  • gbash.WithFileSystem(gbash.MountableFileSystem(...)) — multi-mount namespace over a base filesystem plus sibling mounts
  • gbash.WithFileSystem(gbash.ReadWriteDirectoryFileSystem(...)) — just-bash-style mutable host-backed root
  • gbash.WithFileSystem(gbash.CustomFileSystem(...)) — custom backends

Preload an in-memory sandbox with eager or lazy files:

gb, err := gbash.New(
    gbash.WithFileSystem(gbash.SeededInMemoryFileSystem(gbfs.InitialFiles{
        "/home/agent/config.json": {Content: []byte("{\"mode\":\"dev\"}\n")},
        "/home/agent/big.txt": {
            Lazy: func(ctx context.Context) ([]byte, error) {
                return fetchLargeFixture(ctx)
            },
        },
    })),
)

Compose multiple sandbox mounts under one namespace:

gb, err := gbash.New(
    gbash.WithFileSystem(gbash.MountableFileSystem(gbash.MountableFileSystemOptions{
        Mounts: []gbfs.MountConfig{
            {MountPoint: "/workspace", Factory: gbfs.Overlay(gbfs.Host(gbfs.HostOptions{Root: "/path/to/project", VirtualRoot: "/"}))},
            {MountPoint: "/cache", Factory: gbfs.Memory()},
        },
    })),
)

Mount a host directory as a read-only workspace overlay:

gb, err := gbash.New(
    gbash.WithWorkspace("/path/to/project"),
)

This mounts the host directory read-only at /home/agent/project, starts the session there, and keeps all writes in the in-memory upper layer.

For full control over the mount point or host-file read cap:

gb, err := gbash.New(
    gbash.WithFileSystem(gbash.HostDirectoryFileSystem("/path/to/project", gbash.HostDirectoryOptions{
        MountPoint: "/home/agent/project",
    })),
)

If you want writes to persist directly back to the host directory instead of landing in the in-memory overlay, use the read/write helper:

gb, err := gbash.New(
    gbash.WithFileSystem(gbash.ReadWriteDirectoryFileSystem("/path/to/project", gbash.ReadWriteDirectoryOptions{})),
    gbash.WithWorkingDir("/"),
)

This mode maps the host directory to sandbox /, so it is best suited to compatibility harnesses and other opt-in developer workflows.

Host Platform

By default, gbash uses an internal virtual host adapter. That preserves the existing sandbox behavior: virtual pipes, virtual process metadata, and build-dependent platform defaults.

Embedders can opt into the underlying process and OS view with WithHost(host.NewSystem()):

import (
    "github.com/ewhauser/gbash"
    "github.com/ewhauser/gbash/host"
)

gb, err := gbash.New(
    gbash.WithHost(host.NewSystem()),
)

Network Access

Network access is disabled by default. Enable it to register curl in the sandbox.

For simple URL allowlisting, use WithHTTPAccess:

package main

import (
    "context"
    "fmt"

    "github.com/ewhauser/gbash"
)

func main() {
    gb, err := gbash.New(
        gbash.WithHTTPAccess("https://api.example.com/v1/"),
    )
    if err != nil {
        panic(err)
    }

    result, err := gb.Run(context.Background(), &gbash.ExecutionRequest{
        Script: "curl -o /tmp/status.json https://api.example.com/v1/status\ncat /tmp/status.json\n",
    })
    if err != nil {
        panic(err)
    }

    fmt.Print(result.Stdout)
}

For fine-grained control over methods, response limits, and private-range blocking, use WithNetwork:

gb, err := gbash.New(
    gbash.WithNetwork(&gbash.NetworkConfig{
        AllowedURLPrefixes: []string{"https://api.example.com/v1/"},
        AllowedMethods:     []gbash.Method{gbash.MethodGet, gbash.MethodHead},
        MaxResponseBytes:   10 << 20,
        DenyPrivateRanges:  true,
    }),
)

Allowed URL prefixes are origin- and path-boundary aware. For example, https://api.example.com/v1 matches /v1 and /v1/..., but not /v10.

For full transport control in tests or embedding, inject your own Config.NetworkClient. See examples/oauth-network-extension for a demo that injects OAuth headers from a host-side vault so the sandbox never sees the bearer token.

Observability

Tracing and logging are disabled by default.

Use WithTracing(TraceConfig{Mode: gbash.TraceRedacted}) to populate ExecutionResult.Events for non-interactive runs and to receive structured OnEvent callbacks for both non-interactive and interactive executions. TraceRedacted is the recommended mode for agent workloads. TraceRaw preserves full argv and path metadata and should only be used when you control the sink and retention policy.

Use WithLogger to receive top-level lifecycle logs: exec.start, stdout, stderr, exec.finish, and exec.error. Logger callbacks receive the same captured stdout and stderr strings returned in ExecutionResult.

Security Model

  • The shell only sees the filesystem and runtime configuration you provide.
  • Command execution is registry-backed. Unknown commands never execute host binaries.
  • Network access is off by default. When enabled, requests are constrained by allowlists and runtime limits.
  • The default static policy applies execution budgets such as command-count, loop-iteration, glob-expansion, substitution-depth, and stdout/stderr capture limits.
  • Structured trace events are opt-in. Redacted tracing is the recommended default, and raw tracing is unsafe unless you

Extension points exported contracts — how you extend this code

Expr (Interface)
Expr is the abstract syntax tree for any AWK expression. [19 implementers]
contrib/awk/goawk/internal/ast/ast.go
WordPart (Interface)
WordPart represents all nodes that can form part of a word. These are [*Lit], [*SglQuoted], [*DblQuoted], [*ParamExp], [9 …
shell/syntax/nodes.go
Environ (Interface)
Environ is the base interface for a shell's environment, allowing it to fetch variables by name and to iterate over all [8 …
shell/expand/environ.go
Event (Interface)
Event is implemented by every semantic event value emitted by the observer API. [14 implementers]
shell/analysis/analysis.go
SpecProvider (Interface)
SpecProvider exposes declarative command metadata for shared parsing, help, and version rendering. [96 implementers]
commands/command_spec.go
StdinReader (Interface)
StdinReader is the interface for standard input in the interpreter. It supports reading, closing, and deadline-based can [8 …
internal/shell/interp/virtual_pipe.go
FIFOFileSystem (Interface)
FIFOFileSystem is an optional filesystem capability for creating named pipes. [10 implementers]
fs/fifo.go
FileSystem (Interface)
FileSystem is the project-owned filesystem contract used by the runtime. [14 implementers]
fs/fs.go

Core symbols most depended-on inside this repo

String
called by 1299
contrib/awk/goawk/internal/ast/ast.go
Run
called by 1111
commands/command.go
WriteString
called by 935
shell/syntax/printer.go
exitf
called by 704
internal/builtins/command_helpers.go
WriteByte
called by 509
shell/syntax/printer.go
quoteGNUOperand
called by 446
internal/builtins/basename.go
Has
called by 428
commands/command_spec.go
rune
called by 299
shell/syntax/lexer.go

Shape

Function 7,969
Method 4,069
Struct 1,349
TypeAlias 211
Interface 106
FuncType 27
Class 12

Languages

Go98%
TypeScript2%

Modules by API surface

shell/syntax/parser.go409 symbols
shell/syntax/nodes.go312 symbols
contrib/awk/goawk/internal/ast/ast.go200 symbols
shell/expand/expand.go162 symbols
internal/shell/interp/builtin.go141 symbols
internal/shell/interp/runner.go137 symbols
internal/builtins/sort.go132 symbols
internal/builtins/ls.go126 symbols
internal/builtins/dd.go112 symbols
internal/shell/core.go107 symbols
internal/shell/interp/vars.go106 symbols
internal/builtins/path_test.go105 symbols

For agents

$ claude mcp add gbash \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page