Sandbar is a standalone terminal AI coding-agent harness in Go: a streaming
REPL backed by any OpenAI-compatible model endpoint, cloud (OpenRouter,
OpenAI, Gemini) or local (Ollama, llama.cpp, vLLM), with 15 built-in tools
behind tiered approvals, a SQLite thread store with full-text search,
automatic context compression, subagents, plan mode, themes, and a --json
event stream for scripting. It boots with zero configuration from
OPENAI_API_KEY, ships as a single static binary, and has no server
component and no telemetry: everything runs and stays on your machine.
- Streaming REPL: inline Bubble Tea interface (no alt-screen) with live reasoning display, context gauge, and session timer.
- 15 tools, tiered approvals: file read/write/append/patch, shell (local
or SSH), background jobs, git, web search, content search, web fetch, todo
tracking, subagent delegation/resume, image generation, vision analysis.
Every tool is classified
read/write/exec; approve per tier, per tool, or per session. Approvals fail closed in headless runs. - SHA-256 write preconditions: mutating file tools require the digest observed at last read, so conflicting writes fail loudly instead of silently overwriting.
- Cost rollups: usage events are priced against an embedded models.dev catalog snapshot (fully offline); the status bar and one-shot footer show cumulative spend, hidden for unknown or free models.
- Read schemes:
file_readresolvespr://<n>,issue://<n>(GitHub via yourghCLI) andagent://<task-id>(persisted subagent transcripts) before touching the filesystem. - SQLite thread store: every conversation persisted locally with WAL
journaling, FTS5 full-text search (
/search), session resume, forking, and undo. - Context auto-compression: real BPE token counting (offline-embedded vocabulary), summarization with group-aware boundaries that never split tool calls from results, and observable fallbacks. No silent history loss.
- Subagents: delegate self-contained subtasks (
delegate_task), resume interrupted ones (resume_task); independent delegations run concurrently. - Plan mode:
--plan//planruns a read-only turn that produces a plan you approve before anything changes. - Themes: light/dark/monochrome plus Catppuccin, Tokyo Night, Rosé Pine,
Gruvbox, Dracula, and more;
NO_COLORrespected. --jsonscripting mode: newline-delimitedStreamEventstream for scripts and benchmark harnesses; pipe stdin in, events out.- Workspace jail: file operations resolve to the configured workspace root; path traversal and workspace-escaping shell commands are rejected.
- Zero-config boot:
OPENAI_API_KEYalone is enough to start; a commented config template is written for you on first run. - Single static binary: pure-Go SQLite, CGO disabled, cross-compiles to linux/darwin/windows/freebsd on amd64/arm64.
The fastest path needs no config file at all:
export OPENAI_API_KEY=sk-...
sandbarThat synthesizes an OpenAI provider from the environment (OPENAI_BASE_URL
and OPENAI_MODEL override the endpoint and model alias) and writes a
commented config.yaml template to ~/.config/sandbar/config.yaml for you
to edit later. See config.yaml.example for the full annotated schema:
workspace: "./workspace" # file ops are jailed to this directory
database: "sandbar.db" # resolves under ~/.local/share/sandbar
providers:
- name: openrouter-direct
base_url: "https://openrouter.ai/api/v1"
api_key: "${OPENROUTER_API_KEY}"
models:
google/gemini-3.1-flash-lite:
context_length: 262144
model_defaults:
supports_tools: true
tools:
approval:
mode: "yolo" # yolo | write | always-ask
compression:
enabled: true
threshold: 0.80Prebuilt binary: via the install script:
curl -fsSL https://raw.githubusercontent.com/aetherbird/sandbar/main/install.sh | bashThe script detects your platform, downloads the release archive, verifies it
against the published sha256 checksums, and installs to ~/.local/bin
(override with BIN_DIR; pin a release with SANDBAR_VERSION=v0.3.0).
Prebuilt binaries are not published yet, the install script and pinned
versions will work once a goreleaser release ships (see
docs/RELEASE.md). Build from source or go install in
the meantime.
Build from source (Go 1.25+):
git clone https://github.com/aetherbird/sandbar.git
cd sandbar
make build # produces ./sandbar (static, stripped, version-stamped)
make install # installs to ~/.local/bin/sandbarmake build stamps the binary with git describe --tags --always --dirty;
run sandbar version to see it.
go install: go install github.com/aetherbird/sandbar/cmd/sandbar@latest
installs the binary as sandbar (module root is the repo root); the binary
reports the module version of the tag it was installed from; see
sandbar version.
Homebrew / Scoop: planned after the first release (tap and bucket generated by goreleaser).
- Config path: first of:
--config <path>,$SANDBAR_CONFIG,$XDG_CONFIG_HOME/sandbar/config.yaml,~/.config/sandbar/config.yaml,/etc/sandbar/config.yaml. The working directory is never searched. - Env vars:
SANDBAR_CONFIG(config path),SANDBAR_THEME(theme override); provider keys are interpolated into YAML as${VAR}(e.g.${OPENROUTER_API_KEY},${BRAVE_API_KEY}).OPENAI_API_KEYalone boots the zero-config default. - Client prefs:
~/.config/sandbar/client.yamlholds TUI-only preferences (default_model,theme,color_mode,font_size,show_cost, opt-in session-cost display), auto-created with commented defaults on first run. - Data: the SQLite database lives under
~/.local/share/sandbar/(absolutedatabase:values are honored as-is). - models.json: a legacy-style provider registry layered on top of
config.yaml providers (see
models.json.example). Sandbar looks formodels_json:in the config, thenmodels.jsonnext to the loaded config file. Schema:{"providers": {name: {baseUrl, api, apiKey, compat, models[]}}}with model entries{id, name, modelId, contextWindow, maxTokens}andcompatquirks (supportsDeveloperRole,supportsReasoningEffort,maxTokensField,requiresToolResultName,requiresAssistantAfterToolResult,thinkingFormat,sendSessionId). Keys resolve as$ENV/${ENV}(unset → empty),!command(shell stdout, trimmed), or literal. On a provider-name clash models.json wins (the YAML provider is replaced, not an error); JSON providers are appended after the YAML ones. Importing a legacy file that lackssupports_toolsdefaults imported models to tool support. The zero-config env boot ignores models.json.api: "anthropic-messages"routes to the native Anthropic Messages wire client. Compat quirks currently honored:maxTokensField,requiresToolResultName, andthinkingFormat(mapped onto the reasoning dialect);supportsDeveloperRole,supportsReasoningEffort,requiresAssistantAfterToolResult, andsendSessionIdare parsed but not yet applied.
- Skills: on-demand instruction packs discovered from
<workspace>/.sandbar/skills,.claude/skills, and.agents/skills(then~/.config/sandbar/skills,~/.claude/skills,~/.agents/skills; earlier scopes shadow later ones by name). Each is a folder with aSKILL.mdcarrying adescription:header; the system prompt advertises the list and the model reads the file only when relevant. - Prompt templates: markdown files in
<workspace>/.sandbar/promptsor~/.config/sandbar/promptsbecome slash commands:/name argsexpands the body ($1..$9,$@/$ARGUMENTS,${@},${@:N},${@:N:L}) and submits it as your message. Registered commands win over same-named templates. - Prompt files:
SYSTEM.mdreplaces the base persona instructions (everything else in the prompt still assembles around it),APPEND_SYSTEM.mdappends at the end, andTITLE_SYSTEM.mdtemplates the session title from the first message (all support{{cwd}},{{date}}; the title file also{{message}},{{firstLine}}). Looked up per file in<workspace>/.sandbar,.claude,.codex,.agents, then~/.config/sandbar,~/.claude,~/.codex,~/.agents, first existing wins; no ancestor walk.
| Key | What it does |
|---|---|
/model |
Switch model (picker) |
/sessions |
List and resume past sessions |
/resume [id] |
Resume a session by id or unique prefix (picker without argument) |
/new |
Start a fresh thread |
/delete |
Delete the current thread (two-step: /delete confirm) |
/title <text> |
Set the current session's title |
/fork (/branch) |
Branch the current session |
/compress (/compact) |
Compress context now |
/undo |
Remove the last exchange |
/search <query> |
Full-text search past conversations |
/clear |
Clear the screen and start fresh |
/noformat |
Re-print the last response as raw text |
/redraw |
Repaint (recover from render drift) |
/effort <level> |
Set reasoning effort: low | medium | high | default; tropical toggles TROPICAL mode (below) |
/plan |
Toggle plan mode (read-only turn that produces a plan) |
/theme |
Switch theme (picker or id) |
/help (/?) |
Command reference |
/quit (/q, /exit) |
Exit |
! <command> |
Shell escape, run a command in the workspace |
@path |
Mention a file; its content is expanded into the message |
Editing. file_read stamps every line with an 8-hex content hash; paste
those hash-prefixed lines into file_patch's old_str to anchor the edit; stale anchors are rejected with the current hashes instead of silently
patching the wrong lines.
Pipe input for one-shot use:
cat main.go | sandbar "explain this file"
sandbar --json "list the failing tests" | jq -r 'select(.type=="token") | .content'- All local. Threads, messages, and summaries live in SQLite at
~/.local/share/sandbar. There is no telemetry, no crash reporting, and no server component. Outbound traffic goes only to the LLM/search providers you configure. - Fail-closed approvals. When approval policy requires a prompt but no interactive handler exists (headless/scripted runs), the tool call is denied, never silently allowed.
- Workspace jail. File tools and dynamic shell commands are confined to
the configured workspace; path traversal and workspace escapes are
rejected. Note this is convenience hygiene, not a sandbox: the agent runs
with your full OS privileges. Run it inside a container or OS sandbox if
the context is untrusted. See
SECURITY.md.
make fmt # go fmt ./... (CI also enforces gofmt -l is empty)
make test # go test -race -count=1 -skip TestFullTuiPipeline ./...
make build # CGO_ENABLED=0 static build
go vet ./...Layout:
cmd/sandbar/ REPL entry point (the only binary)
internal/agent/ reasoning loop, subagents, steering
internal/backend/ Backend seam (LocalBackend)
internal/catalog/ embedded models.dev pricing snapshot
internal/cliadmin/ admin subcommands (doctor, config)
internal/cliui/ shared CLI rendering
internal/config/ YAML config, resolution, zero-config boot
internal/llm/ OpenAI-compatible client, stream events, tokens
internal/mcp/ Model Context Protocol clients
internal/memory/ SQLite store, FTS5 search, compression
internal/persona/ system prompt assembly, skills
internal/testutil/ test helpers
internal/tools/ tools, registry, approvals, jobs, ssh
internal/ui/theme/ palettes
migrations/ SQLite schema
tests/fixtures/ test fixtures
docs/DESIGN.md full design document
go.mod / go.sum module github.com/aetherbird/sandbar
Makefile build/test targets
.goreleaser.yaml release pipeline
install.sh curl-pipeable installer
config.yaml.example commented configuration template
models.json.example legacy-style provider registry example
system-prompt.md default persona prompt
MIT; see LICENSE.
Sandbar is forked from an in-house harness. Inspired by pi, opencode, and Claude Code.