@burtson-labs/bandit-stealth-cli
v1.7.503
Published
Bandit — a local-first AI coding agent for your terminal. Same runtime as the Bandit Stealth VS Code / Cursor extension.
Readme
Bandit — Agent CLI
Local-first AI coding agent for your terminal.
Runs on your machine and works with any Ollama model. Your prompts, and the files and command output the agent reads, go to the model server you configure (Ollama at http://localhost:11434 by default). In the default permission mode, a tool that would contact another host asks you first. Network and offline mode lists every connection Bandit makes and how to turn each one off.
Prefer an IDE? The sibling Bandit Stealth extension for VS Code / Cursor ships the same runtime, skills, and tool-use loop — install from the VS Code Marketplace or Open VSX.
Install
Install Ollama and pull a model:
brew install ollama # or download installer ollama pull qwen2.5-coder:7b # fast, tool-calling, ~4.7 GBInstall the CLI globally — curl, or npm if you prefer:
curl -fsSL https://burtson.ai/bandit-stealth-cli/install.sh | sh # or: npm i -g bandit-stealth-cliRun it:
bandit # interactive REPL bandit "explain @src/auth/login.ts" # one-shot with a file mention bandit --read-only # start in plan mode: read and plan, change nothing
The CLI installs two commands that do the same thing: bandit and bandit-agent. The Python security linter Bandit also installs a bandit command; if it comes first on your PATH (common in Python environments and on shared clusters), use bandit-agent. bandit doctor tells you when that happens.
That's it. No API keys. No cloud services. The agent reads your code, searches, runs commands, and writes changes — all locally.
Terms of Use
The first time you start bandit in a terminal (and again whenever the terms change), it prints a short summary of the Terms of Use and asks you to accept them ([y/N]). Your answer is saved in ~/.bandit/terms.json as { "version", "acceptedAt" }. bandit terms prints the URL, the current version and the version you accepted. The full text also ships in this package as TERMS.md.
Runs that have nobody to ask never wait for an answer: a one-shot prompt (bandit "…"), bandit mcp serve, input or output that is not a terminal, and CI (CI set). In those, accept for the run with the environment variable or the flag; otherwise bandit exits with status 1 and one line saying how:
BANDIT_ACCEPT_TERMS=1 bandit "summarize the open TODOs"
bandit --accept-terms "summarize the open TODOs"--accept-terms is saved in ~/.bandit/terms.json like an answer at the prompt, so later runs need neither. BANDIT_ACCEPT_TERMS=1 accepts for that run only and writes nothing (for CI and containers). --help, --version, terms, upgrade, self-update and doctor never ask.
An option bandit does not know (a typo such as --read-onyl) stops with exit status 2 and names the closest one; it is never sent to the model as a prompt. Put prompt text that starts with - after --: bandit -- "-v means verbose?".
What it does
The CLI keeps common coding tools available and loads other tool groups on
demand to reduce prompt size. BANDIT_TOOL_EXPOSURE=all exposes all registered
tools immediately; it does not restore older tool descriptions. Tool permissions
and execution checks still apply to deferred tools. If a provider failure
switches the agent to text tool calling, its prompt retains the groups already
loaded during that turn. Compact prompts keep the tool-group directory so
smaller models can discover tools that have not been loaded yet.
After an edit, a failed check or a background job's start does not count as
completed verification. The agent receives at most one reminder to verify the
requested result or report what remains unverified. This is a bounded prompt
guard, not a guarantee of correctness; BANDIT_VERIFY_BEFORE_DONE=0 disables it.
- Copying with the mouse — drag over anything on screen (history, the reply, the prompt) and release: the text is copied to the clipboard and Copied to clipboard shows at the lower right for two seconds. Text that wrapped onto several rows copies as one line; real line breaks are kept and colours are dropped. A plain click copies nothing. The copy goes through
pbcopy/wl-copy/xclip/clip, plus an OSC 52 escape over SSH and in tmux so it reaches your local clipboard; withBANDIT_NO_CLIPBOARD=1the notice says Copy unavailable instead./copystill lets you choose the latest response or one of its code blocks, preserving the source text without terminal wrapping:/copy response,/copy code 2,/copy choose./linkslists recent response and artifact URLs;/copy linkcopies the latest, and/copy 2copies the second listed URL./copy helpshows the shortcuts. - Clicking prompts and menus — question choices, Continue/Submit, permission choices (allow once/turn/session, deny, Confirm, Cancel), the MCP trust check, the theme and session pickers, the
/command list, the@file list and follow-up suggestions react to a click. In a choice list a click highlights an option and a click on the highlighted one answers with it (as Enter does), so the default takes one click and any other two; a/command or@file is inserted with one click. Arrow keys, digits and Enter keep working, and Alt+1–3 still picks a suggestion. Clicks need a terminal with SGR (1006) mouse reporting: the Stealth IDE terminal, VS Code's, iTerm2, Terminal.app, kitty, WezTerm, Alacritty and GNOME Terminal have it. - Scrolling and native selection — the mouse wheel scrolls the terminal's own scrollback as usual: the first wheel tick hands the mouse back to the terminal (that one tick does not scroll), and the next key you type takes it again. While it is handed back, and whenever you hold Shift (most terminals), Option (iTerm2) or Fn (Terminal.app), dragging is the terminal's own selection, copied with its shortcut (Cmd+C on macOS, usually Ctrl+Shift+C elsewhere). Only what is visible on screen can be selected with the in-app drag; for older text scroll up and use the native selection.
BANDIT_MOUSE=0,--no-mouseor"mouse": falsein~/.bandit/config.jsonturns the mouse features off entirely and leaves the mouse to the terminal. - Clickable web links — labeled links retain their destinations through the live renderer. Stealth opens them in your browser; other terminals use their own link gesture and hyperlink support.
- Agentic tool use — reads files, searches code, runs commands, writes changes
- Auditable approval gate — writes show a colored diff, shell commands show the full command/cwd/risk, and
Allow once/Allow session/Always for targetscopes are recorded in turn traces - Pre-write validation — TypeScript, Python, JSON, C# syntax-checked before the agent can write
- Post-write validation — JSON edits are re-parsed after write; failure feeds back to the agent on the next turn so it self-corrects without you flagging it
- Skills system — agent activates specialized skills based on your prompt, and can create its own
- Background subagents — long investigations spawn detached; status bar shows
bg:N running; you keep talking; synopsis auto-injects when ready (/tasksto inspect, drill down, or cancel) watch_command— run a dev server /--watchtest runner for a bounded window, agent reacts to what came outfind_directory— cross-repo discovery; ask "open the auth-api repo" and the agent sweeps~/Documents/GitHub,~/GitHub,~/Projects,~/code,~/dev,~/repos,~/work,~/src, plus the workspace parent — no "where is that repo?" round-trips- MCP both directions — speaks the Model Context Protocol as a client (
/mcp add github <token>,/mcp add slack,/mcp add gitlab,/mcp add custom <name> <cmd…>) and as a server (bandit mcp serveexposes Bandit's native tool surface over stdio so Claude Desktop / Cursor / Cline / Continue can drive your codebase through it) - Installs CLIs on demand — ask Bandit to install
ripgrep,httpie, the GitHub CLI, etc. and it picks the right package manager (brew,npm install -g,pip install,cargo install,gem install,go install) and runs it through the permission gate - Interactive scaffolders work —
create-vite,create-react-app,ng new, etc. detect a non-TTY stdin and self-abort. Bandit recognizes the pattern and surfaces a clear "run this with!" recovery hint so the model doesn't loop on a "command appeared to succeed" misread - Live command output —
npm install,pip install,watch_command npm run devstream their output to your terminal as it arrives, dimmed, while the spinner keeps animating. No more wondering if a 20-second install is hung - Interrupt + queue — press Esc mid-turn to cancel the agent and clear your queue. Type a follow-up + Enter to queue it (
queued: N · sends after this turnin the status row). The next turn picks it up automatically - Opt-in notifications —
/notify onenables desktop notifications for approvals, failures, background-task completion, and long turns;/notify sound onadds a terminal bell ?shortcuts overlay — type?at an empty prompt for a live cheatsheet that disappears the moment you backspace it!-prefix shell escape —!cmdruns straight in your shell with full TTY access. First-use confirmation gate; per-call yellow box every time after so you can't miss the bypass. Catastrophic patterns (rm -rf,mkfs,dd if=) blocked even here- Plan execution — structured multi-step plans for complex refactors
- Session persistence — every REPL session saved as JSONL under
~/.bandit/sessions/for later resume - Turn traces — every agent turn writes a JSONL trace under
.bandit/turns;/traceturns the latest trace into a readable timeline,/trace listbrowses recent workspace/global turns, and/trace failedfilters recovery/debugging runs /insightsHTML report — local-only activity report: reads global sessions plus workspace/global turn logs, reconstructs cross-repo wins, surfaces bigger arcs and outcome snippets, then adds tool stats, top-touched files, languages, streak, peak day, error patterns, optional AI summary, and mailto share- Model behavior profiles —
/profileshows how Bandit treats the active model: native vs text tools, fallback policy, safe context budget, thinking default, parallel-tool limits, and known failure modes - Project memory — drop a
BANDIT.mdorCLAUDE.mdat your workspace root and it's auto-loaded into the system prompt - File + image mentions —
@pathauto-inlines files; images are either sent multimodally or OCR'd locally (Apple Vision / tesseract) - Clipboard paste —
Ctrl+Vin the REPL pastes an image straight from your clipboard - Hooks + security guard —
PreToolUse/PostToolUse/Stopshell hooks (global~/.bandit/settings.jsonor per-workspace), plus a default-on built-in guard that blocks catastrophic commands. See Settings file - 12 themes — Stealth Light/Dark, Midnight, Onyx, Charcoal, Dracula, Nord, Tokyo Night, Solarized Dark/Light, Catppuccin Mocha, Sepia.
/themeto pick - Cross-platform — macOS, Linux, Windows; Windows
.cmd/.batshims (npm/npx/pnpm/tsc) resolved correctly - Update-aware — fire-and-forget npm-registry check at boot;
update vX.Y.Z availableshows in the status bar when a newer CLI is published. SetBANDIT_NO_UPDATE_CHECK=1(orBANDIT_OFFLINE=1) to skip the check
Slash commands
Type ? on an empty prompt for the at-a-glance overlay; /help for the full list.
| Command | Does |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| /help | Full slash-command list |
| /doctor | Check setup, provider, workspace context, permissions, and next best actions |
| /login <key> | Save a Bandit Cloud API key to ~/.bandit/config.json (also /login, /login clear) |
| /usage | Bandit Cloud session + weekly usage limits (/usage check for one-line ⚠ flag) |
| /model [name] | Switch model mid-session |
| /ollama [url] | Show or set the Ollama endpoint — /ollama default resets to http://localhost:11434 |
| /think on, /think off, /think auto | Override per-model thinking-mode default |
| /profile [model] | Show the active model behavior profile (tool protocol, fallback, context budget, known failure modes) |
| /notify status | Configure desktop/bell notifications for approvals, failures, background tasks, and long turns |
| /theme [name] | Pick a color palette (/theme lists; saved to global config) |
| /skills | List loaded skills |
| /session list, /session resume <id>, /session new | Manage sessions |
| /memory | Show auto-loaded BANDIT.md / CLAUDE.md |
| /config | Show effective config + path (secrets redacted) |
| /clear | Reset conversation (keeps session id) |
| /compact | Trim old tool results to fit the context window |
| /rewind [id] | Restore a file from a per-edit checkpoint |
| /trace, /trace list, /trace failed, /trace <id> | Inspect turn traces from workspace/global .bandit/turns |
| /tasks | List background commands, monitors and subagents (/tasks <id> drill-down, /tasks cancel <id>) |
| /copy | Open text selection: drag, release to copy, see confirmation; Esc returns to chat |
| /copy response, /copy code 2, /copy choose | Copy the original response, a code block, or choose a complete block |
| /plan <goal> | Heuristic plan first, y/N to execute |
| /init | Scaffold BANDIT.md from a repo scan |
| /commit | Draft a conventional-commit message from the staged diff |
| /review [focus] | Code review of staged changes or branch-vs-main, ends with 🟢/🟡/🔴 |
| /refactor <target> | Concrete refactor suggestions with before/after snippets |
| /test <target> | Generate tests in the project's existing framework |
| /explain <target> | Plain-English walkthrough of a file or function |
| /onboard | New-developer setup checklist for the repo |
| /changelog [range] | Release notes drafted from git log |
| /exit | Quit |
Skills
The agent activates specialized skills based on your prompt:
| Skill | Trigger | What it does | | --------------- | -------------------------- | --------------------------------------- | | Filesystem | always | Read, write, search, list, run commands | | Git | always | Status, diff, log, commit | | Code Review | "review my changes" | Diff + full file context | | Testing | "write tests" | Auto-detect runner, generate tests | | Planning | "refactor the auth system" | Structured multi-step decomposition | | Semantic Search | "how is auth implemented" | Local embedding search |
Custom skills (the agent can make its own)
Ask: "create a skill that runs my linter"
The agent writes .bandit/skills/linter.md. Next prompt, it's live. Ask "lint my code" and it runs.
MCP — Model Context Protocol servers
Bandit speaks MCP as a client, so any MCP server you can spawn (filesystem, git, GitHub, Google Drive, Gmail, Slack, Postgres, custom workplace tools…) plugs straight into the same tool-use loop. Each server's tools are namespaced as <server>.<tool> and registered alongside read_file, apply_edit, etc.
Configure at ~/.bandit/mcp-servers.json (global) or .bandit/mcp-servers.json (workspace, takes precedence). Schema is the standard MCP mcpServers shape — the same JSON other MCP clients use, so configs port between them:
{
"mcpServers": {
"fs-tmp": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..." },
},
},
}Manage with the /mcp slash command:
| Command | What it does |
| ------------------------ | --------------------------------------------------------------------------- |
| /mcp | List configured servers + status (connected / idle / error) and tool counts |
| /mcp tools <name> | Spawn the server (lazy) and introspect its exposed tools |
| /mcp connect <name> | Explicit warmup so the first invocation isn't slow |
| /mcp disconnect <name> | Close the server's child process (re-spawns lazily on next use) |
| /mcp reload | Re-read the config files from disk after edits — no restart needed |
Servers spawn lazily on first invocation, persist for the session, and get cleaned up on REPL exit. Failures are isolated — a broken server logs an error and the rest of the loop keeps running on native tools only. Off by default — no config file = zero behavior change.
MCP tools ask before they run, because Bandit cannot see what a server does with the arguments. Add "local": true to a stdio server's entry to say it does not contact other hosts; its read-named tools (list…, get…, search…, read…) then run without asking, and its other tools still ask. Your Bandit Cloud key is passed to a server only when its entry has "auth": "bandit".
Recommended models
Bandit uses native tool calling when supported by the selected provider.
Cloud — Bandit gateway (API key)
| Model | Notes |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| bandit-logic | Default: Kimi K3 through Ollama Cloud, with vision, reasoning and native tools. Advertised context: 1M; the harness keeps conservative working budgets. |
| bandit-logic-2 | Compatibility alias for the same K3 backend. Existing saved sessions continue to work. |
| bandit-core-2 | Existing general-purpose cloud model. |
| bandit-core-1 | Lightweight self-hosted model. |
K3 replaces K2.7 Code in the Logic routes. The initial three-task compatibility screen passed; that small sample is not a comparative quality or latency claim. Cloud Logic uses the existing frontier subscription, usage and credit rules. Use an explicit Qwen model with the Ollama provider for local inference.
Local — Ollama
Pull one with ollama pull <model>.
| Model | Where | Notes |
| ------------------------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| qwen3.6:27b | Local / Mac 48GB+, high-VRAM GPU (~17 GB) | Best local pick. Runs offline. Probes the filesystem instead of asking for clarification — real agent behavior. |
| gemma4:26b | Local / Mac 32GB+ (~17 GB) | Solid alternative when Qwen 3.6 is too heavy for your hardware. Multimodal, 128K context. |
| gemma4:e4b | Local / laptop-class (~3 GB) | Lightweight pick that punches above its weight. Validated on real Bandit runs — clean tool sequencing (ls → narrow → read_file), no hallucinated paths. Right pick when you want a local agent that doesn't pin your fans. |
| gemma4:31b | Local / Mac 64GB+, GPU node | Bigger context, better reasoning for complex refactors. |
| qwen2.5-coder:7b | Local / Mac (~4.7 GB) | Fast lightweight pick. Native tool calling. Best for "given context, do X" tasks rather than autonomous discovery. |
| devstral:latest | Local / Mac 32GB+ | Mistral's agent-tuned model — strong tool use. |
| bandit-core:12b-it-qat | Local / Mac (~9 GB) | Our Gemma-12B fine-tune. Fits most laptops, agent-aware. |
Models we don't recommend (for agent work)
Bandit is an autonomous agent harness — it expects the model to discover repo structure, plan edits, and emit tool calls without being hand-held. Some otherwise-impressive models aren't trained for that workflow and produce unexpected results:
gpt-oss:120band other reasoning-tuned models — post-trained for OpenAI's harmony tool-call format, not the XML/native protocols Bandit uses. Tends to narrate intent ("I'll search for the controllers...") without ever emitting an actual tool call.qwen2.5-coder:32band other code-completion-tuned models — post-trained for fully-specified code-generation benchmarks. On ambiguous prompts it asks for paths instead of probing. Solid for concrete tasks; underwhelming as an autonomous agent.qwen3.6:35b— the larger Qwen 3.6 variant stalls in reasoning-only output and ignores the harness's "act now" nudges. The 27B is the better production pick from this family.
If you want to test models outside the recommended list, expect the reasoning-only / narrate-but-no-action / partial-completion detectors to fire frequently. Those are signal — they mean the model isn't a great fit for autonomous agent work.
Capability dispatch:
- Native tool calling — Qwen 3.6, Qwen 2.5 Coder, Llama 3.1+, Devstral, DeepSeek-Coder-V2+. Tool schemas go in Ollama's
tools:field. Saves ~1500–3000 tokens per turn. - Text-parsing fallback — Gemma 3/4 and anything else. XML-style tool block lives in the system prompt with the full mitigation stack armed.
Behavior profiles sit beside capability detection. Capabilities answer "can this model do native tools or vision?" Behavior profiles answer "what should the harness do with it?" For example, Qwen 3.6 starts on native tools and degrades to text tools on retryable native-parser/watchdog failures; Gemma-family models use compact text-tool prompting and earlier compaction; unknown models default to serialized text tools. The profile's context.outputBudgetTokens and reliability.maxParallelTools now directly drive the loop's heavy-batch serialization and parallel-call cap in both CLI and extension. Inspect the active profile with /profile.
Workspace overrides load from .bandit/model-profiles.json:
{
"version": 1,
"profiles": {
"my-qwen": {
"match": ["my-qwen:14b"],
"protocol": { "preferred": "text-tools", "fallback": null, "envelope": "xml-json" },
"context": { "safeInputTokens": 12000, "outputBudgetTokens": 2048, "compaction": "early" },
"prompting": { "template": "qwen-agent", "examples": "strict", "thinking": "off" },
"reliability": { "maxParallelTools": 1, "knownFailureModes": ["custom parser drift"] },
},
},
}Any Ollama model works — capabilities auto-detect via /api/show.
Configuration
Config file (preferred)
~/.bandit/config.json or <workspace>/.bandit/config.json:
{
"provider": "ollama", // or "bandit"
"model": "qwen2.5-coder:7b",
"ollama": {
"url": "http://localhost:11434",
"headers": { "Authorization": "Bearer ..." }, // optional
},
"bandit": {
"apiKey": "bnd_...",
"apiUrl": "https://api.burtson.ai",
},
}Workspace config overrides user config. Secrets belong in the user-level file, not in a committed workspace file.
Environment variables
| Var | Default | Description |
| ------------------------ | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| BANDIT_PROVIDER | ollama | ollama or bandit |
| BANDIT_MODEL | gemma4:e4b | Model ID |
| BANDIT_API_KEY | — | Required when BANDIT_PROVIDER=bandit |
| BANDIT_API_URL | https://api.burtson.ai | Override Bandit API endpoint |
| OLLAMA_URL | http://localhost:11434 | Ollama endpoint |
| OLLAMA_HOST | — | Ollama's own variable (node:port, http://node:port); used after OLLAMA_URL and config |
| BANDIT_NUM_CTX | chosen per model | Context window to request from Ollama (32768, 32k); same as --num-ctx |
| BANDIT_SMALL_CONTEXT | auto | on/off: small-context mode for small windows (with Ollama, on below 16,384 tokens); same as --small-context/--no-small-context |
| BANDIT_HOME | ~/.bandit | Where sessions, crash reports and the Apptainer image live; settings stay in ~/.bandit |
| BANDIT_NO_LOGIN_HINT | 0 | 1 to hide the note shown on what looks like a cluster login node |
| BANDIT_MAX_ITERATIONS | 20 | Tool-use loop cap |
| BANDIT_AUTO_APPROVE | 0 | 1/true to skip write-approval prompts |
| BANDIT_TELEMETRY | 0 | 1/true to enable OTLP telemetry (opt-in) |
| BANDIT_OTLP_ENDPOINT | https://otlp.burtson.ai | OTLP/HTTP collector base URL |
| BANDIT_OTLP_TOKEN | — | Bearer token for the collector (defaults to your Bandit token) |
| BANDIT_OTLP_MODE | metrics+traces | metrics-only to drop span payloads |
| BANDIT_NO_UPDATE_CHECK | 0 | 1/true to skip the npm-registry update check at start (one plain GET; nothing is installed) |
| BANDIT_OFFLINE | 0 | 1/true: offline mode. Only the model server is contacted (Network and offline mode) |
| BANDIT_NO_MODELS_DEV | 0 | 1/true to never fetch the models.dev catalogue for an OpenAI-compatible server |
| BANDIT_NO_BROWSER | 0 | 1/true to never launch the default browser (insights, share links); the path or link is still printed |
| BANDIT_NO_CLIPBOARD | 0 | 1/true to leave the clipboard alone when a share link is printed |
| BANDIT_ACCEPT_TERMS | — | 1/true to accept the Terms of Use for this run only (--accept-terms also saves it) |
| NO_COLOR | — | Disable ANSI colors |
Settings file — hooks, permissions, security guard
Plan mode is read-only: the agent reads files, searches and runs read-only commands (git status, ls, grep), and every edit, file write, state-changing command and network write is refused, so it ends with a plan instead of changes. Start a session in it with bandit --read-only (also for one-shot prompts: bandit --read-only "how would you add caching here?"), or switch with shift+tab or /plan; BANDIT_PERMISSION_MODE=plan does the same. --read-only cannot be combined with --auto.
Terminal --auto mode runs routine work immediately and reviews other actions with the selected provider/model. The reviewer considers the raw user requests in the current session, the proposed action and bounded local metadata. Requested cleanup can proceed without a blanket deletion prompt; uncertain effects or missing authorization still ask. External, system and irreversible actions need explicit authorization. Reviews use no tools, have a 12-second deadline, and their decisions appear in the turn log and /permissions. Explicit policy rules and security guards remain in force. Targeted edits to existing user-owned temporary helper files can qualify as task-related scratch work when the host verifies file ownership and location; this does not authorize arbitrary temporary-file deletion or replacement. Model review is fallible; ask mode remains available when every ungranted action should be reviewed by you.
Interactive sessions expose monitor_job, check_monitor, and cancel_monitor. Ask the agent to monitor a particular run and specify its cadence and authorized response, for example: “Check this run every five minutes; if it fails, collect its logs and diagnose it.” The monitor returns immediately and uses a timer to run the approved status command. A terminal result resumes the agent, including when other jobs are still running. Follow-up actions go through the normal permissions gate.
Status commands must be read-only and return one compact JSON object with an exact job ID and status string. target_id is checked on every response; id_path and status_path select nested fields. Success, failure and pending state lists are configurable. Unknown status, a mismatched ID, or a failed status command never counts as success. The default interval is 300 seconds, each probe has a 15-second timeout, and the default overall deadline is 24 hours. Three consecutive probe errors stop observation and report that the remote job status is unknown. A retry creating a new run needs a new monitor.
Use /tasks to see monitoring activity and /cancel <task-id> to stop it. Keep the CLI session open: monitors are session-local and stop when it closes or the conversation is cleared. Escape during an active agent turn also stops monitors. Automatic continuation preserves a draft in the composer and does not retry a failed model wake-up endlessly. Use existing command authentication or environment references rather than embedding tokens in polling scripts or tool arguments. Diff previews mask secrets for display while preserving the actual file contents.
Separate from config.json, a settings file controls what the agent is allowed to do. It's read from two places and merged — the global file applies to every repo, the workspace file adds project-specific rules on top:
~/.bandit/settings.json— global (all workspaces)<workspace>/.bandit/settings.json(and.bandit/settings.local.json) — per-project
{
// Built-in pre-tool security guard. ON by default. Blocks the
// handful of tool calls that are almost never legitimate, BEFORE they run —
// a safety net against the model footgunning (e.g. a hallucinated rm -rf /).
"security": {
"guard": {
"enabled": true,
"blockCommands": ["npm\\s+publish"], // optional: extra regex patterns to block
"protectPaths": ["/data/prod"], // optional: extra write-protected path prefixes
},
},
// Your own shell hooks around the tool loop. A non-zero exit from a
// PreToolUse hook BLOCKS the tool call. Placeholders: {{name}}, {{primary}},
// {{duration}} (PostToolUse only).
"hooks": {
"PreToolUse": [{ "match": "write_file", "command": "./scripts/guard.sh {{name}}" }],
"PostToolUse": [{ "match": ".*", "command": "echo {{name}} took {{duration}}ms" }],
"Stop": [{ "command": "./scripts/notify-done.sh" }],
},
// Per-tool permission policy (deny > allow > ask > built-in defaults).
"permissions": { "allow": ["run_command:git *"], "deny": [], "ask": ["run_command"] },
}What the guard blocks when enabled (high-confidence patterns only, so false positives stay rare):
catastrophic rm -rf / · ~ · /* · a remote script piped to a shell (curl … | sh) · raw disk writes (dd of=/dev/sda, mkfs) · fork bombs · recursive chmod/chown on / · writes or redirects into system/credential paths (/etc, ~/.ssh, ~/.aws, …) · reading a credential file and sending it over the network in one command.
The guard is defense-in-depth against the model, not a sandbox and not protection against a malicious user (who controls this file). It is enabled by default; set security.guard.enabled to false only in a controlled environment. It runs before your hooks and the approval gate, and behaves identically in the CLI and the IDE extension.
Hooks — any scripted step, not just guardrails
Hooks run your own commands at three points in the loop. Only PreToolUse is a gate; the rest are fire-and-forget side effects — so reach for them for version bumps, formatters, notifications, CI triggers, anything scriptable.
| Event | Fires | Blocks the tool? |
| ------------- | --------------------- | ---------------------------------------------------------------------- |
| PreToolUse | before a tool runs | Yes — a non-zero exit aborts the call (this is the guardrail path) |
| PostToolUse | after a tool finishes | No — runs for its side effect |
| Stop | when the turn ends | No |
Each rule is { "match": "<regex on tool name — omit to match all>", "command": "<shell>", "timeout": 10000 }. Placeholders are substituted into command (shell-escaped): {{name}} (tool name), {{primary}} (first arg — path / cmd / pattern / url), {{duration}} (ms, PostToolUse only).
"hooks": {
// guardrail: refuse edits to a generated file
"PreToolUse": [{ "match": "write_file|apply_edit", "command": "grep -q '@generated' {{primary}} && exit 1 || exit 0" }],
// housekeeping: format after every edit; bump the build number after any tool
"PostToolUse": [
{ "match": "write_file|apply_edit", "command": "prettier --write {{primary}} || true" },
{ "match": ".*", "command": "./scripts/bump-build-number.sh {{name}}" }
],
// notify when a turn ends
"Stop": [{ "command": "terminal-notifier -message 'Bandit finished' || true" }]
}Telemetry (opt-in)
Off by default. When enabled, Bandit emits OpenTelemetry over OTLP/HTTP: one trace per turn (LLM + tool calls as spans, errors marked) plus usage metrics (tokens, time-to-first-token, turn duration). The wire format is standard OTLP, so you point it at your own collector — Grafana/Tempo, an OpenTelemetry Collector, App Insights, Datadog, anything that speaks OTLP/HTTP.
Where does it go? You decide. Set
endpointto your collector. The example below usesotlp.burtson.ai— that's Burtson's hosted collector and only accepts Bandit Cloud accounts; if you're not a Bandit Cloud user, pointendpointat your own and set your ownheaders.
// ~/.bandit/config.json
{
"telemetry": {
"enabled": true,
"endpoint": "https://otel.your-company.com", // YOUR OTLP collector (otlp.burtson.ai = Bandit Cloud only)
"mode": "metrics+traces", // or "metrics-only" for stricter privacy
"headers": { "Authorization": "Bearer ..." }, // your collector's auth (defaults to your Bandit token)
},
}The default endpoint is https://otlp.burtson.ai only because the default audience is Bandit Cloud; set your own otherwise.
What is sent: the model name, tool names, the first 120 characters of each tool's main argument (a file path, command, URL or search query), token counts, durations and short error text, after secret redaction. The CLI does not send your prompt or the model's replies. metrics-only drops the traces, and with them the tool arguments. Nothing is sent unless you set enabled: true (or BANDIT_TELEMETRY=1), and nothing in offline mode.
Remote GPU
Running a bigger model on a remote Ollama instance? Point OLLAMA_URL at the remote endpoint and set BANDIT_MODEL to the bigger model. Requests route to the remote node; everything else stays local.
Rented GPU (RunPod / Vast.ai / Lambda)
When you need to run a model your local hardware can't fit, Bandit talks to any remote Ollama endpoint — including rented GPU pods. Same shape on every provider: spin up a pod with Ollama on port 11434, copy the proxy URL, point OLLAMA_URL at it.
RunPod (recommended — simplest UX):
# 1. From the RunPod template gallery, pick any Ollama template.
# H100 SXM is the right pick for 27-32B models; multi-GPU only
# needed for 70B+. Network volume optional but useful if you want
# model weights to persist across pod restarts.
# 2. Once the pod boots, copy its proxy URL from the dashboard.
# Format: https://<pod-id>-11434.proxy.runpod.net
# 3. SSH into the pod and pull a model:
ollama pull qwen3.6:27b
# 4. Locally, point Bandit at it:
export OLLAMA_URL="https://<pod-id>-11434.proxy.runpod.net"
export BANDIT_MODEL="qwen3.6:27b"
banditTear the pod down when you're done. ~$2/hr for an H100 SXM × 15-20 min agent session = under $1.
Vast.ai / Lambda Labs: same pattern. Find an Ollama-preloaded image (or apt install Ollama yourself), expose port 11434, set OLLAMA_URL to the host URL.
Recommended models for rented GPU:
| Model | Size | What it's good at |
| ------------------- | ------ | --------------------------------------------------------------------- |
| qwen3.6:27b | ~17 GB | Native tool calling, vision, 256K context. Best general-purpose pick. |
| qwen2.5-coder:32b | ~20 GB | Code-specialist post-train. Strongest on file edits and refactors. |
| qwen3.6:35b | ~24 GB | Bigger Qwen 3.6 variant — slower, marginally better reasoning. |
Avoid for agent work: gpt-oss:120b and similar reasoning-tuned models. They're post-trained for OpenAI's harmony tool-call format, not the XML protocol Bandit uses for non-native models — they tend to narrate intent without emitting tool calls. Great for math/proofs in chat, poor for filesystem agent loops.
University clusters (Slurm, Ollama in a GPU job)
The usual setup: a batch job starts ollama serve on a GPU node with a random port and writes node:port to a file; you work from an interactive job.
srun --pty bash # or salloc / your site's interactive command; not a login node
export OLLAMA_HOST=$(cat ~/.ollama_server) # node:port written by the Ollama job
export BANDIT_HOME=/scratch/$USER/bandit # sessions and caches off the home quota
bandit-agent --model qwen2.5-coder:32bEndpoint. Bandit reads
OLLAMA_HOSTthe way Ollama's own client does (node:port,http://node:port,0.0.0.0), after--ollama-url,OLLAMA_URLand the config file. The line under the banner says which endpoint is in use and where it came from.Context window. Ollama's own default is 4,096 tokens on GPUs under 24 GB. Bandit always asks for a window per request (at least 16,384; 24,576 for most 7–14B models, 32,768 for 27B and up; never more than the model's own), so
OLLAMA_CONTEXT_LENGTHand custom Modelfiles are not needed. Bandit's instructions and tool definitions take about 5,500 tokens before your files. Set the window with--num-ctx,BANDIT_NUM_CTXor"ollama": { "numCtx": 32768 }. After the first reply Bandit checks what Ollama loaded and warns when part of the model spilled to the CPU, with a window that would fit. A GPU under 24 GB does not limit you to 4,096: Ollama loads the window Bandit asks for and puts what does not fit on the CPU, which is slower (qwen3:8b at 16,384 on an 8 GB card: about a fifth on the CPU, replies about 5× slower), never a failure. The window is really fixed only when the server ignores the request's: Ollama's OpenAI-compatible/v1endpoint,llama-server -c, LM Studio or vLLM with a set window, or a gateway that drops options. If the KV cache is what does not fit,OLLAMA_KV_CACHE_TYPE=q8_0in the Ollama server's environment halves it (qwen3:8b at 16,384 then fits an 8 GB card); set it whereollama serveruns, not in Bandit.Small windows: small-context mode. With Ollama below 16,384 tokens (other providers only when forced, since their window for a model Bandit does not list is a guess; or with
--small-context,BANDIT_SMALL_CONTEXT=on,"ollama": { "smallContext": "on" }) Bandit switches to a small-context agent and says so at startup: a short prompt and 7 tools (read, search, list, edit, write, run, test) with short definitions, 800–1,200 tokens in all instead of 5,500; tool output cut to a share of the window; older results shortened first; the reply's room reserved; thinking off unless/think on. Subagents, git, web, document, memory and artifact tools are off. Measured on 5 public benchmark tasks (one-file bug fixes, a new function with tests, a four-file rename), qwen3.6:27b passed 14/15 at 4,096 (three seeds), 5/5 at 6,144 and 5/5 at 8,192 tokens, where the full agent passed 0/5 and 0/5 (and 4/5 at 8,192, with three runs stopped at the iteration limit); qwen3:8b passed 2/10, 4/10 and 2/10 (full agent 0/5, 0/5, 2/5), so an 8B model fails most tasks at these windows either way. What it cannot do: at 4,096 there is room for about one 40-line file at a time, so a task that needs two files in view at once goes back and forth between them (the Go task ran into the iteration limit in all three 4,096 runs, twice after making the fix); long command output is cut to its start and end; earlier steps are shortened to one line; there are no subagents. At 16,384 and above nothing changes;--no-small-contextkeeps the full agent at any window. It is a different thing from the REPL's/compactcommand, which shortens the current conversation.Login nodes. Started over SSH on a Slurm login node (outside a job), Bandit prints a one-line reminder to start an interactive job. It never blocks.
BANDIT_NO_LOGIN_HINT=1hides it.Home quota.
BANDIT_HOMEmoves what grows (sessions/,crashes/,training/, and the Apptainer sandbox image whenAPPTAINER_CACHEDIRis unset) to scratch. Settings stay in~/.bandit(config.json,settings.json,mcp-servers.json,lessons.md,terms.json; a few KB) so a scratch purge never signs you out. Each project also gets a.bandit/folder with turn logs and checkpoints.Resume after reconnecting. Sessions are saved with the directory they started in.
bandit --continue(or-c) resumes the latest session of the current directory from any login;bandit --resumelists this directory's sessions first.Scripts and batch jobs. Piped stdin is sent with the prompt, and
--output jsonprints one JSON object (answer, tool calls, files changed, status, timing) on stdout with progress on stderr:cat slurm-${SLURM_JOB_ID}.out | bandit-agent "the job failed: explain the error" bandit-agent --auto --output json "summarize results.csv" > summary.jsonExit codes:
0done,1failed (model, endpoint or runtime),2bad usage,3stopped before finishing (iteration limit, or no answer). With stdin as input there is nobody to answer an approval prompt, so approvals are denied; use--auto, orBANDIT_PERMISSION_MODE=dangerousinside a sandbox, for unattended runs.Sandboxed commands without Docker.
bandit --sandboxruns agent commands under Apptainer or SingularityCE when there is no Docker. Seebench/agent/CLUSTER.md.
Security & privacy
- Network — with
provider=ollamathe conversation goes only to your Ollama server. The other connections Bandit can make, and how to turn each off, are listed under Network and offline mode. Tools that contact another host ask first in the default mode, andBANDIT_OFFLINE=1turns all of it off. - Approval gate — all file writes show a unified diff before touching disk (unless
BANDIT_AUTO_APPROVE=1). - Command allowlist —
run_commandonly executes from an internal allowlist (git, gh, kubectl, helm, brew, standard *nix tools). Arbitrary shell is refused. - Container sandbox (opt-in) —
bandit --sandboxorBANDIT_SANDBOX=1runs every agentrun_command/watch_commandinside a short-lived Docker or Podman container with the same policy as the Stealth IDE: one workspace mount, no network, no Linux capabilities, read-only root,no-new-privileges, bounded CPU/memory/pids/output/time, your uid:gid. On a cluster with neither, Apptainer or Singularity run the same image (pulled by digest into a SIF under$APPTAINER_CACHEDIR/stealth-sandboxor~/.bandit/sandbox-apptainer), as you, with the network namespace checked before every command and the command refused when it cannot be isolated (sandbox/README.md, Apptainer).BANDIT_SANDBOX_RUNTIME=apptainer(ordocker,podman,singularity, comma-separated) picks the runtime. Fail-closed once enabled — no runtime means the command is refused, never quietly run on the host. Image:ghcr.io/burtson-labs/stealth-sandbox, chosen from the project's files — the base (node, git, ripgrep, python3, C toolchain) or the dotnet, go, rust or python image built on it; override withBANDIT_SANDBOX_IMAGE(a stack name or an image reference).BANDIT_SANDBOX_INSTALL_NETWORK=1gives install commands (npm install,pip install,dotnet restore,go mod download, …, run on their own) the network; every other command stays offline. Enterprise-managed installs should setBANDIT_SANDBOX=1in the environment. Inside the sandboxrun_commandalso runs programs the workspace itself provides: a binary the agent just built (./heat,build/heat), a repo script (tools/dryrun.sh), a venv interpreter (.venv/bin/python), and a shell given such a script (bash tools/dryrun.sh;bash -cstays blocked). The allow-list exists to keep the agent from running arbitrary code with your files, keys and network; in the container the workspace mount is the only thing a program can change and there is no network, so a workspace program has the same reach asmake,npm testorpython3 script.py, which the list already allows. On the host (no--sandbox) the allow-list applies as before and the model is told to ask you to run the program. Paths must be relative, contain a/and stay inside the workspace (no..,~, absolute paths or drive letters); the blocked-pattern check runs on every command either way. - Pre-tool security guard (default-on) — configure
security.guardin your settings file to extend or explicitly disable blocking of catastrophic commands (rm -rf /,curl … | sh, disk wipes, credential exfil, writes to/etcor~/.ssh) before they run. Defense-in-depth against the model, applied in both CLI and IDE. - Custom hooks — your own
PreToolUseshell scripts can veto any tool call; configure globally (~/.bandit/settings.json) or per-project. - Secret hygiene — API keys are redacted in
/configoutput and never logged. The optional telemetry exporter sends no prompt or reply text, and runs the tool arguments it does send through secret redaction. - Local sessions — stored as JSONL under
~/.bandit/sessions/. Inspect at any time. - Turn logs — each turn is logged to
<project>/.bandit/turns/(your prompt, tool calls and short excerpts of tool output). When Bandit creates.bandit/, it writes.bandit/.gitignoreso these logs, checkpoints and pasted images are not committed. It never overwrites an existing.bandit/.gitignore. If your project's.bandit/predates this, addturns/andcheckpoints/to it yourself.
Network and offline mode
What Bandit contacts on its own:
| Connection | When | What is sent | Turn it off |
| --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| Your model server (OLLAMA_URL, OPENAI_BASE_URL, or Bandit Cloud with --provider bandit) | Every turn, plus a short follow-up request after each turn for next-prompt suggestions and project memory | The conversation: prompts, system prompt, file contents and command output the agent read, tool results | Choose the server |
| registry.npmjs.org | When an interactive session starts (not in one-shot bandit "prompt" mode) | A plain request for the latest version number. No account, device or project data | BANDIT_NO_UPDATE_CHECK=1 or offline mode |
| models.dev | At start-up with --provider openai-compatible, only when OPENAI_BASE_URL is a public host. Never for a server on this machine or a private network address | A request for the public model catalogue. No user data | BANDIT_NO_MODELS_DEV=1 or offline mode |
Telemetry is off unless you turn it on (Telemetry). Bandit has no analytics or crash reporting.
Tools that contact another host ask first. In the default ask mode, Bandit shows an approval card before any of these run, with the full address (or, for git, the remote's URL):
web_fetch,fetch_image(any URL; private, loopback and link-local addresses are refused unlessBANDIT_ALLOW_PRIVATE_WEB_FETCH=1),web_search(Tavily, only with a Tavily key);git_push,git_pull;- the Bandit Cloud artifact tools (
publish_artifact,share_artifact,email_artifact, …), which exist only when Bandit Cloud is the provider; - MCP tools, unless the server's entry says
"local": true(then its read-named tools, such aslist…/get…, run without asking; its other tools still ask).
Plan mode refuses all of them. In auto mode (--auto), the CLI's reviewer model decides whether such a call is one you asked for, and can let it run without a card; list_artifacts, get_artifact and MCP read-named tools run there without review. dangerous mode runs everything. An allow rule in .bandit/settings.json (for example "web_fetch:https://docs.example.com/*") lets matching calls run without asking. run_command always asks in the default mode; --sandbox runs commands with no network. Graph runs (bandit graph, bandit spec) cannot ask, so they leave out web_fetch and web_search unless BANDIT_GRAPH_WEB=1.
MCP servers and your Bandit key. Bandit gives your Bandit Cloud key to an MCP server only when its entry asks for it with "auth": "bandit" (and to Burtson Labs' own server, which /mcp google connect sets up).
Offline mode. Set BANDIT_OFFLINE=1, or put "offline": true in ~/.bandit/settings.json or the project's .bandit/settings.json. An administrator can set it for every user of a machine with { "network": { "offline": true } } in /etc/bandit/policy.json (macOS: /Library/Application Support/Bandit/policy.json; Windows: C:\ProgramData\Bandit\policy.json). Any one of these turns it on, and none of the others can turn it off. A policy file that exists but cannot be read as JSON also turns it on. In offline mode:
- Bandit connects only to your model server and to loopback addresses. An HTTP request to any other host fails at once with a message naming the host.
- The update check, models.dev and telemetry are off.
web_fetch,web_search,fetch_imageand the artifact tools are not offered to the model.git_push,git_pulland MCP tools from servers not marked"local": trueare refused.- Bandit Cloud features are refused with a message: the
banditprovider, sign-in,/remote,/usage,/artifact,bandit runner,bandit artifactandbandit upgrade.
Offline mode does not control the commands the agent runs with run_command (they ask first in the default mode; use --sandbox for no network), what a stdio MCP server you configured does on its own, or docker/podman/apptainer pulling the sandbox image (set BANDIT_SANDBOX_REGISTRY_MIRROR or load the image beforehand).
Requirements
- Node.js 20+
- Ollama running locally (or remote via
OLLAMA_URL) — unless you useBANDIT_PROVIDER=bandit rg(ripgrep) onPATHfor fast code search; falls back togrepif absent
Troubleshooting
Ollama not detected — Make sure it's running: ollama serve. The CLI checks on startup and surfaces a setup hint if it can't connect.
Model not installed — Pull it: ollama pull <model>. Run /model <name> in the REPL to switch without restarting.
Slow responses — Check your model size against available VRAM. Switch to a smaller model from the recommended list.
Stuck approval prompt in CI — Set BANDIT_AUTO_APPROVE=1 to skip the diff-approval gate.
"accept the Terms of Use first" in a script or CI — Set BANDIT_ACCEPT_TERMS=1 or pass --accept-terms. See Terms of Use.
Support
- Issues, feature requests, and questions: [email protected]
- More from Burtson Labs: burtson.ai
Bandit CLI is built by Burtson Labs. Source for the runtime packages is currently private — open source release planned.
Clipboard and long commands
Ctrl+V attaches a clipboard screenshot at the idle prompt or while a turn is
running. Typing during capture stays in the composer. Dragging over the screen
copies on release with a "Copied to clipboard" notice at the lower right (see
the input section above). /copy opens a scrollable selection view of the
whole latest response, including the parts that have scrolled away: drag to
highlight, release to copy, and look for the confirmation below the text.
Escape returns to chat. This view preserves code indentation and soft-wrapped
text.
For downloads, builds and conversions, the agent can use run_background_command
to start one process and return immediately. Its completion or failure wakes the
agent with the real exit status and bounded output, so it can continue the
original task. /tasks shows these processes and /tasks cancel <id> stops one.
They share normal command permissions and stop when the session changes or
closes. This tool is available in host execution mode; a requested container
sandbox keeps its existing bounded command tools and never falls back to a host
background process. monitor_job remains the tool for polling an existing remote
job without repeatedly spending model turns.
