buildwithnexus
v0.15.1
Published
Fast, local-first AI coding agent for the terminal, written in Rust. Works with hosted APIs (Anthropic, OpenAI) and local models (Ollama).
Maintainers
Readme
buildwithnexus
An agentic AI CLI with 2 ms startup, written in Rust. Remote models via API key, or local models on your machine. It plans, edits files, and runs commands, asking before each change. One self-contained binary, 10 direct dependencies, no runtime to babysit — and a terminal UI built to feel instant: an incremental wrap cache with atomic frames, live autocomplete, GitHub-grade diffs, clickable files and links, and multimodal input straight from your clipboard.
npm install -g buildwithnexus # fetches the checksum-verified binary on first run
# or, with a Rust toolchain:
cargo install buildwithnexus --locked # installs `buildwithnexus` + the `bwn` alias
buildwithnexusThe first launch walks you through choosing a model, and setup finishes only once that model has answered. Then describe a task. What is sent to a hosted provider, and what stays on your machine, is on the data and privacy page.
A daily background check tells you when a new version is out;
buildwithnexus update installs it (buildwithnexus update --check exits 10
when one is available). Set auto_update: "install" in settings to apply
patch releases automatically (a new minor version is announced, not
installed; "install-any" installs those too), or "off" to silence the
check. The check reads BWN_UPDATE_REGISTRY, else npm's
npm_config_registry, else the public registry. The npm launcher keeps the
downloaded binary in ~/.buildwithnexus/bin/<version>/ (under NEXUS_HOME
if set), outside the npm package, so npm update does not delete it.
Requirements
- npm install: Node.js 18 or later.
- cargo install / source build: Rust 1.94 or later (
rust-versioninharness/Cargo.toml). - Prebuilt binaries: Linux x64 and arm64 (glibc 2.34 or later: Ubuntu
22.04+, Debian 12+, RHEL/Rocky/AlmaLinux 9+, Amazon Linux 2023, Fedora
35+), macOS x64 and arm64, Windows x64 (the C runtime is linked in, so
the Visual C++ Redistributable is not needed). Other platforms (for example
musl/Alpine or Windows on Arm) need a source build pointed to by
BWN_BIN. If the prebuilt binary cannot run, the first run says why (glibc too old, musl, or blocked by endpoint protection) instead of reporting it ready. - Optional tools:
ffmpegfor video attachments and image previews,bwrap(bubblewrap) for the Linux sandbox,tmuxfor background dev servers, andgit,rgandpython3for the tools that use them.buildwithnexus doctorreports what is missing and prints the command that installs it; bwn never installs them itself.
Try it in a sandbox
Want to kick the tires without touching your machine?
GitHub Codespaces — one click, in the browser.
Open a codespace on this repo
— the devcontainer preinstalls the binary, so when the terminal appears just
run bwn. Add ANTHROPIC_API_KEY or OPENAI_API_KEY as a Codespaces secret
(or export it in the terminal) to talk to a hosted model.
Docker — local and fully disposable. A scratch container that is deleted on exit, so nothing the agent does can reach your files:
docker run -it --rm -e BWN_ALLOW_BOOTSTRAP=1 -e ANTHROPIC_API_KEY \
node:22-slim npx -y buildwithnexusDrop --rm to keep the sandbox between runs, or add -v "$PWD":/work -w /work
once you're ready to let it loose on a real project.
The TUI
- Instant startup, never a black frame — chrome paints before anything else; dependency probes and connection warming run off the critical path.
- Fast rendering — each transcript line is wrapped once, repaints are frame-coalesced (~60fps) and wrapped in synchronized-output brackets, so streaming is smooth on kitty/iTerm2/WezTerm/Alacritty with zero tearing.
- Live autocomplete — type
/for a command popup with descriptions;@completes files,kb:symbols. ↑/↓ navigate, Tab/Enter accept. - Clean diffs — line-number gutters, background-tinted rows, changed-span highlighting, hunk elision. Same renderer for previews and applied changes.
- Clickable everything — file paths and links are OSC 8 hyperlinks: click a path in an edit header and it opens in your OS default app.
- Images you can actually see —
Ctrl+Vpastes a clipboard screenshot and it appears right there in the transcript before you send it: at real pixel resolution on kitty, Ghostty and WezTerm (kitty graphics protocol, Unicode placeholders — works through tmux), over Sixel on Windows Terminal 1.22+, foot, mlterm and xterm, as half-block art everywhere else. Previews re-fit when you resize the window. Drop a file onto the terminal and its path becomes an@attachmentwith the same preview.@clip.mp4runs ffmpeg to sample frames + metadata for vision models (text-only models get a clear "not multimodal" notice instead of silent drops). - Code that looks like code — fenced blocks are syntax-highlighted for 20+
languages by a zero-dependency lexer; markdown tables are drawn aligned,
with header rules and column alignment;
---, task lists and~~strikethrough~~render too. - Know what it's doing — the banner names the model and the server
answering it (
LM Studio (localhost:1234)); the footer shows the model, a spinner, elapsed time and streamed tokens/s while the agent works. Each tool call is one plain line, and the agent's todo list is a checklist that ticks off items. When a long turn ends, or an approval or a question is waiting, while you're in another window you get a desktop notification (OSC 99/777/9) and a taskbar progress state on terminals that have one. - Claude-Code-grade ergonomics —
Escinterrupts the agent; messages typed while it works queue and auto-send; ↑ history is prefix-filtered and never destroys your draft; double-click selects a word, triple-click a line, and every copy confirms itself in the footer via OSC 52 (terminals known to ignore OSC 52 get a one-time notice instead). - Prompts own the keyboard — while an approval, a question or a picker is
open, the input box shows it and keys go only to it.
Esc,Ctrl+C, orCtrl+Don an empty line cancels a question. Pickers filter as you type, a digit moves to a numbered row, and only Enter picks, and only a row that is shown.Ctrl+Con an empty prompt quits only on a second press within 2 s (Ctrl+Dquits at once; with workflows waiting it asks first). - What you type is what is sent — quotes, tabs and line breaks are kept;
a pasted line break shows as
↵in the input box. A paste over 1,000 characters or 10 lines shows as[pasted 20,024 chars]and is sent in full.@completes files by name anywhere in the project. A message that starts with an absolute path (a dropped screenshot) is sent with the image attached. - Themes and line mode —
dark,light(at least 4.5:1 contrast) andansi(the terminal's own 16 colours); thethemesetting defaults toauto, which follows the terminal's background, and/themeswitches and saves it.--plain(orTERM=dumb) is line mode: no alternate screen, no cursor addressing, no spinner frames, for screen readers and plain consoles. Narrow terminals wrap between words.
Why
The original buildwithnexus was a TypeScript CLI talking to a Python /
LangGraph backend over HTTP. This is a ground-up rewrite that keeps the benefits
of that engine — planning, a ReAct tool loop, approval gates, role-specialized
agents — as plain Rust control flow, with none of the framework weight. No
Python, no Docker, no tunnel. The orchestration that LangGraph did at runtime is
just code here, which is where the speed comes from.
Design bias, in order: performance, then fewer lines, then fewer
dependencies — never at the cost of the UX. Enums and match over trait
objects; flat data tables over registries; one pooled HTTP connection reused
across every step of the agent loop.
The speed is measured: 2 ms full-process startup, a 4.6 MiB resident TUI, and 3.6 µs to render a streamed chunk into a 2,000-line transcript. Every number and how to regenerate it: BENCHMARKS.md.
Package history
If you browse the npm version history you'll see the same name carrying earlier, unrelated architectures — that's expected, not a hijack:
| npm versions | what they were | |---|---| | 0.1.x – 0.7.x | a VM-isolation "runtime" experiment (QEMU/Docker era) | | 0.8.x | the TypeScript orchestrator with the Python/LangGraph backend | | 0.10.1 and later | this codebase — the ground-up Rust CLI (0.10 inline UI, 0.11+ full-screen TUI) |
The pre-0.10 versions share nothing with the current code and aren't
maintained; install latest. The Rust line is also the only one published
to crates.io.
Models
Three wire protocols — Anthropic Messages, OpenAI chat completions, and native
Ollama — cover everything. Pick a provider during setup (or bwn init):
| Provider | Kind | Key |
|----------|------|-----|
| Anthropic (Claude) | remote | ANTHROPIC_API_KEY |
| OpenAI | remote | OPENAI_API_KEY |
| OpenRouter | remote | OPENROUTER_API_KEY |
| Groq | remote | GROQ_API_KEY |
| Hugging Face | remote | HF_TOKEN |
| Ollama | local | — |
| llama.cpp server | local | — |
| LM Studio | local | — |
Env vars override the stored key, so CI and one-offs Just Work. Keys live in
~/.buildwithnexus/.env.keys (0600). A key is typed hidden (dots, then only
the masked key) and saved only after the provider accepts it. If a key is
rejected later, /login in a session (or buildwithnexus login) replaces it
in place, checked first; the /model picker marks a key whose last check
failed key rejected (recorded as a hash in
~/.buildwithnexus/key-checks.json, never the key).
Local models. No key is needed. The Ollama app and its Linux service
start the server for you; without them (WSL without systemd, for example),
run ollama serve in a second terminal. Check that it answers, then pick it
in bwn init, or pass --provider on a headless run. These lines work the
same in bash, zsh and PowerShell:
ollama list # lists your models; "could not connect" means no server
ollama pull llama3.2 # the default Ollama model
bwn init # choose Ollama
bwn run --provider ollama --model llama3.2 "summarize this repo"Default endpoints: Ollama http://localhost:11434, llama.cpp server
http://localhost:8080/v1, LM Studio http://localhost:1234/v1. For any
other OpenAI-compatible server (vLLM, TGI, LiteLLM, a gateway), choose the
custom provider (default http://localhost:8000/v1); setup, /model and
/login ask for its key if it needs one. A custom endpoint's key is saved
for that endpoint only (CUSTOM_API_KEY@<scheme://host[:port]> in
.env.keys, the port left off when it is the scheme's default), filed under
the host the request actually reaches, so /model to a new address asks for
that address's own key (Enter for none) and never sends another server's.
buildwithnexus login --provider custom --base-url <url> saves the key for
that address. CUSTOM_API_KEY in the environment is the key of the custom
endpoint a run starts on; a /model to another address does not get it. The
single CUSTOM_API_KEY an earlier version saved moves, the first time 0.15
reads it, to the custom endpoint in your own settings; with none it waits as
CUSTOM_API_KEY@unbound, is never sent, and /model and setup offer it
(send it to <host>? [y/N]) at the next new endpoint.
To change where any provider connects, set base_url in
~/.buildwithnexus/settings.json:
{ "provider": "ollama", "model": "qwen2.5-coder", "base_url": "http://gpu-box:11434" }--base-url <url> does the same for one run (with --provider custom it
reads CUSTOM_API_KEY from the environment). /model and setup remember the
address last used with each provider (endpoints in your own
~/.buildwithnexus/settings.json, never a project's), so switching back to a
provider, or --provider on a headless run, returns to that server.
/model http://host:11434 <model> picks Ollama's native API; a URL ending in
/v1 is a custom OpenAI-compatible endpoint. provider, model and
permission may be left out of a settings file: an empty model means the
preset's default.
A configured key is only sent over HTTPS or to a loopback address.
Setup, /login and /model share one key question. It refuses an answer that
cannot be a key without sending it: one with spaces (pasted line breaks count
as spaces), a leading /, only digits, a provider name, or more than 4096
characters. A /command closes it (in setup it stops setup). To use a key that
really has spaces, set it in the environment variable or .env.keys. For an
endpoint on plain http on another machine bwn does not ask for a key: it
explains the https and ssh -L options. A keyed OpenAI-compatible gateway
lists its models once the key works.
Local servers. llama.cpp, LM Studio and vLLM report their context window,
and bwn uses it (context_tokens in settings fixes a value); /context shows
the total and what fills it (system prompt, tools, MCP tools, conversation,
images). A message bigger than a known window is refused before it is sent.
Whether a model takes images follows what the server reports (Ollama
capabilities, LM Studio model type, llama.cpp modalities); "vision": true or
false decides outright. /local lists the configured server and every local
preset with its models, then the GGUF files on disk. A busy server (429 or
5xx) is retried 3 times (BWN_MAX_RETRIES=0…20), a model that is still
loading for about two minutes. Errors say what happened and what to do, with
the server's own message underneath (nothing is answering at <host>, a
missing Ollama model with its ollama pull command).
/context and /teamwork say when a local or custom endpoint did not report
its window (bwn assumes 8k; context_tokens changes it).
Reasoning. reasoning_effort in settings (off by default, or low / medium /
high; --effort <level> per run, /effort in-session) maps to each API's
native control: Claude 4.6+ gets adaptive thinking with output_config.effort,
older Claude models a budget_tokens thinking budget (2048 / 8192 / 16384),
OpenAI reasoning models (o1/o3/o4/gpt-5) reasoning_effort, and
Ollama's native API think: true (an Ollama model that does not support
thinking is retried without it). Other models — including anything behind a
local OpenAI-compatible server — receive no reasoning parameters at all.
Cost. Every request's usage block feeds a session ledger: /cost shows
input / output / cache-read / cache-write tokens, the request count, and an
estimated dollar figure from a built-in price table (local providers show
$0.00 (local); an unlisted model shows tokens only, never a guessed price).
--max-budget-usd <n> (or max_budget_usd in settings) stops the agent
before the next model request once the estimate exceeds n, with a notice
event in --json mode; a headless run then exits 5. With a cap set, a remote
model that has no known price stops before the first request (exit 2): give
it a price in USD per million tokens under prices, which wins over the
built-in table (cache_read and cache_write default to input):
{ "prices": { "my-gateway-model": { "input": 3.0, "output": 15.0 },
"llama3.1:70b": { "input": 0, "output": 0 } } }Modes
- PLAN — decompose the task into steps you approve, edit (Edit Step: pick a step and change its text in the input box) or push back on (Revise Plan: say what to change and get a revised plan), then execute.
- BUILD — the agentic ReAct loop: read/edit files, run commands, iterate.
- BRAINSTORM — free-form chat with read-only tools (read, grep, fetch, read-only commands); never writes. A task typed here gets a hint to switch; the mode changes only when you change it (Shift+Tab,
/mode, or bare/plan,/build,/brainstorm).
All three modes are one conversation: each turn sees the earlier ones, the
whole conversation is saved as the session, and /context counts it. /new
starts a fresh one.
buildwithnexus # full-screen interactive session
buildwithnexus run <task> # execute a task (agentic, headless)
buildwithnexus plan <task> # decompose, approve, then execute
buildwithnexus brainstorm <q> # free-form chat (read-only tools)
buildwithnexus continue # reopen this folder's latest session
buildwithnexus resume <id> # reopen a session (bwn sessions lists them)
buildwithnexus init # (re)configure provider / model / key
buildwithnexus login # replace the provider's API key, checked first
buildwithnexus providers # list built-in providers
buildwithnexus doctor # diagnose setup (keys, tools, connectivity)
buildwithnexus review # read-only review of your changes (exit 9: blocking)Inside the interactive session:
/model [name] hot-swap the AI model mid-session
/login replace the API key (checked before it is saved)
/effort [off|low|medium|high] show or set reasoning depth (persisted to settings)
/permissions [mode|default <mode>|list|remove <entry>|reset] see Permissions
/theme [dark|light|ansi|auto] colour theme (saved)
/local local servers, their models, and GGUF files on disk
/compact compress context (free up token budget)
/context context window usage and what fills it
/cost session tokens by category, request count, estimated cost
/diff [turn] changed and new files, one summary, a file's diff; turn: the last turn's changes
/review [--base <ref>|--staged] [focus] read-only review of uncommitted, staged or branch changes
/commit AI-drafted commit message; commits only after [c]ommit
/pr AI-drafted pull request title + description
/undo [latest|git|all|<id>] revert the last agent turn's edits (asks before overwriting yours)
/rewind go back to an earlier prompt: code, conversation, or both
/checkpoints tasks and files /undo can restore
/resume pick a saved session (this folder first; type to filter)
/rename <name> name this session
/export [file] this conversation as Markdown (default ~/.buildwithnexus/exports/<id>.md)
/copy the last answer to the clipboard (OSC 52)
/ask <question> a side question that is not added to the conversation
/init run setup, then offer to write AGENTS.md from this repository
/agents helper agents from agent files, then Agents.md
/add-dir [path] also work in another folder for this session (no path: list them)
/schedule <delay> <task> run a task once in the background (5s, 2m, 1h)
/loop <interval> <task> run a task repeatedly in the background (up to max_concurrent_workflows at once, default 2)
/workflows list and manage background workflows (i<id> shows a run's log, kept in ~/.buildwithnexus/workflows/)
/btw <context> add a note to your next message
/config configure hooks, memory, and commands via AI
/memory view and edit session memory
/skills list skills and custom commands
/trace inspect hooks, tools, skills, and subagentsBackground workflows (/schedule, /loop) run under the session's
permission, provider and model. Nobody can answer an approval for them, so
outside auto a change they try is refused: a refused /loop run stops the
loop with ✗ workflow #N blocked: …. Use /permissions auto in the session
that schedules a workflow that should edit unattended. Bare /loop,
/schedule or /btw print their usage; /help lists every command with
its arguments, the keys, and the answers to an approval prompt, and the /
popup and Tab complete the same commands and their subcommands
(/permissions Tab offers the modes and default, list, remove,
reset). buildwithnexus --help lists every command, subcommand and option.
Both link what leaves your machine.
Sessions and undo
Every conversation is saved as it runs (~/.buildwithnexus/sessions/) and
belongs to the folder it started in.
bwn continue # reopen this folder's latest session in the UI
bwn continue "now add tests" # or run one more task on it, headless
bwn resume <id> # reopen a session; bwn resume alone opens the picker
bwn sessions # this folder first, with age and message count
bwn sessions export <id> # the conversation as Markdown
bwn sessions rm <id> # delete a saved sessionbwn -c is bwn continue. With no session in this folder, continue takes
the latest one anywhere and says so. In a session, /resume picks a saved
session (this folder first; type words to filter), /rename <name> names the
current one, and /new starts a fresh one. /export [file] writes the
conversation as Markdown, /copy puts the last answer on the clipboard
(OSC 52, so it works over SSH), and /ask <question> answers a side question
from the conversation without adding it. /btw <note> adds a note to your
next message.
Every file write is checkpointed first. Bare /undo reverts the last agent
turn's edits, /undo all everything from the last 24 hours, and
/undo <id> one checkpoint from /checkpoints. When a file has changed since
the agent edited it, /undo asks <file> changed after the agent edited it —
overwrite your changes? [y/N], and n keeps your version while the rest is
restored. /undo also says what it cannot undo: changes made by shell
commands, commits, and files too large to snapshot (the approval prompt warns
about those too). /rewind (or Esc Esc on an empty input box) goes back to
an earlier prompt and restores the code, the conversation, or both; the prompt
comes back in the input box to edit and send again. /diff lists every changed and new file
with one summary line and shows a chosen file's diff; /diff turn shows what
the last turn changed. /commit drafts a message and commits only when you
answer c ([c]ommit · [e]dit · [n]o). Each folder keeps its newest 500
checkpoints. RECOVERY.md has the details.
Headless and CI
run, plan, brainstorm, continue and resume run without the TUI. With
no settings file, a headless run uses --provider, or the first provider
whose API key is in the environment, and writes nothing.
| Flag | Effect |
|---|---|
| --provider <id> | provider for this run (buildwithnexus providers lists ids) |
| --model <name> | model for this run |
| --base-url <url> | model endpoint for this run, such as a gateway (--provider custom reads CUSTOM_API_KEY) |
| --permission-mode <mode> | ask, accept-edits, auto or readonly (see Permissions); with no terminal, ask blocks every change, so CI usually wants auto or readonly. An unknown name exits 2. |
| --sandbox <off\|auto\|require> | OS sandbox for shell commands (see Sandbox) |
| --effort <off\|low\|medium\|high> | reasoning depth |
| --max-budget-usd <n> | stop before the next request once the estimated cost exceeds n |
| --json | machine-readable events on stdout instead of text |
| --yes, -y | plan only: approve the plan and execute it |
| --legacy-exit-codes | exit 0 when a run stops short without failing (codes 4 to 8, and 3 or 1 for refused or unrun calls, below) |
| --trust-project <digest> | trust exactly this content of the folder's project settings for this run (also BWN_TRUST_PROJECT; buildwithnexus trust --print prints the digest) |
| --trust-project-allow <keys> | let that digest also trust base_url and/or permission from the project settings, e.g. base_url,permission (also BWN_TRUST_PROJECT_ALLOW) |
| --worktree <name> | work in .bwn/worktrees/<name> on branch bwn/<name> (created from HEAD, or reused); on exit bwn prints the branch and git merge bwn/<name> |
| --add-dir <path> | also read and change files in <path>; repeatable (see More folders). A missing folder, a file, / or a folder holding your home exits 2. |
| --plain | line mode for the terminal UI: no alternate screen or cursor addressing (as with TERM=dumb) |
| -- | everything after it is task text, even if it looks like a flag |
An unknown option anywhere before -- is a usage error (exit 2) with a
"did you mean" hint, and nothing is sent.
Piped input. When stdin is not a terminal, run, plan and
brainstorm read it: with no task argument it is the task, and with one it
is added after the task as a [stdin] block (up to 1 MiB; the rest is cut
with a notice). With a task argument, a pipe that sends nothing for 3 s is
ignored. No task at all exits 2 before any request; pass </dev/null to keep
stdin out.
git diff | bwn run --permission-mode readonly "review this diff for bugs"| Exit code | outcome | Meaning |
|---|---|---|
| 0 | success | the task finished |
| 1 | failed | the run failed, no provider could be set up, or the turn ended right after a call that could not run |
| 2 | | usage error: unknown option or --provider, a bad --permission-mode or --effort, a flag missing its value, no task, plan with no terminal and no --yes, a spend cap on a model with no known price, a repository command that is not trusted (bwn run '/deploy'), or words after acp |
| 3 | approval_blocked | changes were blocked for lack of approval (ask or accept-edits with no terminal; the closing line names them), or a hook, a deny rule or read-only mode refused a call (changes were denied: …). A check_work round nobody could approve is not a blocked change: the run says the checks were not run. |
| 4 | hook_blocked | a UserPromptSubmit hook blocked the task |
| 5 | budget_stop | --max-budget-usd stopped the run before the next request |
| 6 | step_limit | the turn used every step without finishing |
| 7 | check_work_failed | the model finished, but the project's checks (check_work) still fail |
| 8 | verification_failed | the model finished, but the verifier still blocks after its fix rounds |
| 9 | review_blocking | buildwithnexus review found a blocking issue |
| 130, 143 | interrupted | SIGINT or SIGTERM; the session is saved and the line names it |
With --json, the last event is the result event: outcome, exit_code,
session_id, turns, tokens_in, tokens_out, cost_usd, denied and
denials (the refused calls, each with its reason). When more than one
applies, the first reason the run stopped short is reported.
--legacy-exit-codes (or BWN_LEGACY_EXIT_CODES=1) restores the pre-0.15
behavior: codes 4 to 8, and 3 or 1 for refused or unrun calls, become 0,
while the result event still names the outcome. buildwithnexus update
--check exits 10 when a newer release exists, and buildwithnexus doctor
exits 1 when a check fails (--json doctor prints one check event per
check; --json sessions one session event per saved session).
buildwithnexus mcp login and logout exit 1 when the sign-in or sign-out
fails and 2 on a usage mistake.
Reviews in CI. buildwithnexus review [--base <ref> | --staged] [focus]
reviews uncommitted changes (plus the branch since <ref> with --base, or
only staged changes with --staged), including new files git does not track
yet; key and credential files are named but never sent. It is read-only in
every permission mode, prints a finding event per issue with --json, and
exits 9 when one is blocking. /review takes the same arguments in a session.
Trusting a repository in CI. Project hooks, MCP servers, allow rules and
the repository's commands, skills and agents need folder trust, and CI has
nobody to answer the prompt. Run buildwithnexus trust --print in the
checkout to see what the project settings run, the commands, skills and
agents it carries, and their digest, then pass --trust-project <digest> (or
set BWN_TRUST_PROJECT): if the files change, the run stops with exit 2 and
names them. Settings that set base_url (where your requests and API key go)
or permission are trusted only when those keys are named too, with
--trust-project-allow base_url,permission (or BWN_TRUST_PROJECT_ALLOW), as
the terminal asks about them on their own; trust --print prints the line.
Custom commands headless. bwn run '/deploy staging' runs a custom command
or skill with its arguments, as in a session. A command from the repository
that is not trusted exits 2 and says how to trust it.
Each --json event has a schema_version (now 1). What may change in a
minor or a patch release (flags, settings keys, events, session files, exit
codes) is in docs/VERSIONING.md.
ANTHROPIC_API_KEY=... bwn run --permission-mode auto --json "fix the failing test"On npm installs in CI, add --bootstrap or set BWN_ALLOW_BOOTSTRAP=1 so the
launcher can fetch the binary on first run.
GitHub Actions. The repository is also an action: it installs bwn from
npm, runs run or review with --json, turns the outcome into the step's
result with annotations (review findings land on their lines), uploads the
event log as an artifact and, with comment: true, posts the summary on the
pull request, updating the same comment on later pushes.
examples/github/bwn-review.yml reviews
every pull request:
- uses: Garretts-Apps/[email protected]
with:
command: review # or run, with prompt: <task>
review-base: origin/${{ github.base_ref }}
provider: anthropic
max-budget-usd: "1"
comment: "true" # needs pull-requests: write
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}permission-mode defaults to readonly. Exit 0 passes the step; every other
exit code fails it with an error annotation naming the outcome, unless the
outcome is listed in allow-outcomes (for example budget_stop,step_limit),
which makes it a warning. The outputs are outcome, exit-code, passed,
session-id, cost-usd, findings, summary and events (the log's path).
install takes a version, an npm package spec or a tarball from npm pack
(empty: the action's own version), and BWN_BIN in the step's environment
runs a binary you built instead. Inputs reach the scripts as environment
variables, never as script text, and the token is passed only to the comment
step. All inputs are in action.yml.
Permissions
Every mutating tool (write_file, edit_file, run_command) passes a gate:
ask (default), accept-edits, auto (yolo), or readonly. Set it during
setup, with --permission-mode, or with /permissions. In readonly,
mutations are refused outright — never prompted — so an approved
sensitive-path or dangerous-command confirmation can't slip one through.
accept-edits applies file edits inside the project without asking, while
commands, deletions, network access and anything inside .git still ask.
Claude Code's names are accepted too (acceptEdits, default for ask,
plan and dontAsk for readonly, bypassPermissions for auto); an unknown
name is an error, and a misspelt permission setting warns and uses ask.
A switch made in a session (/permissions auto, or typing "use auto") lasts
for that session. /permissions default <mode>, or "save as default" in the
/permissions picker, saves it for every new session. /permissions list
(and the bare picker) shows the saved approvals and the rules in force;
/permissions remove <entry> forgets one approval.
The prompt shows the whole command, with line breaks marked ⏎, and names
what s / a would allow from then on: a binary (cargo), a subcommand
(git status), a host, or, for shells, interpreters and other programs that
run what they are given (sh, python3, python3.12, node, awk, sed,
env, fakeroot, …), and for programs whose arguments decide what they destroy or stop
(rm, mv, cp, ln, chmod, chown, dd, truncate, kill, pkill,
killall, del, robocopy, …) or git commands that discard or rewrite
(git rm, git clean, git checkout, git restore, git reset --hard,
git push --force, git branch -D, git stash drop, git filter-branch,
git reflog expire, git submodule deinit, git gc --prune, …), only that exact command. Answering y
allows the call once; d <reason> refuses it and tells the model why; Esc
or Ctrl+C refuses it and stops the turn. Answering a (always allow) remembers it
for the current project only (project_allowed in
~/.buildwithnexus/settings.json, keyed by directory). /permissions reset
forgets those answers for the project you're in. The legacy global
allowed_commands list keeps working, except for a shell or interpreter saved
by name alone (python3, as 0.14.3–0.14.8 stored them): those are ignored,
and bwn lists them at startup and in /permissions.
Network tools (fetch_url, web_search, headless_browser,
wait_for_url, open_browser) ask once per host and port in every mode but
auto, readonly included, since a fetch or a search query can carry data
out or reach services on your network. web_search sends its query to
lite.duckduckgo.com. s / a allow that host; an allowed_commands entry
"fetch *" allows every host.
Commands the agent runs (including check_work and start_server, sandboxed
or not) do not inherit provider keys: variables ending in _API_KEY or
_API_TOKEN, and the presets' key variables such as HF_TOKEN, are removed
from their environment. List any your build needs in
"shell_env_passthrough": ["MAPS_API_KEY"]. Hooks are your own and keep
their environment.
More folders
--add-dir <path> (repeatable) or /add-dir <path> adds a folder to work
in besides the one you started in, for the rest of the session. The file
tools may change files there under the same permission mode (accept-edits
included), searches without a folder of their own (find_files,
grep_files, find_paths) and @ completion cover it, the sandbox binds
it writable, and helpers and background workflows get it too. The footer
shows +N dirs, and /add-dir alone lists them.
What stays the same: sensitive paths there still ask in every mode, a link
inside the folder that leads out of it is outside, and its .git stays
read-only to sandboxed commands and asks in accept-edits. Folder trust
does not extend to it: its settings, hooks, commands, skills and agent
files never load. Its AGENTS.md (or the first instruction_files name at
its top) is sent to the model, after a notice names it, as instructions
for that folder. The filesystem root, a folder that holds your home folder,
bwn's own folder and credential stores cannot be added. A resumed session
starts without the folders; add them again.
Allow, ask and deny rules
permissions and network in settings.json set rules that apply before
the mode: a deny rule refuses in every mode, an ask rule always prompts (even
in auto or after an a answer), and an allow rule skips the prompt. Deny
beats ask, ask beats allow, and all three beat the mode.
{
"permissions": {
"allow": ["run_command(cargo test*)", "write_file(src/**)"],
"ask": ["write_file(migrations/**)"],
"deny": ["run_command(git push*)", "WebFetch(domain:pastebin.com)"]
},
"network": { "allow": ["docs.rs", "*.github.com"], "deny": ["*.internal.example"] }
}A rule is Tool or Tool(pattern). The tool part is matched like a hook
matcher, so Claude Code names work (Bash, Edit, Write, WebFetch), and
a rule for run_command, bash or Bash also covers the commands of
check_work and start_server. The pattern is a */? wildcard matched
against the command for shell tools (Claude Code's git push:* means
git push*), the host for network tools (domain: is optional), the query
for web_search, and the touched paths for file tools: project-relative
(migrations/**), or absolute or ~/ for any path. An allow rule must cover
a whole plain command (no chaining or redirection) and every path; ask and
deny rules match any part of a compound command and any path. Ask and deny
rules also see the command behind a wrapper (env, sudo, nice,
command, time, timeout, xargs, exec, eval, find -exec,
fakeroot, firejail, bwrap, torsocks, proxychains, numactl,
chronic, run0, sg, ssh-agent, systemd-inhibit, uv/poetry/pipenv
run, bundle exec, direnv/mise exec, nix-shell --run, FOO=1,
/usr/bin/git), inside sh -c '…', bash -lc "…", cmd /c,
pwsh -Command, $(…) and backquotes, past git's own options (-C, -c,
--git-dir, --work-tree, --attr-source, --shallow-file), through an
alias given with -c alias.<name>=… and into git's git-<command>
programs, so
run_command(git push*) also refuses git -C . push and
sudo sh -c 'git push'. An alias saved in git's config is not seen. A deny rule also refuses a pipeline or compound
command that names its program anywhere (make && git status under
run_command(git push*)), since such a command can build what it runs from
parts, and so does code handed to an interpreter (perl -e 'system "git push"')
or a word in bash's $'…' quoting; the refusal says to run that command on
its own. On Windows the rules read a command as cmd.exe passes it on
(g^it push is git push), and a command with ^ or % counts as
compound. Rules are a guard against mistakes, not a sandbox: a program the
list of wrappers does not know, or a script file, can still run what a rule
names. network
entries are host patterns (example.com, *.example.com) for the network
tools and http hooks. Rules and saved approvals see the host a URL
reaches: percent-escapes decoded, a trailing dot dropped and a default port
written out (:443 on https) left off, so https://%6Cite.example.:443/ is
lite.example, and an IP address in any spelling (2130706433, 0x7f.1,
[::ffff:7f00:1]) is the dotted address it reaches, 127.0.0.1. Names are
not resolved: to refuse this machine, list localhost as well as
127.0.0.1 and ::1. A refusal names the rule and whether it came from user or project
settings, and a headless run refused by one exits 3.
Rules add up across ~/.buildwithnexus/settings.json, settings.local.json
and the project's .buildwithnexus/settings.json, so a project cannot drop
your deny rules. A project file adds ask and deny rules on its own, and allow
rules only once you trust the folder.
Headless plan needs a terminal to approve the plan; pass --yes / -y to
auto-approve and execute (in --json mode the plan is emitted as a plan event
first). Without a terminal and without --yes it exits 2 immediately.
Sandbox
An optional OS-level layer under the permission gate for shell commands
(run_command/bash, !cmd, and check_work). Settings key "sandbox"
(or --sandbox <mode>, or /sandbox <mode> in a session):
"off"(default) — commands run as today."auto"— confine commands when a backend works; otherwise run them unsandboxed and say so once per session."require"— refuse to run commands when no backend works.
Backends are external binaries, probed once per session: bwrap (bubblewrap)
on Linux and sandbox-exec (Seatbelt) on macOS. Windows and WSL have none.
Confined: filesystem writes anywhere except the working directory and the
temp dirs. On Linux, /tmp is a fresh private tmpfs (with TMPDIR=/tmp)
that is discarded when the command exits, so files written there are not
visible to the host or to the next command. On macOS, /tmp and $TMPDIR
are the real, shared directories. Everything else, including
~/.buildwithnexus and tool caches such as ~/.cargo or ~/.npm, is
read-only to the command. Set "sandbox_network": false to also block the
network inside the sandbox (default true, allowed).
Not confined: reads (the whole filesystem stays visible), the agent's own
file tools (already fenced to the working directory), hooks, MCP servers, and
start_server. The sandbox never approves anything — the permission gate is
unchanged; it only limits what an approved command can touch. Sandboxed
commands are marked [sandboxed] on the tool header; /sandbox status and
buildwithnexus doctor show backend availability and whether commands would
be confined. To escape for one command, switch with /sandbox off and back.
Inline images
Attach an image three ways — Ctrl+V with a screenshot on the clipboard,
drag a file onto the terminal (or paste its path), or type @shot.png — and
it is drawn inside the transcript, above the composer, before you send it:
| Terminal | What you see |
|---|---|
| kitty ≥ 0.28, Ghostty, WezTerm (placeholder support) | the real pixels, at up to the full width of the window, scrolling with the text; works inside tmux with set -g allow-passthrough on |
| Sixel terminals: Windows Terminal 1.22+, WezTerm, foot, mlterm, xterm -ti vt340 | the real pixels, detected at startup; needs ffmpeg on PATH |
| any other truecolor terminal (iTerm2, Alacritty, older Windows Terminal, VS Code…) | half-block art (needs ffmpeg on PATH to decode) |
| NO_COLOR, non-truecolor | no preview, never garbage |
A pasted PNG needs no external tool at all. JPEG, WebP, GIF and a video's
first frame are converted with ffmpeg when it is installed. Control it with
the images settings key ("auto" default, "kitty", "sixel", "blocks",
"off") or BWN_IMAGES=… for one run. Sixel and block previews take at most
half the window's width and a third of its height, and re-fit when the
window is resized. Uploads are freed when you /clear or exit.
Pictures, PDFs and screenshots the agent reads
The model can look at pictures too, when it takes images (see vision under
Models). read_file on a PNG, JPEG, GIF or WebP (up to 5 MB)
returns the picture: Anthropic gets it inside the tool result, and
OpenAI-compatible servers and Ollama get it in a user message right after
the tool results. A text-only model is told why it sees none, and the
transcript shows what the picture was (image shot.png (image/png,
1280x800, …)). read_file on a PDF returns its text through
pdftotext from poppler when that is installed (apt install
poppler-utils, brew install poppler), and otherwise says what to install;
a line range works as for any file.
screenshot_url opens a page served on this machine
(http://localhost:3000, a dev server the agent started) in a local
headless Chrome, Chromium or Edge and shows the model the picture (width
and height optional, default 1280×800). The browser is BWN_CHROME when
set, else one found on PATH, in the usual install folders, or a Playwright
build. It runs with a throwaway profile and without provider keys in its
environment. Only loopback addresses are opened unless network.allow (or
an allow rule for screenshot_url) names the host, and outside auto each
host is approved like a fetch. Every request the page or the browser makes
to any other host, link-local and cloud-metadata addresses included, goes to
a local proxy that refuses it, and the result lists the hosts the page
tried. The tool is offered only to models that take images.
"vision": false in settings.json makes bwn treat the model as text-only:
read_file then returns a notice instead of a picture and screenshot_url
is not offered. Pictures a tool returned are kept in the session file, like
attached images.
A related setting: notify ("auto" — desktop notification when a turn of
8 s or longer ends, or an approval or a question is waiting, while the window
is unfocused; "always"; "off").
Hooks
Run your own commands at the same lifecycle points as Claude Code, configured in
~/.buildwithnexus/settings.json (user) and/or .buildwithnexus/settings.json
(project). User hooks are always active; project hooks run only after you trust
that folder (you're prompted once, and a project hook may deny a tool but
never grant one — so cloning a hostile repo can't run or unlock anything).
The prompt shows each hook's command line and each project MCP server's
command and arguments. Trusting also pins the project files those commands
run: scripts they name (./x.sh, or a bare setup that sh or cmd.exe
would find in the folder), and package.json or the Makefile when they
call npm, make and the like, in the project root or in a folder the
command names (make -C sub, cd web && npm test). Editing one of those
asks again, and the check is repeated before every run of a project hook: a
script that changed since you trusted it is asked about
(scripts/fmt.sh changed since you trusted it — run it?), or skipped with a
warning in a headless run. The prompt is one screen with one question; when
the project sets base_url (where your requests and key go) or permission,
e trusts everything except those. A later prompt names the file that
changed since you trusted the folder.
For CI, see Trusting a repository in CI under Headless and CI.
Events: SessionStart / SessionEnd (once per process), UserPromptSubmit,
PrePrompt (before each model request in a BUILD turn), PreToolUse,
PermissionRequest (where bwn is about to ask you to approve a call),
PostToolUse, PostResponse, OnError, Stop (after every BUILD, PLAN,
BRAINSTORM, or chat response), SubagentStop (when a spawn_subagent call
returns; its payload carries the subagent's tool_input), PreCompact (before
the conversation is summarized; trigger is auto, or manual for
/compact), and Notification (where bwn raises a desktop notification,
whatever the notify setting and window focus; notification_type is
permission_prompt, question, done when a turn ends, or idle_prompt when
the prompt has waited idle_notify_secs, default 60, 0 for never). A
PreCompact or Notification matcher matches the trigger or the type. Each
hook receives the event as JSON on stdin with Claude Code's field names:
hook_event_name, session_id (the id the transcript is saved under),
transcript_path, permission_mode (ask | accept-edits | auto | readonly), cwd, plus
the event's own fields (tool_name, tool_input, tool_response, prompt,
stop_hook_active, trigger, notification_type, message).
PreToolUse can gate a tool: exit code 2 (or a JSON
permissionDecision: "deny") blocks it — even under auto. "allow" skips the
prompt; otherwise the normal gate applies. A PreToolUse hook that gives no
answer also blocks the call, with a message naming it: one that times out
("timeout" in seconds, default 10), cannot start (missing script or
interpreter, not executable, a .rs hook that does not compile) or is killed by
a signal. Any other non-zero exit is shown with the end of its stderr and
does not block, unless the hook sets "on_error": "deny", which makes a
crashing guard block the call. Other events never block on a failed hook, but
the failure is shown. Matchers are *, an exact tool name, or a
|-separated list; each segment may use * and ? wildcards ("*_file",
"mcp__*", or Claude Code's "mcp__.*"). Claude Code tool names stand for
the bwn tools that do the same thing: Bash (run_command, bash,
start_server), Edit, MultiEdit, Write, Read, Grep, Glob, LS,
WebFetch, WebSearch, Task and TodoWrite, so a matcher copied from a
Claude Code settings file guards the same calls. Edit and Write also cover
multi_edit, remove_path, move_path and create_dir, and a matcher
naming the shell (Bash, run_command, bash) also runs for check_work
and start_server calls that carry a command. tool_input carries Claude
Code's field names beside bwn's, with the values the tool will use:
file_path (absolute), old_string, new_string, content, prompt,
subagent_type, glob, path and todos. A call that touches several
files (move_path, read_many_files, apply_patch) is shown to
PreToolUse once per file, and PostToolUse sees a move's destination. The
legacy mcp_call tool is matched, ruled and approved as the
mcp__<server>__<tool> it calls. A * or empty matcher does
not run on finish and exit_plan; name them to guard them. A hook under an
event bwn does not fire, of an unknown type, or without its command is
reported at startup and by doctor instead of being ignored. See
examples/settings.json.
Other events answer as Claude Code's do. What a PostToolUse hook says
reaches the model with the call's result (the call already ran):
{"decision": "block", "reason": …},
{"hookSpecificOutput": {"additionalContext": …}}, or its stderr with exit 2.
A Stop or SubagentStop hook that exits 2 or answers
{"decision": "block", "reason": …} keeps the agent (or the helper) going,
with the reason as the next message, at most 3 rounds in a row;
stop_hook_active is true while it does. It never sends on a turn you
stopped, and in PLAN the plan waits for your approval instead.
PermissionRequest may answer for you with
{"hookSpecificOutput": {"decision": {"behavior": "allow" | "deny" | "ask", "message": …}}}
(or {"decision": …}), and exit 2 denies. It also runs in headless runs,
where nobody could answer the prompt; only your own hooks can allow.
A hook's type is command (a shell command line), python or script (a
file; the interpreter follows the extension), or http:
{"type": "http", "url": "https://…", "headers": {"Authorization": "…"}}
POSTs the payload as JSON through bwn's HTTP client (proxy and certificate
settings apply) within the hook's timeout, following no redirects. The
response body counts as the hook's output, and a status outside 2xx as a
failed hook (reported as answered HTTP <status>). A host that network.deny
names is refused. The trust prompt shows a project's http hook URLs (header
names only). A header value can take a secret from the environment:
"Authorization": "Bearer $HOOK_TOKEN" with "allowed_env_vars": ["HOOK_TOKEN"]
fills in only the variables that list names, so a repository's settings cannot
read the rest of your environment.
{
"hooks": {
"PreToolUse": [
{ "matcher": "run_command",
"hooks": [{ "type": "command", "command": "echo 'no shell on main' >&2; exit 2" }] },
{ "matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "./scripts/guard.sh", "on_error": "deny" }] }
]
}
}Project instructions (AGENTS.md / CLAUDE.md)
The same instruction files other coding agents read are loaded into every
system prompt — BUILD, PLAN, BRAINSTORM, quick chat replies, sub-agents, and
headless --json runs. Discovery order (most general first, later files take
precedence):
~/.buildwithnexus/AGENTS.md(global, optional)- every directory from the git root (or filesystem root) down to the cwd:
AGENTS.md, elseCLAUDE.md(so a repo shipping both isn't loaded twice), then.buildwithnexus/AGENTS.md
Each file is capped at 32 KiB (cut with a visible marker) and 96 KiB in
total. A dim line at startup lists what was found. The repository's own files
are asked about once per folder and content: instructions from this repo:
AGENTS.md, src/AGENTS.md, then use them? y yes · n no · r review [N]. Only
y uses them (and is remembered); Enter, n or Esc keeps them out of the
prompt for this session and asks again next launch, and r shows them. A task
typed while the question is up is not an answer: it waits in the input box.
After a y the line just names them, until a file changes. A headless run, or a session with prompts piped in, cannot ask, so until
then it prints one line (a headless run on stderr): instructions from this repo: AGENTS.md (not reviewed — …).
/init offers to write AGENTS.md from the repository's own build and test
files (or to improve the one there), shown as a diff you approve;
buildwithnexus init --agents-md does the same headless.
The instruction_files settings key changes which names are looked up
(default ["AGENTS.md", "CLAUDE.md"]; add "GEMINI.md", or [] to disable).
The mixed-case .buildwithnexus/Agents.md is different: it defines agent
roles/capabilities (/agents shows it) and is loaded after the instructions.
~/.buildwithnexus/system.md adds your own text to every system prompt. A
project's .buildwithnexus/system.md is used only once you trust that folder
(it is listed in the same prompt as project hooks), and it is added after
yours, not in place of it. Set "project_system_prompt": "replace" in
~/.buildwithnexus/settings.json to let a trusted project's file replace yours.
Skills
Skills are markdown instructions loaded on demand: the system prompt carries
only each skill's name and description, and the full body is loaded by
/<name>, the load_skill tool (alias skill), or list_skills → load_skill.
Two layouts are supported, from any of these roots:
~/.buildwithnexus/skills/ ./.buildwithnexus/skills/ (source: user / project)
~/.claude/skills/ ./.claude/skills/ (source: claude)
~/.agents/skills/ ./.agents/skills/ (source: agents)<name>/SKILL.mdfolders (the Agent Skills open standard) with YAML frontmatter —name(defaults to the folder name) anddescription(required for a useful listing; a missing one shows as(no description)with a one-time warning). Unknown keys are ignored. When loaded, the skill reports its directory soscripts/orreferences/inside it can be read with the file tools.<name>.mdflat files, where the first prose line is the description.
On a name collision a SKILL.md folder beats a flat file of the same name,
and your own skills beat bundled ones. A skill from the project (the ./
roots above, a relative skill_dirs entry, or any skill_dirs entry a
project settings file adds) loads only once you trust the folder (see
Project commands, skills and agents below), and never replaces a bundled or
user skill: it loads as /project:<name> instead, with a notice at startup.
Set "project_skills_override": true in ~/.buildwithnexus/settings.json to
let project skills replace them, as before 0.15. Add more roots with the
skill_dirs settings key (["~/my-skills", "tools/skills"]).
/skills lists every skill with its source and description.
Custom commands
A Markdown file in a commands folder is a slash command named after the file:
deploy.md is /deploy. Its body, with the YAML frontmatter stripped, is the
prompt; description in the frontmatter is what the command popup shows.
$ARGUMENTS in the body is replaced by everything typed after the command,
and $1…$9 by single words (quotes group words), so /deploy staging fills
in staging. A body with no placeholder gets the arguments added after it.
A .sh, .bash or .py file runs as a command instead, through the same
hooks and permission gate as run_command.
Commands load from ~/.buildwithnexus/commands/ and ~/.claude/commands/,
and, once you trust them, from the project's .buildwithnexus/commands/
and .claude/commands/. Skills and commands work headless too:
bwn run '/deploy staging'.
---
description: Deploy to an environment
---
Run the deploy for $ARGUMENTS, then check the health endpoint.Helper agents
The model can hand a subtask to a helper with the task (or
spawn_subagent) tool, as the built-in engineer or researcher role or as
a helper you define. An agent file is <name>.md with name, description
and tools frontmatter and the helper's instructions as the body; tools
limits what it may use, and Claude Code's tool names work (Read, Grep,
Bash, …). A helper with a tools list can never delegate further (task
and spawn_subagent are left out), and a call naming an unknown role fails
with the list of roles. /agents lists the helpers.
---
name: test-writer
description: Writes focused unit tests for one module
tools: Read, Grep, Glob, Write
---
Write tests for the module you are given. Do not change the module itself.A helper started with read_only: true, or from an agent file with
read_only: true or a tools list that only reads (Read, Grep, Glob),
can read and search but never change anything. Read-only helpers and
isolated ones (isolate: true) that the model starts in the same reply run
at the same time, up to max_parallel_helpers at once (default 3; 1
runs them one after another). On a local server the default is one at a
time, or as many as the server reports it answers at once (llama.cpp's
slots): a server with one slot queues the others until they time out. Isolated helpers take turns while folders
added with --add-dir are in use: a worktree does not cover those, and each
helper may write there. Each shows its work in one labelled block
when it finishes, a helper that needs an approval says which one it is,
and Esc or Ctrl+C stops them all. Helpers that write in your folder always
run one after another. Their tokens and cost count toward the session.
Agent files load from ~/.buildwithnexus/agents/ and ~/.claude/agents/,
and, once you trust them, from the project's .buildwithnexus/agents/
and .claude/agents/. A helper that runs isolated in a git worktree shows
its branch and the merge command when it finishes (a subagent_result event
in --json mode), and commits with your git identity. To run a whole session
on its own branch, start it with --worktree <name>.
Project commands, skills and agents
A repository's command files speak to the model in your name, its skills
and agents steer it, and a script command runs code, so they stay off until
you trust them. The folder trust prompt lists each one with its file
(command /deploy (.claude/commands/deploy.md)), also when the repository
has nothing else to trust, and trusting pins their contents: adding, editing
or removing one asks again at the next start. Until then a startup notice
names them and how to trust, and typing one says it is off. A file linked to
somewhere outside its folder never loads. buildwithnexus trust --print
lists them for CI. Run from your home folder, these folders are your own and
load as such.
MCP servers
buildwithnexus is a full Model Context Protocol
client. Configure servers under mcp_servers in ~/.buildwithnexus/settings.json
(user) or .buildwithnexus/settings.json (project); both are merged.
{
"mcp_servers": {
"fs": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
"env": { "LOG_LEVEL": "warn" } },
"remote": { "url": "https://mcp.example.com/mcp",
"headers": { "Authorization": "Bearer …" }, "timeout_secs": 15 },
"old": { "command": "legacy-server", "enabled": false }
}
}| Key | Meaning |
|---|---|
| type | "stdio" or "http" (Streamable HTTP). Optional: a url means http, a command means stdio. |
| command, args, env | stdio: the process to spawn, kept alive for the whole session; stderr is captured for diagnostics. |
| url, headers | http: the endpoint and extra request headers (a static token goes here). Mcp-Session-Id is tracked automatically. |
| oauth | http: a client registered ahead of time, for a server whose authorization server has no dynamic registration: client_id, optional client_secret, scopes, and callback_port when the redirect URI must be exact. |
| timeout_secs | Per-request deadline (default 30). A server that hangs or exits is reported once and its tools drop out for the session. |
| enabled | false keeps the entry but never connects. |
Servers connect lazily in the background on the first prompt (headless runs
connect before the first request, waiting at most 5 s, or a server's own
timeout_secs, and skip a server that is not ready with a notice); each one logs
mcp: <name> connected, N tools or its error. Discovered tools are advertised
to the model as mcp__<server>__<tool> with the server's own description
and input schema, and answer over the persistent connection. They count as
mutating under the permission gate (prompted under ask, blocked under
readonly). A server's readOnlyHint: true annotations are only honoured
when you set "trust_read_only_hints": true on that server, because the hint
is the server's own claim. The older
mcp_call tool (server, tool, arguments) still works over the same
connection.
/mcp servers: transport, status, tool count
/mcp <name> a server's tools with descriptions
/mcp add [--force] <name> <command> [args...] stdio server → settings.json, then reconnect
/mcp add [--force] <name> --url <url> [--header K=V]... [--timeout <secs>]
[--client-id <id>] [--callback-port <port>]
/mcp remove <name>
/mcp login <name> sign in to an http server that asks for OAuth
/mcp logout <name> revoke and forget that sign-in
/mcp reload reconnect every serverbuildwithnexus mcp list|<name>|add|remove|login|logout|reload mirrors this
for scripts (add/remove only edit the settings file; list connects).
Everything after the server name is the server's own command line, so
buildwithnexus mcp add fs npx -y some-server --json keeps -y and --json
for the server; a -- before the command, as Claude Code writes it, is
accepted. add refuses
to replace a server that already has that name (exit 1) unless you pass
--force. /doctor and
buildwithnexus doctor connect to every configured server and report the
outcome. Legacy SSE-only (type: "sse") servers are not supported.
Servers that ask you to sign in (OAuth). An http server that answers
401 with a Bearer challenge is listed as needs login; connecting never
opens a browser, and a headless run skips the server with a notice naming
bwn mcp login <name>. bwn mcp login <name> (or /mcp login <name> in a
session, which reconnects it) follows the MCP authorization spec: it reads
the server's protected-resource metadata and its authorization server's
metadata, registers bwn as a client when the server allows that (otherwise
set oauth.client_id), and opens the browser on an authorization-code + PKCE
(S256) sign-in that comes back to a one-shot listener on 127.0.0.1. The
URL is printed too, for a browser elsewhere. The tokens are saved owner-only
in ~/.buildwithnexus/mcp-auth/<name>.json, only for the server's URL as
configured, are refreshed when they expire, and are sent only over HTTPS or
to this machine. A server that echoes its token back gets [redacted] in
place of it before the model, the transcript or the terminal sees it.
bwn mcp logout <name> asks the authorization server to revoke the token
and deletes the file. A server with an Authorization header in headers
is left as configured.
Editors (Agent Client Protocol)
buildwithnexus acp speaks the Agent Client Protocol
(version 1) on stdin and stdout, so Zed, JetBrains IDEs and Neovim plugins
that run ACP agents can drive bwn: the editor shows the streamed reply, each
tool call with its diff, the plan, and the approval questions. In Zed, add it
to settings.json, then pick it from the Agent Panel's new-thread menu:
{
"agent_servers": {
"buildwithnexus": {
"type": "custom",
"command": "bwn",
"args": ["acp"],
"env": {}
}
}
}Other ACP clients take the same command and arguments. bwn uses the provider,
model and key you set up in the terminal (~/.buildwithnexus); put an API key
or NEXUS_HOME in env to use others, and the options of
Headless and CI in args (for example
["acp", "--permission-mode", "accept-edits"]). If the editor cannot find
bwn, give the full path that command -v bwn prints.
What carries over from the terminal:
- Approvals. Every approval the terminal would ask for is asked in the
editor with Allow once, Always allow (the terminal's
a: remembered for this project in your settings) and Reject (told to the model as a denial). Permission modes, allow/ask/deny rules, hooks and the sandbox apply as in the terminal. The model's `q
