Repository navigation
Home
A minimal, local-first Go coding agent with bounded context and sandboxed execution.
- Delegation-first: sub-agents isolate work so it never fills the parent conversation
- Sandboxed execution: bash and subprocess tools run inside a bubblewrap sandbox; sandbox violations prompt for user decision
- Provider-agnostic: same config shape for local and cloud providers
Most coding agents are designed for cloud models with large context windows. Steiner is designed for the opposite constraint: local LLMs where context is expensive, reasoning quality degrades as the window fills, and you want to know exactly what is happening before anything changes on disk.
The core bet is that delegation is a better context strategy than summarisation. When a sub-agent handles exploration or code changes, the parent conversation never sees the intermediate turns — only the result. Combined with per-source byte budgets and late-stage compaction, this keeps the working context lean across long sessions.
By default, bash and subprocess tools run inside a sandbox that prevents writes outside the workspace and filters credential-bearing environment variables. Users can temporarily allow writable access to specific paths or disable sandboxing entirely with the --unsafe flag.
The provider/model split means you can point steiner at Ollama, LM Studio, OpenRouter, or a native Anthropic or OpenAI endpoint with the same config shape and the same tool behavior.
-
Prerequisites: Go
1.25+. For local models, Ollama or LM Studio. -
Start a local model (Ollama example):
ollama run qwen2.5-coder:14b
-
Run from source:
Or start interactive mode:
go run ./cmd/steiner --exec "summarize this repository in one sentence"go run ./cmd/steiner
-
Inspect resolved configuration before doing real work:
go run ./cmd/steiner config
-
Optional — build a local binary:
make build-binaries ./bin/steiner
Interactive mode — launches the TUI, accepts natural-language requests, streams tool calls and responses:
go run ./cmd/steiner
# or
./bin/steinerSingle-shot — runs one request and exits:
go run ./cmd/steiner --exec "explain the auth package"Commands reference:
| Command | What it does |
|---|---|
version |
Print the build version; --short for script-friendly output |
config |
Print the resolved configuration (providers, models, limits) |
update / upgrade
|
Self-update to the latest release; optionally specify a version |
tools |
List configured tools and their approval status |
skills |
List discovered skills |
--help |
Print all flags and usage |
steiner --version and steiner version print a multi-line panel with
build metadata:
version v0.1.0
commit abc1234
built 2026-06-17T12:00:00Z
go go1.25.0
channel stable
Labels are bold with the configured accent color; values use the default foreground. Use
steiner version --short to print only the raw version string (no styling,
no labels) for scripts and CI.
steiner update (alias upgrade) runs in two phases: check then apply.
checking version
Current: v0.1.0
Latest: v0.2.0
updating...
✔ updated
If already up to date, the apply phase is skipped:
checking version
Current: v0.2.0
Latest: v0.2.0
✔ Up to date
When switching between dev and stable channels, a warning is shown:
⚠ notice switching from dev to stable — this replaces your current build
checking version
…
A spinner animates during check and download. On a non-TTY or when NO_COLOR is
set, the spinner degrades to a static line.
Configuration is loaded in this precedence order (later overrides earlier):
- Compiled defaults
~/.config/steiner/config.yaml-
.steiner/config.yaml(project-local) - Environment variables with the
STEINER_prefix - CLI flags
Example — local LLM (Ollama or LM Studio):
providers:
local:
type: openai_compat
base_url: http://localhost:11434/v1
models:
default: local
definitions:
local:
provider: local
id: qwen2.5-coder:14bSee Configuration for all provider types, model fields, limit overrides, sandbox settings, sub-agent config, and environment variables.
| Tool | Description |
|---|---|
read |
Read files with offset/limit pagination; detects and base64-encodes images |
mutate |
Apply structured file mutations atomically (create, write, replace, delete, move, insert); parent directories must exist for workspace paths |
glob |
Find files by pattern |
grep |
Search file contents with surrounding context |
ls |
List directory contents |
bash |
Run shell commands (sandboxed by default) |
scratchpad |
Record working state; persists across compaction |
fetch_url |
Fetch a URL and return its content: HTML converted to markdown, text formats (JSON, YAML, plain text, CSV, etc.) returned raw, images returned for vision models; large responses saved to disk |
display_file |
Show a file in the TUI overlay without adding to conversation |
advisor |
Ask a stronger-model steering advisor for guidance, optionally passing question and files for it to review (requires advisor.enabled) |
workflow_handoff |
Transition to a different workflow with approved artifacts |
MCP tools from connected servers appear alongside built-ins with the mcp__<server>__<tool> prefix.
By default, bash and subprocess tools run inside a Linux sandbox (using bubblewrap) that restricts writes to files outside the workspace and filters credential-bearing environment variables. The sandbox bind-mounts the entire host filesystem read-only (--ro-bind / /), with writable overlays for the workspace and a sandbox state directory (.steiner/home/). All installed toolchains, system libraries, and user config files are accessible read-only without per-path configuration.
Steiner also creates an ephemeral in-memory OpenSSH client-config overlay for sandboxed commands so ssh can read the system client config and static drop-ins without copying SSH config into the workspace. If OpenSSH still rejects the config inside the sandbox, Steiner can prompt to rerun the command outside the sandbox.
When a sandboxed tool attempts to write to a file outside the workspace, the user is prompted to either grant temporary access, use the --unsafe flag to disable sandboxing, or cancel the operation. This protects against accidental workspace-external writes and environment variable leakage. Reading host files (including on-disk credentials) is not restricted — sandboxing is not a confidentiality boundary, and private SSH keys are never copied into the workspace.
Platform support: Linux only (automatic detection; sandboxing is disabled on macOS and Windows). When sandboxing is unavailable or bypassed, the sandbox status is reported in the TUI and an optional warning banner is shown (see sandbox.warning_on_unsupported_platform in Configuration). Bash and subprocess tools fall back to unsandboxed execution.
For configuration, environment variable filtering, mount layout, and troubleshooting, see docs/tool-sandboxing.md.
Local LLMs have limited context windows — often measured in tens of thousands of tokens, not millions. Every turn accumulates tokens from model output and tool results, and long contexts cost more while degrading reasoning quality. Steiner keeps the context window lean through three mechanisms, ordered by effectiveness.
Delegation is the primary strategy. Sub-agents isolate work from the parent conversation. The full turn-by-turn transcript of exploration, code changes, or research never enters the parent context at all — only the result comes back. This is the most effective mechanism because it prevents context growth rather than managing it after the fact.
Per-source byte budgets cap each context source (preamble, agent summaries, skills, tool results, conversation history) so no single category can crowd out the others. Compaction kicks in when estimated prompt tokens reach 70% of the context window: older turns are summarised by the model into a compact durable prefix, then dropped from the live history.
See docs/context-management.md for the full reference.
Delegation is steiner's primary context management strategy. steiner exposes eight sub-agent tools that delegate bounded tasks to isolated child agents. Sub-agent delegation is enabled by default — the model sees these tools alongside the built-ins, and its system prompt casts it as the orchestrator: it plans, dispatches, verifies, and integrates rather than doing the default implementation work itself:
| Tool | What it does | Can mutate? |
|---|---|---|
explore |
Navigate the codebase to find files, symbols, call sites, and patterns | No |
research |
Gather and synthesise information from the codebase or web | No |
code |
Implement a scoped change — read relevant files, write changes, run tests | Yes |
evaluate |
Analyse a sub-problem, evaluate options, and produce a structured recommendation | No |
sanity_check |
Run tests, linters, builds, or other checks and report pass or fail | No |
review |
Examine code changes for bugs, regressions, missing tests, or plan adherence | No |
vision |
Analyze a pasted image by ID — sub-agent receives the image directly | No |
follow_up |
Resume an existing sub-agent session by agent ID with a new user message | No |
See docs/sub-agent-delegation.md for full documentation, including per-agent tool allowlists and safety restrictions.
Interactive sessions run in plan or build mode. In plan mode project edits are restricted: mutate writes are limited to .steiner/plans/, the code sub-agent tool and any follow_up targeting a mutation-capable child are denied, and when the bubblewrap sandbox is active the rest of the project is bind-mounted read-only for bash as well. Plan artifacts may be written under .steiner/plans/. Plan mode doubles as a chat/Q&A mode: discuss freely, and write a plan file only when handing off. Call workflow_handoff to hand an approved plan off: implement and review start structured /implement and /review workflows from overview.md + plan.yaml, while build directly executes a standalone plan.md in build mode without skill discovery.
Switch modes with Shift+Tab or /mode [plan|build]. The mode never changes the system prompt prefix or tool definitions. In plan mode, a bracketed notice is prepended to every outgoing user message (sticky). In build mode, a one-shot notice announces mode transitions. These notices are stored in the conversation and maintained through prompt caching. The default mode is set by modes.default in config (build unless configured otherwise).
Without a working sandbox (sandbox.enabled: false, non-Linux, or bwrap unavailable), plan mode is an agent/tool policy, not a filesystem-level guarantee: bash runs unenforced, while the mutate write restriction and the sub-agent denials stay hard-gated regardless of sandbox state.
See docs/execution-modes.md for the full enforcement matrix and persistence details.
Go tests guard against the orchestration canon (delegationInstructions in internal/prompt/system.go) drifting out of sync with the skill and oneshot prompt files that describe delegation to the model.
The specialist roster is typed Go data (internal/prompt/specialists.go) and the ## Your specialists table is rendered from it, so the table cannot diverge from the roster. On top of that:
- a roster vocabulary check in
internal/prompt/flags consumer files that name a sub-agent or tool no longer in the roster; - a companion test in
internal/delegation/asserts the roster matches the registeredAgentTypeconstants; - a gated-tool check asserts canon never names a tool that is not registered in every session it renders in;
- shared-block checks pin the prose that bundled skills, and the oneshot prompts, deliberately duplicate verbatim.
All run as part of go test ./..., and therefore as part of make check.
See docs/canon-drift-checks.md for what counts as canon, the consumer file list, and how to resolve a failing check.
When you paste an image (Ctrl+V), steiner saves it to .steiner/tmp/images/ and assigns it a session-unique ID (e.g. img-1). The TUI displays the ID, dimensions, size, and file path below the submitted message.
The strip placeholder — shown on subsequent turns when image data is omitted — includes the ID and file path so the model knows where to find the image. To re-examine it, call vision with the image ID:
vision(task: "what color is the button?", image_id: "img-1")
For follow-up questions about the same image, use follow_up with the agent_id returned by the initial vision call — the image is already in the sub-agent's history and cached server-side.
The vision tool requires a vision-capable model configured under sub_agent.agents.vision.model. Image files are deleted automatically when the agent exits.
Connect Model Context Protocol servers to give the model access to external tools. MCP is enabled by default. Each server is configured with its own approval mode (ask, allow, deny) and the model sees registered tools alongside built-ins with the mcp__ prefix.
mcp:
servers:
context-mode:
enabled: true
command: npx
args: [-y, context-mode]
env:
npm_config_cache: /tmp/npm-cache
microsoft-learn:
enabled: true
transport: http
url: https://learn.microsoft.com/api/mcpSee MCP servers for the full reference.
MCP behaviour is covered by hermetic, CI-safe tests for both transports; live validation against real third-party servers is tracked in #438.
A stronger-model steering pass that reviews the live conversation and returns concise guidance. No tools, no mutation, no child loop.
advisor:
enabled: true
models:
advisor: advisor-modelSee Advisor for configuration options and behavior reference.
Token-weighted prompt-cache hit rate, always-on with no configuration. Surfaces in the sidebar and via the /cache-stats overlay command. Includes main agent model calls and advisor traffic.
See Cache Stats for the metric definition, storage format, and privacy notes.
Autonomous three-phase orchestration (plan, implement, review). Each phase runs as a sub-agent in an isolated git worktree. Runs are resumable.
oneshot:
auto_pr: false
models:
oneshot:
plan: ""
implement: ""
review: ""See Oneshot Mode for invocation, configuration, and resume behaviour.
Native notifications when the agent needs approval or handoff. Linux fully supported; macOS and Windows no-op with a startup warning.
desktop_notifications:
enabled: trueSee Desktop Notifications for platform support and configuration.
These features are documented in Optional Features:
-
cave_human— terse output with anti-AI-writing style instruction -
Accent colour — customizable TUI accent with 13 presets and
/accentpicker - Web search — model-facing search tool with Google, Kagi, Brave, and SearXNG backends
- Image paste and recall — Ctrl+V image input with auto-resize, token accounting, and vision sub-agent re-examination
- Conversation forking — fork live or saved sessions into independent copies
- Code simplification — parallel sub-agent analysis for reuse, simplification, efficiency, and altitude
- Codex OAuth — use OpenAI Codex subscription models without a separate API key
- MCP servers — connect Model Context Protocol servers with per-server approval control
Steiner can self-update via the update (or upgrade) command. It fetches the
latest release from GitHub, verifies the binary checksum, and atomically
replaces the running executable.
A GitHub token is not strictly required, but GitHub API rate limits may apply
without one. Set STEINER_GITHUB_TOKEN in your environment to authenticate:
export STEINER_GITHUB_TOKEN=ghp_...Pass a version tag to install a specific stable release instead of the latest:
steiner update v1.2.0
steiner update 1.2.0 # "v" prefix is optionalThe update command selects the release channel via the --dev flag, which is
a root-level persistent flag and also works on the update subcommand:
# Stable channel (default): installs the latest stable release.
steiner update
# Dev channel: pulls the build published on every merge to main.
steiner update --dev
steiner --dev update--dev selects the dev release channel; its absence selects the stable
channel. Both a dev build version string (dev or dev-<sha>) and a stable
semver (vX.Y.Z) honor the flag. The dev binary is always replaced with the
latest dev release; the stable binary is only replaced when a newer semver
is available.
When switching channels — dev to stable or stable to dev — a warning is
printed with the channel names highlighted in the accent colour. --dev and
a specific version cannot be used together.
# Build
go build ./...
make build-binaries
# Test
go test ./...
go test -race ./...
# Lint and vet
go vet ./...
golangci-lint run ./...
# All checks (lint + vet + format check)
make check
# Format
make fmtFor agent contributor context, architecture constraints, and conventions, see AGENTS.md.
Get started
Using Steiner
- How to work with steiner
- Interactive mode
- CLI reference
- Skills
- Sub-agent delegation
- Oneshot
- Context management
- Advisor
- Security audit
Tools and integrations
Safety
Usage and cost
Reference
Internals and maintenance