Sir Bone
Sir Bone is a Rust CLI coding agent: it streams responses from an LLM, executes tools (bash, file ops, web fetch, …), and renders the conversation in a REPL or a full-screen TUI. Think “Claude Code clone”, written from scratch in ~10k lines of Rust.
It supports prompt caching, extended thinking, multimodal image input, per-project configuration, workspace snapshots with rollback, and MCP servers.
Why it exists
- Single static binary. No runtime, no node_modules —
cargo buildand run. - Provider-agnostic. Anthropic out of the box; any OpenAI-compatible endpoint (OpenAI, Ollama, Groq, …) via two env vars.
- Safe by default. A permission pipeline classifies tool calls (allow / ask / deny) before anything touches your machine; mutations are snapshotted for rollback.
See it without installing
There’s an interactive live demo of the TUI running entirely in your browser (compiled to WebAssembly, no API key, scripted responses). It’s the fastest way to get a feel for the interface before building anything.
How to read these docs
- Getting Started — build, configure, first run.
- Providers — Anthropic vs OpenAI-compatible, model selection.
- Configuration — layered JSON config,
.env. - Tools — the built-in toolset.
- Permissions — how tool calls are gated.
- The TUI — keys, panels, slash commands.
- Sessions & Snapshots — persistence and rollback.
- MCP — connecting external tool servers.
Getting Started
Try it without an API key
sirbone demo # the bundled recording
sirbone demo path/to/session.jsonl # any session file you have
demo replays a real recorded run — a failing test suite, two bugs found and fixed, tests green —
inside the actual TUI: same rendering, same diffs, same tool boxes. No provider is contacted, and
typing a prompt says so instead of calling one. It is a recording, not a live agent.
The bundled recording is an artifact, not prose: scripts/record-demo.sh resets playground/ to
its committed broken fixtures, runs one real turn, and scrubs the recording machine’s paths out of
the session file. The prompt lives in assets/demo-prompt.txt, and a test asserts the recording
still starts from it — recipe and artifact cannot drift.
Build
Requires a Rust toolchain.
cargo build # debug build
cargo build --release # optimized
The binary auto-detects its provider from environment variables, so the minimum to run is an API token.
Credentials
sirbone login is an interactive wizard: pick a provider from a preset menu (Anthropic, z.ai/GLM,
OpenRouter, Groq, Google AI Studio, OpenAI, or a custom OpenAI-compatible endpoint), paste your key,
and it writes one global ~/.sirbone/.env (chmod 600), then runs a live connection test. If a
key is already exported in your shell it offers to persist it — no re-pasting. Configure once
and run from any directory; a per-project .env (or a real environment variable) still wins where
present. Non-interactive shells (CI, pipes) fall back to seeding a template.
login and doctor both run without a key, so setup never requires entering the TUI first.
To write ~/.sirbone/.env by hand instead — the provider is auto-detected
(ANTHROPIC_AUTH_TOKEN → Anthropic, else OpenAI-compatible):
# Anthropic
ANTHROPIC_AUTH_TOKEN=sk-ant-...
SIRBONE_MODEL=claude-opus-4-7
# OpenAI / compatible (Ollama, Groq, LiteLLM, …)
OPENAI_API_KEY=sk-...
OPENAI_BASE_URL=https://api.openai.com/v1 # omit for OpenAI default
SIRBONE_MODEL=gpt-4o
sirbone env lists every variable the agent reads, with its current value.
First run
# TUI (the default front-end)
ANTHROPIC_AUTH_TOKEN="…" SIRBONE_MODEL="claude-opus-4-7" cargo run
# One-shot (non-interactive)
ANTHROPIC_AUTH_TOKEN="…" SIRBONE_MODEL="claude-opus-4-7" cargo run -- "summarize src/agent.rs"
# REPL / readline mode
cargo run -- --repl
# Headless (like `claude -p`): one turn, print, exit. Prompt from arg or stdin.
cargo run -- -p "summarize src/agent.rs"
echo "summarize src/agent.rs" | cargo run -- -p
cargo run -- -p --output-format json "…" | jq # {result,status,usage,session}
Useful flags
| Flag | Effect |
|---|---|
--repl | readline REPL instead of the default TUI |
--review-only | read-only run: no writes, no MCP, bash limited to the read-only whitelist |
--session <path> | resume a saved session (~/.sirbone/sessions/<uuid>.jsonl) |
--thinking-budget <n> | enable extended thinking with an n-token budget |
--temperature <t> | pin the sampling temperature (unset = provider default; Claude models after Opus 4.6 accept only 1.0) |
--image <path> | attach an image to the prompt (multimodal) |
-p / --print | headless: run one turn, print, exit (prompt from arg or stdin) |
--output-format <text|json> | one-shot output; json emits {result,status,usage,session} |
--oracle | explicitly enable the configured post-Done verification gate in headless/REPL runs |
cargo run -- --session ~/.sirbone/sessions/<uuid>.jsonl "follow-up question"
cargo run -- --thinking-budget 10000 "design a retry strategy for the HTTP client"
cargo run -- --image screenshot.png "what's wrong with this UI?"
Configure project verification
Run the deterministic wizard from the project root:
cargo run -- setup-verification
cargo run -- setup-verification --json # discovery only; never writes
It reads structured Rust, Python, and Node/TypeScript manifests, then shows any
authoritative and post-edit commands and offers the opt-in high_risk
confirmation preset before asking whether to run or save the exact patch.
Configuration alone does not enable the oracle in headless/REPL runs; add
--oracle or set SIRBONE_ORACLE=1. See Configuration
for hooks, ablations, and telemetry.
Try the UI first
If you just want to see the interface, open the in-browser live demo — no install, no key.
Providers
Sir Bone picks a provider from environment variables, in priority order:
SIRBONE_CODEX=1→ ChatGPT Plus/Pro through the official Codex CLIANTHROPIC_AUTH_TOKENset → Anthropic- else
OPENAI_API_KEYset → OpenAI / OpenAI-compatible
The model is chosen with SIRBONE_MODEL.
ChatGPT Plus/Pro (Codex OAuth)
ChatGPT subscriptions and OpenAI API billing are separate. To use a Plus/Pro subscription, install the official Codex CLI and authenticate it in the browser:
sirbone login --codex
SIRBONE_CODEX=1 sirbone
You can also use sirbone login codex or sirbone --codex. Sir Bone delegates
the turn to codex exec --json; it never reads or stores the Codex OAuth token.
In this mode Codex owns the model/tool execution, so Sir Bone’s own API-key
client and tool-confirmation pipeline are not used.
Codex uses its workspace-write sandbox by default. If Sir Bone already runs
inside a trusted outer sandbox that prevents nested Linux namespaces, set
SIRBONE_CODEX_SANDBOX=danger-full-access. This disables only Codex’s inner
sandbox, so use it only when the outer sandbox provides the intended boundary.
Anthropic
export ANTHROPIC_AUTH_TOKEN="sk-…"
export SIRBONE_MODEL="claude-opus-4-7"
cargo run
The Anthropic client streams via SSE and supports:
- Prompt caching —
cache_controlon the system prompt and tool definitions. - Extended thinking —
--thinking-budget <tokens>. - Multimodal — images attached with
--imageare sent as base64 blocks.
On GLM via z.ai (either the Anthropic-compatible or the OpenAI-compatible endpoint) the thinking dial is translated into the reasoning-effort levels z.ai actually honours — a token budget sent there only selects a heavy default:
Dial (TUI t / --thinking-budget) | What GLM runs |
|---|---|
| off / light (no budget) | light thinking (thinking: disabled / minimal) |
| 8k | low |
| 16k | medium |
| 32k | max |
Full off does not exist on z.ai: every “disable” spelling is converted to a
lightweight-thinking low. Measured on glm-5.2/glm-5.3: light ≈ 30–80
reasoning tokens, low ≈ 130–670, medium ≈ 50–880, max ≈ 350–1160 — with wait
time scaling accordingly. On the OpenAI-compatible endpoint the same dial is
sent as reasoning_effort; leaving it unset there means z.ai’s default, which
is max.
OpenAI-compatible (OpenAI, Ollama, Groq, …)
Any endpoint speaking the OpenAI chat-completions API works via OPENAI_API_KEY plus
an optional OPENAI_BASE_URL:
# OpenAI
export OPENAI_API_KEY="sk-…"
export SIRBONE_MODEL="gpt-4o"
# Ollama (local) — any non-empty key, point the base URL at the daemon
export OPENAI_API_KEY="ollama"
export OPENAI_BASE_URL="http://localhost:11434/v1"
export SIRBONE_MODEL="llama3.2"
# Groq
export OPENAI_API_KEY="gsk_…"
export OPENAI_BASE_URL="https://api.groq.com/openai/v1"
export SIRBONE_MODEL="llama-3.3-70b-versatile"
Images on an OpenAI-compatible endpoint
No OpenAI-compatible endpoint advertises whether its model can see, so sirbone does not
guess: images are dropped unless you declare the capability with --vision
(SIRBONE_VISION=1). With the flag set, attachments travel as image_url parts carrying
a base64 data URI — the format llama.cpp, OpenAI and OpenRouter all read.
# llama-server with a vision projector
llama-server -m qwen3-vl-8b.gguf --mmproj mmproj-qwen3-vl-8b.gguf -c 32768
export OPENAI_API_KEY="local"
export OPENAI_BASE_URL="http://127.0.0.1:8080/v1"
sirbone --vision --image shot.png "what's wrong with this UI?"
The flag is a claim about the endpoint, not a probe: switching to a text-only model with
/model leaves it on, and the model will hallucinate the image instead of reading it. On
the Anthropic path it does nothing — vision is inferred from the host there.
Image bytes count against the server’s context but not against sirbone’s compaction
estimate, which weighs base64 as text. On a small local window, set SIRBONE_CONTEXT_WINDOW
conservatively (e.g. 30000).
Both clients retry with backoff on HTTP 429 and 5xx responses.
Configuration
Sir Bone reads layered JSON configuration plus environment variables (.env is
loaded automatically).
Layers
- Global —
~/.sirbone/config.json - Per-project —
~/.sirbone/projects/<slug>/config.json
Merge rules:
- Most sections in the per-project file replace the global section wholesale.
mcpServersis merged per key (see MCP).- An empty config falls back to legacy default behavior.
Common keys
| Key | Purpose |
|---|---|
permissions | allow / soft-deny globs + classifier settings — see Permissions |
post_edit_check | map of file glob → lint/format command run after edits |
hooks | deterministic presets plus pre_tool_use, post_tool_use, tusk result filters, and bounded stop checks |
oracle | authoritative command run after Done when explicitly enabled |
mcpServers | external MCP servers to spawn at startup |
environment | enables the LLM permission classifier for undecided bash commands |
.env
Credentials and model selection are usually kept in a .env at the project root:
ANTHROPIC_AUTH_TOKEN=sk-…
SIRBONE_MODEL=claude-opus-4-7
# or an OpenAI-compatible endpoint:
# OPENAI_API_KEY=…
# OPENAI_BASE_URL=https://api.groq.com/openai/v1
Guided verification setup
Run sirbone setup-verification from the project root, or
/setup-verification in the TUI. The wizard reads only structured manifests:
Cargo metadata, Python pyproject.toml tool sections, and declared
package.json scripts with a detected Node lockfile. It performs no LLM or
network call. It shows the command, directory, source, and exact JSON patch
before offering Run and save, Save without running, Edit, or
Cancel. A failed command needs a second confirmation before it can be saved.
With no TTY it writes nothing. sirbone setup-verification --json prints the
candidate commands and their sources without modifying configuration. Manual
commands and glob/command pairs cover ecosystems without a built-in detector.
The atomic project write preserves unrelated keys and never creates pre or
stop hooks automatically. The wizard separately offers the opt-in
high_risk confirmation preset and includes that choice in the exact patch
shown before saving.
{
"oracle": {
"test_command": "cargo test -q",
"max_attempts": 3
},
"hooks": {
"post_tool_use": {
"*.rs": "cargo check -q --message-format=short"
}
}
}
Hooks and oracle
post_tool_use is advisory: matching checks run once per distinct command and
failures are appended to tool feedback. pre_tool_use is an advanced policy
gate; stop is a bounded completion-invariant gate (exit 2 requests another
turn). Output is bounded and all hooks time out. The built-in high_risk
preset returns the normal interactive Ask verdict before dependency or
migration operations, manifest/schema changes, and recognizable public-API
edits. It never approves an operation automatically.
{
"hooks": {
"presets": ["high_risk"],
"pre_tool_use": [{"match": "bash", "command": "./policy-check"}],
"post_tool_use": {"*.py": "python -m ruff check ."},
"tusk": [{"match": "*", "command": "./redact-secrets"}],
"stop": ["./completion-invariant"]
}
}
pre_tool_use exit codes
The hook receives {"tool": …, "input": …} on stdin and decides with its exit
code. Exits 0, 4 and 5 skip the LLM command classifier.
| Exit | Meaning |
|---|---|
0 | allow |
2 | deny — the output is the reason the model reads |
3 | ask — route through the normal interactive confirmation |
4 | allow a rewritten call — stdout is a JSON object merged into the tool input |
5 | short-circuit — stdout is the tool result and the tool never runs |
| anything else | no verdict: fall through to the normal permission path |
A hook that exits 4 without printing a JSON object is a configuration error,
and the call is denied rather than run unchanged. Use 4 to normalize a command
(npm → pnpm), 5 to answer from a cache or to replace a built-in tool with
your own implementation.
tusk: filtering tool results
tusk filters see a tool result before anything else does — the model’s
context, the session transcript and the UI are all written from the single point
where the filter runs. This is where a secret a command printed can be removed,
which no post_tool_use check can do (it only appends).
The result arrives raw on stdin, so an ordinary text filter is a valid hook and
a chain of them composes like a shell pipeline (each filter sees what the
previous one produced). Metadata comes through the environment: SIRBONE_TOOL,
SIRBONE_TOOL_INPUT (the tool input as JSON), and SIRBONE_IS_ERROR (0/1).
Filters run on failed calls too — an error quoting the command that produced it
is exactly as likely to carry a secret.
| Exit | Meaning |
|---|---|
0, no output | pass the result through unchanged |
0, with output | stdout replaces the result |
2 | withhold the result; the output becomes the reason |
| anything else, spawn failure, timeout | withhold — a filter that fails open is not a filter |
Because any non-zero exit withholds, a filter must exit 0 on the happy path:
write grep -v SECRET || true, since grep exits 1 when it selects no lines.
Configuring any tusk filter also disables spill-to-file for the run — spilling
happens inside the tool, before a filter can see the content, and would leave
the unfiltered original on disk.
A filter you can actually use
Redact anything shaped like a key, so a stray env, a cat .env, or a stack
trace quoting a token never becomes part of the transcript:
# ~/.sirbone/redact-secrets — chmod +x
#!/bin/sh
sed -E \
-e 's/(sk-[A-Za-z0-9_-]{8})[A-Za-z0-9_-]+/\1…[redacted]/g' \
-e 's/(gh[pousr]_[A-Za-z0-9]{4})[A-Za-z0-9]+/\1…[redacted]/g' \
-e 's/((API_KEY|TOKEN|SECRET|PASSWORD)[[:space:]]*=[[:space:]]*).+/\1[redacted]/g'
{"hooks": {"tusk": [{"match": "*", "command": "~/.sirbone/redact-secrets"}]}}
sed exits 0 whether or not it substituted anything, which is exactly the
behaviour a filter needs. The token rules keep the first few characters on
purpose (sk-ant-api0…[redacted]): the model can still tell two different keys
apart, and still see that a value was there, without learning it. The
KEY=/TOKEN= rule is deliberately blunter and drops the value entirely.
To refuse a whole result instead of editing it, exit 2 — here, rather than let
the model read a production dump at all:
#!/bin/sh
if grep -qi 'BEGIN .*PRIVATE KEY'; then
echo 'result withheld: it contained a private key' >&2
exit 2
fi
# nothing to say: pass the result through untouched
exit 0
That script consumes stdin in grep, so it prints nothing on the happy path —
and exit 0 with no output means “unchanged”, not “empty result”.
Verify a filter before trusting it, without spending a model call:
printf 'export API_KEY=sk-live-abcdefghijklmnop\n' | ~/.sirbone/redact-secrets; echo "exit=$?"
Once it is configured, tusk_runs, tusk_edits and tusk_withheld in
sirbone stats tell you whether it ever actually fired. A filter that never
ran is not protection — zero edits on a run that printed a secret means the
glob or the pattern is wrong, not that you were safe.
Asking sirbone to write the hook for you
Nothing here has to be typed by hand: “add a tusk filter that redacts API keys” is a normal request, and the agent writes both the script and the config entry. Two things are worth knowing before you accept the edit.
It takes effect on the next turn, not the next launch. Configuration is read from disk on every agent run, so a hook written during one turn is live for the following one. The exception is spill-to-file, which is switched off once per run: the turn that creates the first filter still has spilling on.
Writing the config asks first, and that prompt is the point. Both
~/.sirbone/config.json and the per-project
~/.sirbone/projects/<slug>/config.json are inside the trust root, so a write to
either routes through confirmation even under a permissive policy — see
trust. A hooks entry is a command that will run in a shell on
every matching call, and a permissions entry rewrites the gate itself, so read
the diff rather than approving it reflexively. The script the entry points at is
an ordinary file at an ordinary path and is not guarded; the confirmation you
get is for the config, not for the code it names.
Trimming noise you would otherwise pay for
Redaction is the motivating case, not the only one. A filter sees the raw
result, so whatever it removes is never paid for: a build log’s thousands of
lines, an npm progress bar, Docker timestamps — all of it is input tokens
charged on every turn for as long as it stays in context.
{"hooks": {"tusk": [
{"match": "bash", "command": "grep -v 'warning: unused' || true"},
{"match": "bash", "command": "tail -200"}
]}}
Filters chain in order, each seeing what the previous one produced.
This is not what truncation and spill-to-file do, and the difference is the
reason configuring a filter switches spilling off. Truncation fires only on an
oversized result, keeps its head and tail, and writes the full original to
~/.sirbone/projects/<slug>/spill/ so the elided middle is still recoverable —
the noise is preserved, on disk, by design. A tusk filter runs on every result
and the text it removes never exists anywhere.
The oracle is authoritative only after the agent declares Done. Configuration
alone does not activate it in headless or REPL runs: pass --oracle or set
SIRBONE_ORACLE=1. The TUI keeps its per-project /oracle toggle. The
model-facing verify tool is registered only when oracle.test_command exists;
the human /verify command remains available.
Tool profiles remain cost-aware: ask_user and todo are registered only for
interactive frontends, while undo and job_status remain available headless
because prior experiments established their recoverability value. Verification
adds no system-prompt instruction; deterministic output reaches the model only
when a hook, oracle, or explicit verify run actually executes.
For controlled ablations, set comma-separated SIRBONE_DISABLE entries:
hook:pre, hook:post, hook:tusk, hook:stop, oracle:gate, or ask:rounds. Ablation is
applied after loading configuration, so paired arms receive identical commands.
The experimental multi-question ask_user schema is enabled with
SIRBONE_ASK_ROUNDS=1; it remains disabled by default. Interactive trials
validated aggregate replies (fewer submissions and provider calls with the same
decisions), but did not establish a stable latency benefit or enough evidence to
change the model-facing default. The campaign is closed unless default promotion
is reconsidered. Both the single-question and round schemas require decision
context and an explanation of the consequence or trade-off for each option.
Old string-only payloads remain readable for session replay.
Verification counters are emitted in [usage], persisted in session telemetry,
and aggregated by sirbone audit and sirbone stats: hook runs/failures/retries,
tusk runs/edits/withholdings, oracle runs/failures/retries/rollbacks/exhaustion,
and ask-user rounds/questions.
verify_tool_runs distinguishes explicit one-shot verification from the
post-Done oracle gate. A zero run count means the mechanism was not exercised,
not that it had no value; smoke results with zero invocations are non-discriminating.
The files under info/ may motivate hypotheses, but they are neither runtime
dependencies nor stronger evidence than the paired benchmarks required by
MISSION.md.
Legacy post-edit checks
post_edit_check runs a command after a file matching the glob is written — handy for
auto-formatting or linting. It remains supported as an alias for
hooks.post_tool_use:
{
"post_edit_check": {
"**/*.rs": "cargo clippy --quiet -- -D warnings"
}
}
A check re-runs after every batch of edits, so its output would otherwise be
re-appended in full each time — on a file with pre-existing warnings, the model
pays for all of them on every edit. Sirbone keeps a per-command record of the
diagnostics it has already shown (matching them by message, not by line number,
so they survive the lines around them moving) and reports only what is new.
Anything already reported collapses into a counted reminder that the check is
still failing; a red check never goes quiet. Turn the deduplication off with
SIRBONE_DISABLE=hook:ledger.
Stream rules
A project constraint written into the system prompt is paid for on every single turn, whether or not the model was about to break it. A stream rule costs nothing until it fires: its pattern watches the response as it is being generated, and the first match stops the response mid-sentence, hands the rule back to the model as a reminder, and restarts the turn — so the model reads the rule exactly when it is about to violate it.
{
"stream_rules": [
{
"name": "box-leak",
"pattern": "Box::leak",
"message": "Never Box::leak in production paths; use Arc<str> instead."
}
]
}
pattern is a regular expression matched against the tail of what has been
generated so far. An invalid pattern is skipped with a warning rather than
failing the run.
The mechanism is bounded: a turn can be restarted at most twice, and a rule that
has fired is disarmed for the rest of that turn, so a model that ignores the
reminder still finishes. The aborted text is discarded and never enters the
conversation. With no stream_rules configured nothing changes; ablate a
configured set with SIRBONE_DISABLE=stream:rules.
Tools
Tools are how the agent acts on your machine. Each is a typed unit (input validated
via serde/schemars) registered in a ToolRegistry; the agent can call several in
parallel within a turn.
Built-in tools
| Tool | What it does |
|---|---|
bash | run a shell command (supports background jobs) |
read | read a file (with offset/limit) |
write | create or overwrite a file |
edit | exact-string replacement in a file |
grep | search file contents |
glob | match files by glob |
web_fetch | fetch a URL (HTML converted to markdown) |
web_search | search via the required search2md CLI |
load_skill | pull a skill’s instructions into context |
note | record a working note that survives context compaction |
todo | maintain the live step list for multi-step tasks — rendered as a checklist in the TUI, VS Code, and Zed (ACP plan) |
ask_user | ask the user a multiple-choice question when the decision is theirs (library/approach choice, ambiguous requirement) |
code_map | build/query a structural map of the codebase |
job_status | inspect background bash jobs |
undo | revert the last file mutation |
verify | run the configured authoritative project command once; registered only when oracle.test_command exists |
ask_user questions include a short decision context and 2–4 choices. Each
choice has a stable label plus a concrete consequence or trade-off; frontends
show both. Single-question calls return the selected label; round calls return
answers keyed by question ID with value, option index, and origin (user,
free_text, or dismissed). The first choice is the recommendation. Historical
payloads with string-only options are still accepted, but new tool schemas
publish the explanatory object form.
With SIRBONE_ASK_ROUNDS=1, one call can contain 1–3 independent questions.
The TUI collects the choices locally and sends one aggregate reply; REPL and
bidirectional stream JSON accept one comma-separated reply such as 1,2,1.
ACP v1 cannot represent a multi-question form, so its adapter displays native
permission dialogs sequentially while retaining one model-facing tool call.
Rounds remain experimental and default-off: live trials preserved the decisions
and reduced aggregate submissions and provider work, but latency was not stable
enough to justify changing the default. Disable them explicitly during an
ablation with SIRBONE_DISABLE=ask:rounds.
ask_user and todo are available only in interactive profiles. undo and
job_status remain available headless; verify is conditional on project
configuration, so projects without an authoritative command pay no tool-schema
cost for it.
Truncated output
A tool result that exceeds the size budget keeps its head and tail and elides the
middle. Before the cut, the full text is written to
~/.sirbone/projects/<slug>/spill/<hash>.txt, and the truncation marker names the
file:
... (8412 lines truncated — full output: /home/you/.sirbone/projects/myproj/spill/a3f1....txt; narrow with grep/offset/limit) ...
So the elided part is one read away rather than one re-run away — which matters
most for bash, where re-running also repeats the command’s side effects. Only
over-budget results are written; the file name is the content hash, so the same
output spilled twice is one file; and each project keeps at most 32 spill files
(256 MB), oldest pruned first. Turn it off with SIRBONE_DISABLE=tool:spill.
Structural read (opt-in)
With SIRBONE_READ_OUTLINE=1, reading a supported source file whole (no
offset/limit) that runs past 80 lines returns its declaration outline instead
of every line:
src/agent/state.rs — outline (882 lines, 14 decls)
30 | pub async fn run(ctx: &mut AgentContext) -> Result<()>
398 | pub(crate) async fn run_turn(ctx: &mut AgentContext) -> Result<AgentState>
elided: 1-29, 31-397, 399-467 — read ONLY these ranges with offset+limit; never guess their contents.
Reads that already ask for a range behave exactly as before, and the freshness
stamp still records the full file, so the guard against writing to a file you
have not read stays as strict as it was. Ablate with
SIRBONE_DISABLE=read:outline.
The patch tool (opt-in)
SIRBONE_HASHLINE=1 swaps edit out for patch — never both, so the model is
never choosing between two ways to say the same thing. Under the flag, read
prefixes a file with its content tag and numbers the lines:
[src/lib.rs#4F2A]
1| pub mod ablate;
2| pub mod acp;
A patch cites those numbers rather than re-copying the text to be replaced, and the tag is checked before anything is written: if the file changed since the read, the call is refused instead of misapplied.
[src/lib.rs#4F2A]
PUT 2.=2:
+pub mod acp;
+pub mod agent;
CUT 10.=14 @moved
PUT >40 @moved
| Form | Meaning |
|---|---|
PUT A.=B: | replace lines A through B with the + body |
PUT <A: / PUT >A: | insert before / after line A (>$ = end of file) |
CUT A.=B [@name] | delete lines A through B, optionally capturing them into a register |
PUT <A @name / PUT A.=B @name | paste a register into a gap or over a range |
MV dest/path.rs | rename the file |
REM | delete the file |
+TEXT | one literal body line (a bare + is an empty line) |
Every address refers to the file as it was read; the edits are applied bottom-up, so one never shifts the addresses of another. One file section per call.
Background jobs
bash with a background flag returns immediately and keeps running; the TUI info bar
shows a live gauge, and a result block lands when the job exits. The job_status
tool lets the agent track progress without busy-waiting.
Skills
A skill is a reusable instruction block. It can be invoked two ways:
- by you, typing
/skill-namein the TUI, or - by the model, calling the
load_skilltool.
Both inject the same skill body into the conversation.
Sir Bone’s native skill roots are ~/.sirbone/skills/ and .sirbone/skills/.
It also reads ~/.agents/skills/ and .agents/skills/ for Agent Skills
compatibility. Prefer the user-level .sirbone root for portable personal
skills you want to back up and carry across machines; project roots are useful
only when a repository intentionally ships its own shared agent workflow.
Every tool call passes through the permission pipeline before it runs, and file mutations are snapshotted so they can be rolled back.
Web search backend
web_search runs search2md search --no-cache --json from PATH. The child process has a
20-second timeout and bounded stdout/stderr capture. Missing binaries, non-zero exits, invalid JSON,
and timeouts are surfaced as explicit tool errors; Sir Bone does not fall back to another engine.
Search results and fetched pages are untrusted data and must not override user or system instructions.
Permissions
Every tool call is classified into a Decision — Allow, Ask, or Deny —
before it executes.
The pipeline
The checks run in order:
--review-only— ahead of everything, including your ownallowglobs.allowglobs — matching calls are allowed outright.soft_denyglobs / destructive heuristics — matching or obviously-destructive calls are blocked (or routed to a prompt).- MCP server trust, then the trust-root guard: writing into
~/.sirbone/{system,prompts,skills}, or to anyconfig.json— the global one or a per-project~/.sirbone/projects/<slug>/config.json— asks first. Those files are how a policy or a hook command is defined, so writing one is a request to widen sirbone’s own trust, not an ordinary edit. The guard reads each tool’smutation_target, so it covers every writer rather than a hardcoded list. Caches and session state in the same directories are not guarded; the match is on the filename. pre_tool_usehooks — your own deterministic gate, decided by exit code. It can allow, deny, ask, rewrite the call, or answer it without running the tool; every verdict except “no opinion” skips the classifier below and the turn it would have cost. See configuration.- LLM classifier — only for undecided, non-safe bash commands, and only when an
environmentis configured. The classifier may also rewrite the command (updatedInput) to a safer equivalent before it runs.
A decision here governs whether the call runs. What it produced is a separate
question: tusk filters can
rewrite or withhold the result before the model, the session file, or the UI
sees it.
Anything that resolves to Ask is routed through a prompt bridge and surfaced as a multi-choice dialog:
- Allow once — run this call, remember nothing.
- Allow always — run it and persist an editable glob rule to the per-project
permissions.allowlist, so the same call is auto-approved from now on. - Deny (with optional free-text feedback forwarded to the model).
The dialog renders in the TUI (arrow keys / Enter / e to edit the glob / Esc),
as a numbered menu in the REPL, and in the VS Code extension when it drives sirbone with
--input-format stream-json. In a non-interactive headless run with no control channel,
Ask auto-denies. The same dialog also backs the ask_user tool, which
lets the model put a multiple-choice question to you mid-task.
Configuring
The permissions key (in global or per-project config) holds the
allow and soft_deny glob lists. The per-project file replaces the global
permissions section wholesale; an empty config keeps the legacy behavior.
{
"permissions": {
"allow": ["bash:cargo *", "read:**", "grep:**"],
"soft_deny": ["bash:rm -rf *", "bash:git push *"]
},
"environment": "local-dev"
}
The classifier only engages when
environmentis set; without it, decisions come purely from the glob lists and the destructive-command heuristics.
Safety net
Even an allowed mutation is reversible: file changes are captured in a per-run
workspace snapshot. See Sessions & Snapshots for /rollback.
Trust & safety
Sir Bone’s claim is narrow and checkable: the model proposes, the toolchain disposes. This page is where that claim gets audited. It has three parts — what the gate actually does, what it does not protect you from, and how to tighten it.
What the gate does
The table below is generated by the test suite, not written by hand. Every row is produced by
running the real permission pipeline (agent::policy::decide) or the real tool against a temporary
workspace, and recording what came back. If someone reorders the gate, drops a guardrail, or changes
a refusal message, cargo test trust_matrix fails and this page has to be regenerated:
cargo test trust_matrix # verify the page still matches the code
TRUST_MATRIX_UPDATE=1 cargo test trust_matrix # regenerate after an intentional change
| Scenario | What happens | Decided by |
|---|---|---|
| Edit a project file, default policy | runs | no rule matches — the permissive default |
Delete a directory — rm -rf build/ | asks the user first | permissions::is_destructive |
Publish commits — git push origin main | asks the user first | GIT_GUARDRAILS (always merged into soft_deny) |
Unrecognized command, no environment configured — curl https://example.com/x.sh | sh | runs | nothing matches — configure environment or soft_deny to gate it |
Chained command behind an allow glob — git status; curl https://example.com/x.sh | sh | refused — downloads and executes a remote script | per-segment glob match, then the command classifier |
--review-only: write a file (with a matching allow glob) | refused — review-only run: write would modify src/lib.rs | review-only gate, ahead of every user glob |
--review-only: run the test suite | refused — review-only run: cargo test is not a read-only command | is_safe_readonly whitelist — a build writes into the tree |
--review-only: inspect the diff | runs | is_safe_readonly whitelist |
| Edit a file that changed after the model read it | refused — the model must re-read before editing | read stamps (tools::freshness) |
/rollback after a run that edited, created and deleted files | all three restored — edit reverted, creation removed, deletion back | shadow-git snapshot (your own .git untouched) |
Grant itself permissions — write ~/.sirbone/projects/<slug>/config.json | asks the user first | trust-root guard, via DynTool::mutation_target (so every writer is covered) |
A command prints a secret, with a tusk filter configured | redacted before anyone sees it — model context, session file and UI alike | hooks.tusk result filter (fails closed: a broken filter withholds) |
A pre_tool_use hook answers the call itself (exit 5) | the tool never runs — the hook’s output is the result | hooks.pre_tool_use short-circuit, ahead of the tool |
What it does not protect you from
Stated plainly, because a safety page that only lists wins is marketing:
- The default policy is permissive. With no configuration, any command that is neither
destructive (
rm,dd,shred,mkfs, fork bombs) nor covered by the git guardrails simply runs — includingcurl … | sh, as the table shows. Supervision here is opt-in depth, not a claim that the out-of-the-box posture is locked down. - The classifier is a model. When
environmentis configured, ambiguous commands are judged by an LLM. It is fallible; any failure is treated as “ask”, never as “allow”, but it is not a proof. --review-onlyblocks MCP tools wholesale. Nothing in the MCP protocol declares whether a remote tool writes, so there is no honest way to admit a read-only one. If you need an MCP server in a review run, you need a normal run with a policy.- Snapshots cover the work tree, not the world.
/rollbackrestores files under the project root. It does not undo a database migration, a pushed commit, or an HTTP request. - Secrets are not redacted unless you configure it. With no
hooks.tuskfilter, whatever a command prints — anenvdump, acat .env, a stack trace quoting a token — goes into the model’s context, the session JSONL on disk, and the UI, exactly as printed. The mechanism to stop that exists and is proven by the table above, but it is off until you write the filter. - The trust-root guard covers the config, not the scripts it points at. Writing a
config.jsonasks first (see the table). The hook script a config already names —~/.sirbone/redact-secretsin the examples — is an ordinary file at an ordinary path, so overwriting it is an ordinary write. It only matters once a hook is configured, and it is then arbitrary code on every matching call: keep your hook scripts where you would keep anything else you execute, and read a diff before accepting an edit to one. - A result filter cannot unsay what the model wrote.
hooks.tuskgoverns what a tool produced. If the model itself composesecho $API_KEY, the secret is in the command — its own tool-call block — not in the result, and no filter over results can remove it. That is the gate’s job (pre_tool_use,soft_deny), and it is why the generated row above asserts only over tool results. - Nothing here constrains what the model says. The gate governs actions, not sentences. For
claims about the code there is a separate, deterministic check —
sirbone ground FILEverifies the paths, symbols and counts a document asserts, with no model in the loop.
Tightening it
Three configuration surfaces, in increasing order of effort:
soft_denyglobs — force confirmation for a pattern, e.g.Bash(docker *). Recall-biased: the raw command or any chained segment matching is enough to ask.allowglobs — auto-approve the boring commands so the prompts you do get are meaningful. For bash, every chained segment must match on its own; substitution ($(…), backticks) never auto-allows.environment— describe the machine in plain language (“production database”, “developer laptop”) and undecided bash goes to the classifier before running.hooks.pre_tool_use— your own gate, decided by a shell exit code and no model call. It can deny, ask, rewrite a call into a safer equivalent, or answer it outright so the tool never runs at all.
Those four decide whether a call runs. One more decides what the rest of the system is allowed to see of what it produced:
hooks.tusk— filters over the tool result, applied at the single point the model’s context, the session file and the UI are all written from. This is the only place a secret a command printed can be removed before it is recorded anywhere. It fails closed: a filter that crashes, times out, or exits non-zero withholds the result rather than passing it through, and configuring any filter disables spill-to-file for the run so no unfiltered copy reaches disk.sirbone statsreportstusk_runs/tusk_edits/tusk_withheld— zero edits on a run that printed a secret means your pattern is wrong, not that you were safe.
See permissions for the JSON schema and the full gate order, and configuration for the hook contracts and a copy-pastable redaction filter.
Review-only runs
sirbone --review-only (or SIRBONE_REVIEW_ONLY=1) is the mode for CI and pre-commit hooks: the
agent reads and reports, and has no authority to change anything.
sirbone --review-only -p "review the staged diff for correctness bugs"
sirbone --review-only -p --output-format json "…" | jq -r .result
Three things happen at once:
- the writing tools (
write,edit/patch,undo) are never registered, so the model does not plan edits it cannot make and you do not pay for their schemas; - the permission gate refuses any tool that declares a
mutation_target, ahead of the user’s ownallowglobs — a glob stored for normal work cannot unlock a write inside a review run; - bash is held to the read-only whitelist, and MCP tools are refused.
The mode is a gate, not a prompt: it does not ask the model to behave, it removes the option.
As a pre-commit hook
Same gate, no CI, no repository secret: the key stays in ~/.sirbone/.env, where it already is.
sirbone hook install # writes .git/hooks/pre-commit
sirbone hook uninstall # removes it again
Every commit then gets one review-only run over the staged diff. The agent cannot amend the
commit, stage files, or run a build — it reads git diff --cached and answers, ending with a
VERDICT: BLOCK <reason> or VERDICT: OK line. Only BLOCK stops the commit, and only the last
such line counts, since a diff under review can quote the word itself.
It fails open, deliberately. No sirbone on PATH, no jq, no provider configured, a
timeout, an empty answer — all let the commit through. A review assistant that blocks work when
the network is down is a review assistant people uninstall. Escape hatches:
git commit --no-verify # skip once
SIRBONE_HOOK=off git commit … # skip always
SIRBONE_HOOK_TIMEOUT=60 # give up sooner (default 180s)
install refuses to touch a pre-commit hook it did not write, and uninstall refuses to delete
one — if you already have a hook, call sirbone --review-only from inside it.
Why not in CI?
Nothing stops you — sirbone --review-only -p --output-format json is one step in a workflow — but
no action ships here, on purpose: it would need a key in a repository secret, and there is no free
hosted model to fall back on. GitHub Models was retired on 30 July 2026 (playground, catalog
and inference API). Actions minutes are free on public repositories; the inference is not. If you
do wire one up, remember that pull_request from a fork gets no secrets, and that
pull_request_target — which does — checks out the base repo, so pointing it at fork code hands
an attacker your token.
The TUI
Launch with sirbone (cargo run from source) — the TUI is the default front-end. The interface has a chat pane, a side trail
panel listing each turn/tool as a clickable step, a status row (run state + extended
thinking tail), and an info bar (provider, model, context usage, cache-hit share,
background jobs).
Try it live: the in-browser demo is this exact TUI compiled to WebAssembly — scripted, no API key. Great for learning the keys.
Keys
| Key | Action |
|---|---|
Tab | switch focus: chat ↔ input |
↑ / ↓ | scroll output (in chat focus) · history (in input focus) |
PgUp / PgDn | fast scroll |
G | jump to bottom (re-enable auto-scroll) |
Alt+B | toggle the trail panel |
Alt+P | cycle color palette |
Alt+A | about screen (keybindings + boar) |
Alt+S | settings (toggle localize, plan, oracle, thinking budget) |
Esc | cancel a running turn / drop a queued message |
Ctrl+C | cancel current turn; at the prompt, quit |
Shift+drag | select text in the chat pane (see below) |
Selecting text with the mouse
The TUI captures the mouse so the scroll wheel, trail clicks, and panel resizing work — which means the terminal’s own text selection is turned off. To copy text, hold a modifier while you drag:
- Linux / WSL (most terminals):
Shift - macOS (iTerm2, Terminal.app):
Option/Alt - Windows Terminal:
CtrlorShift
Release the modifier and the scroll wheel goes back to scrolling the chat.
Markdown & syntax highlighting
Agent responses are rendered as markdown — headings, tables, fenced code blocks with syntax highlighting, and even mermaid diagrams drawn in the terminal.
Slash commands
Type / in the input for built-in commands (e.g. /help, /init, /model,
/plan, /rollback, /tokens, /quit) and any installed skills (/your-skill). Skills typed
this way inject their instruction body into the conversation. /quit exits
immediately — even mid-turn, it cancels the in-flight request first.
Verification has three dedicated commands:
/setup-verificationopens the offline project wizard and writes only after confirmation./oracletoggles and persists the authoritative post-Donegate for this project./verifyruns the configured authoritative command once, independently of the gate toggle.
Post-edit hooks remain advisory; oracle failures are authoritative and can return the agent to the loop within the configured attempt limit.
Palettes
Six built-in palettes (sirbone, catppuccin, gruvbox, nord, tokyo-night, rose-pine).
Cycle with Alt+P; the choice is remembered per project.
Sessions & Snapshots
Sessions (JSONL)
Every run is persisted as an append-only JSONL file under
~/.sirbone/sessions/<uuid>.jsonl — one record per message/event. Resume any session:
cargo run -- --session ~/.sirbone/sessions/<uuid>.jsonl "follow-up question"
This replays the prior transcript into context and continues from there.
Context compaction
Long conversations are compacted automatically: when the context reaches ~87.5% of the window, older messages are summarized by the LLM and the most recent few are kept verbatim. The TUI shows a one-shot warning as you approach the threshold, and the info bar tracks live context usage.
Workspace snapshots & rollback
Before the first mutation of a run, Sir Bone takes a shadow-git snapshot of the workspace. File-mutating tools build on it, so you can undo a whole run’s changes:
/rollback— list available snapshots/rollback <n|id>— restore a specific snapshot
The undo tool reverts the most recent single mutation, while /rollback restores an
entire snapshot — your safety net when an automated edit goes wrong.
Algorithmic project memory (historia)
The historia tool reconstructs project memory on demand from every JSONL session under
~/.sirbone/projects/<slug>/sessions/. The JSONL schema is interpreted structurally: human
requests, assistant answers, plans, mutated paths, tool failures, and run status are distinct
search fields. Thinking, images, successful tool output, injected control messages, duplicate
compaction tails, and compaction boilerplate are removed deterministically.
An empty query returns recent project state. A query can be restricted to requests, assistant answers, plans, files, problems, or status; an unrestricted search weights requests and plans above incidental path/error matches. Results are bounded and newest-first after relevance, so recalling context does not inject every raw transcript into the model context.
This memory is read-only: the chat sessions remain the single source of truth, and no LLM-authored
HISTORIA.md entry is required. Ask the agent what happened, why a solution was chosen, or to
continue an earlier plan and it will call historia explicitly.
Use /historia to continue from the latest relevant state, or add a date/topic such as
/historia 2026-08-10 compaction. The command runs the structured lookup before the model turn and
supplies its result with an explicit continuation contract: inspect the current workspace, resume
unfinished plans, reuse solutions that worked, and avoid repeating documented failed attempts.
MCP
Sir Bone is a generic Model Context Protocol client. It spawns MCP servers over stdio at startup, in the background, and registers their tools before the first turn.
Where servers are declared
- Global —
mcpServersin~/.sirbone/config.json - Per-project —
./.mcp.json
The two are merged per server name; on a name collision the per-project entry wins.
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/project"]
}
}
}
Naming
Each remote tool is registered as mcp__<server>__<tool> so it never collides with a
built-in tool. From the agent’s perspective it’s just another tool, subject to the same
permission pipeline.
Lifecycle
Servers are launched asynchronously at startup so they don’t block the first prompt; their tools become available once the handshake completes (before the first turn runs).
Editor Integrations
Sir Bone runs inside editors two ways: as an ACP external agent (Zed) and as
a VS Code extension. Both drive the same core agent — same tools, sessions,
permissions, and the live todo plan view.
Zed (ACP)
Sir Bone speaks the Agent Client Protocol
(JSON-RPC 2.0 over stdio): Zed launches sirbone acp as a subprocess and talks
to it from the native Agent panel. No extension package needed.
What you get: streaming replies, tool-call cards, permission prompts (allow
once / allow always / reject), session resume, and the agent’s live step list
rendered through ACP’s native plan updates whenever the model uses the todo
tool.
Setup (Zed settings.json):
{
"agent_servers": {
"Sir Bone": { "command": "sirbone", "args": ["acp"] }
}
}
The agent must run in the same environment as the project files (native paths,
no Windows↔WSL translation). Full walkthrough per OS — including the
Windows + WSL remote topology — in the repo under
editors/zed/.
VS Code
The extension in editors/vscode/
(sirbone-vscode-<version>.vsix) embeds a chat webview that drives
sirbone -p --output-format stream-json --input-format stream-json:
- streaming markdown replies (mermaid + syntax highlighting),
- tool boxes with IN/OUT rows and timing — the
todotool renders as a live checklist (completed struck-through, current step highlighted), - permission and
ask_userprompts answered from the UI, - session history replay, slash commands,
@filementions.
Install: code --install-extension sirbone-vscode-<version>.vsix, then set
credentials once with sirbone login (the extension reuses ~/.sirbone/.env).
Plan mode can be enabled from VS Code Settings with Sir Bone: Plan Mode
(sirbone.planMode). The extension then passes SIRBONE_PLAN=1 automatically
on every new turn; the setting is off by default.
Headless / other front-ends
Any front-end can integrate the same way the VS Code extension does: spawn
sirbone -p --output-format stream-json and read one NDJSON event per line
(text, thinking, tool_start, tool_end, ctx, ask, final result).
With --input-format stream-json, permission and multi-choice prompts arrive
as {type:"ask"} events and are answered on stdin.
Live Demo
The whole TUI runs in your browser — compiled to WebAssembly with ratzilla, no API key, no install.
If the button doesn’t work (e.g. you’re reading this outside the published site), the demo lives at
/demo/relative to the docs root.
What it is
The agent itself (real bash, file I/O, network, LLM calls) can’t run in a browser, so the demo reuses the project’s real rendering layer driving a scripted session. Everything you see — markdown, syntax highlighting, mermaid diagrams, tool boxes, the braille boar animation, the side trail, palettes — is the actual TUI code, not a mockup.
Try these
- Watch the scripted conversation play out on its own.
- Type into the input box.
Alt+Pto cycle the color palette.Alt+Afor the about screen,Alt+Sfor settings.↑ / ↓ / PgUp / PgDn / Gto scroll;Tabto switch focus.
Interaction is keyboard-only in the browser build (the web backend reports pixel mouse coordinates with no wheel events, so mouse navigation is disabled). In the real terminal app, mouse scrolling, click-to-expand tool boxes, and panel resizing all work — see The TUI.