# bx — Brave Search CLI A zero-dependency, token-efficient CLI for the [Brave Search API](https://brave.com/search/api/), built for AI agents and LLMs. One binary, JSON in/out, no runtime dependencies. `bx context` replaces search + scrape + extract in a single call with token-budgeted output — purpose-built for RAG and LLM grounding. ## Quick Start **macOS/Linux** (macOS 11.7.1 or later) ```bash curl -fsSL https://raw.githubusercontent.com/brave/brave-search-cli/main/scripts/install.sh | sh ``` **Windows (PowerShell)** ```powershell powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/brave/brave-search-cli/main/scripts/install.ps1 | iex" ``` **npm** (requires Node ≥ 16) ```bash npm install -g @brave/brave-search-cli ``` ```bash bx config set-key YOUR_API_KEY # get a key at https://api-dashboard.search.brave.com bx "your search query" # shorthand for: bx context "your search query" bx --help # see all commands; bx --help for flags # To search for a word matching a subcommand name: bx -- web or bx context "web" ``` ## For AI Agents **Use `context` by default.** It returns pre-extracted, relevance-scored web content ready for LLM prompt injection. One API call replaces the search → scrape → extract pipeline. ```bash # RAG grounding with token budget bx context "Python TypeError cannot unpack non-iterable NoneType" --max-tokens 4096 # Direct AI answer (OpenAI-compatible, streams by default) bx answers "explain Rust lifetimes with examples" # Raw web search when you need site: scoping or result filtering bx web "site:docs.rs axum middleware" --count 5 ``` ### When to Use Which Command | Your need | Command | Why | |-----------|---------|-----| | Look up docs, errors, code patterns | `context` | Pre-extracted text, token-budgeted | | Get a synthesized explanation | `answers` | AI-generated, cites sources | | Search a specific site (site:) | `web` | Supports search operators | | Find discussions/forums | `web --result-filter discussions` | Forums often have solutions | | Check latest versions/releases | `context` or `news --freshness pd` | Fresh info beyond training data | | Research security vulnerabilities | `context` or `news` | CVE details, advisories | | Boost/filter specific domains | `--goggles` on context/web/news | Custom re-ranking, no other API has this | ### Response Shapes **`bx context`** — RAG/grounding (recommended) ```json { "grounding": { "generic": [ { "url": "...", "title": "...", "snippets": ["extracted content...", "..."] } ] } } ``` **`bx answers`** — AI answer (streaming, one JSON chunk per line) ```json {"choices": [{"delta": {"content": "Rust lifetimes ensure references..."}}]} ``` **`bx answers --no-stream`** — AI answer (single response) ```json {"choices": [{"message": {"content": "Rust lifetimes ensure references..."}}]} ``` **`bx web`** — Full search results ```json { "web": { "results": [{"title": "...", "url": "...", "description": "..."}] }, "news": { "results": [...] }, "videos": { "results": [...] }, "discussions": { "results": [...] } } ``` ### Agent Workflow Examples **Debugging an error:** ```bash # Agent encounters: "TypeError: cannot unpack non-iterable NoneType" bx "Python TypeError cannot unpack non-iterable NoneType" --max-tokens 4096 ``` **Evaluating a dependency:** ```bash # Is this crate maintained? Any security issues? bx context "reqwest crate security issues maintained 2026" --threshold strict bx news "reqwest Rust crate" --freshness pm ``` **Corrective RAG loop:** ```bash # 1. Broad search bx "axum middleware authentication" --max-tokens 4096 # 2. Too general? Narrow down bx "axum middleware tower layer authentication example" --threshold strict --max-tokens 4096 # 3. Still need synthesis? Ask for an answer bx answers "how to implement JWT auth middleware in axum" --enable-research ``` **Checking for breaking changes before upgrading:** ```bash bx context "Next.js 15 breaking changes migration guide" --max-tokens 8192 bx news "Next.js 15 release" --freshness pm ``` **Focused search with Goggles (custom re-ranking):** ```bash # Boost official docs, prefer /docs/ paths, suppress SEO spam bx "Python asyncio gather vs wait" \ --goggles '$boost=3,site=docs.python.org /docs/$boost=3 /blog/$downrank=2 $discard,site=geeksforgeeks.org $discard,site=w3schools.com' --max-tokens 4096 ``` **Token budget control:** ```bash bx context "topic" --max-tokens 4096 --max-tokens-per-url 1024 --max-urls 5 ``` ## Goggles — Custom Search Re-Ranking Brave Goggles let you define custom re-ranking rules for search results. Boost domains, URL paths, or content patterns; downrank noise; discard SEO spam — from simple domain allow/deny lists to complex multi-rule ranking profiles. No other search API offers this. Supported on `context`, `web`, and `news`. ### Domain Shortcuts — `--include-site` / `--exclude-site` For the common case of restricting to or excluding specific domains, use the convenience flags (available on `context`, `web`, `news`): ```bash # Only include results from these domains (allowlist) bx "rust axum" --include-site docs.rs --include-site github.com # Exclude specific domains bx web "rust tutorial" --exclude-site w3schools.com --exclude-site medium.com ``` These generate Goggles rules internally. For more advanced re-ranking (boosting, path patterns, wildcards), use `--goggles` directly. The three flags are mutually exclusive. ### Why Agents Should Use Goggles - **Domain allow/deny lists**: Boost, downrank, or discard entire domains — but that's just the start - **URL path patterns**: Boost `/docs/` paths, downrank `/blog/` paths, discard `/ads/` — fine-grained URL targeting with wildcards - **Full power of `site:`** — Brave converts `site:` operators to Goggles internally, but explicit Goggles unlock the full DSL: hundreds of rules, path patterns, content matching, boost/downrank strengths — without bloating the query string - **Keep queries clean**: A single `--goggles` parameter (or URL) replaces long `site:X OR site:Y` chains in the query, saving tokens - **Reusable & shareable**: Host a `.goggle` file on GitHub — share among team members, agents, CI pipelines. Document your ranking preferences as code - **Community-maintained**: Leverage existing Goggles like [Tech Blogs](https://raw.githubusercontent.com/brave/goggles-quickstart/main/goggles/tech_blogs.goggle) that are continuously updated ### Inline Rules (zero setup) ```bash # Domain boosting — prioritize official sources for a Python error bx "TypeError cannot unpack non-iterable NoneType" \ --goggles '$boost=3,site=docs.python.org $boost=2,site=github.com $discard,site=geeksforgeeks.org $discard,site=w3schools.com' # Path-based boosting — prefer /docs/ over /blog/ across all sites bx context "axum middleware tower" \ --goggles '/docs/$boost=5 /api/$boost=3 /blog/$downrank=3' --max-tokens 4096 # Ecosystem focus — boost Rust sources for crate research bx context "serde custom deserializer" \ --goggles '$boost=5,site=docs.rs $boost=5,site=crates.io $boost=3,site=github.com' --max-tokens 4096 # Downrank blog spam in news results bx news "npm security advisory" --freshness pd \ --goggles '$downrank=5,site=medium.com' ``` ### DSL Quick Reference | Rule | Effect | Example | |------|--------|---------| | `$boost=N,site=DOMAIN` | Promote domain (N=1-10) | `$boost=3,site=docs.rs` | | `$downrank=N,site=DOMAIN` | Demote domain (N=1-10) | `$downrank=5,site=medium.com` | | `$discard,site=DOMAIN` | Remove domain entirely | `$discard,site=w3schools.com` | | `/path/$boost=N` | Boost matching URL paths | `/docs/$boost=5` | | `*pattern*$boost=N` | Wildcard URL matching | `*api*$boost=3` | | Generic `$discard` | Allowlist mode — discard all unmatched | `$discard` (as last rule) | Separate multiple rules with newlines. Full DSL + pattern syntax: [goggles-quickstart](https://github.com/brave/goggles-quickstart). ### From a File (`@file`) — ideal for agents Agents can generate a `.goggle` file on the fly and reference it: ```bash # Agent writes rules to a file, then uses it across multiple queries cat > /tmp/rust.goggle << 'EOF' $boost=5,site=docs.rs $boost=5,site=crates.io $boost=3,site=github.com /blog/$downrank=3 $discard,site=w3schools.com $discard,site=geeksforgeeks.org EOF bx context "axum middleware tower" --goggles @/tmp/rust.goggle --max-tokens 4096 bx context "serde custom deserializer" --goggles @/tmp/rust.goggle --max-tokens 4096 ``` ### From stdin (`@-`) — pipe generated rules ```bash # Pipe rules from another command echo '$boost=5,site=docs.rs $boost=3,site=github.com' | bx web "tokio runtime" --goggles @- ``` ### Hosted Goggles (reusable, shareable) Host a `.goggle` file on GitHub/GitLab, [submit it to Brave](https://search.brave.com/goggles/create), then reference by URL: ```bash # Use the official Tech Blogs goggle bx web "distributed systems" \ --goggles 'https://raw.githubusercontent.com/brave/goggles-quickstart/main/goggles/tech_blogs.goggle' ``` Community Goggles: [brave/goggles-quickstart](https://github.com/brave/goggles-quickstart) | [Discover page](https://search.brave.com/goggles/discover) ## Getting an API Key 1. Sign up at 2. Choose a plan — all plans include **$5/month free credits** (~1,000 free queries) 3. Different endpoints may require different plans (e.g. Search vs Answers) 4. Go to "API Keys" in the dashboard and generate a key ## Configuration Settings are resolved in priority order: CLI flag > environment variable > config file > default. | Setting | CLI Flag | Env Var | Config Key | Default | |---------|----------|---------|------------|---------| | API key | `--api-key` | `BRAVE_SEARCH_API_KEY` | `api_key` | *(interactive prompt)* | | Base URL | `--base-url` | `BRAVE_SEARCH_BASE_URL` | `base_url` | `https://api.search.brave.com` | | Timeout | `--timeout` | — | `timeout` | `30` (`300` for `answers --enable-research`; max `86400`) | Streamed `answers` responses have no total deadline — every other command keeps one. The timeout bounds *idle* time: connecting, and then each read. A response that keeps arriving is never cut off however long it runs, and a server that only sends keep-alives never ends. Deep research pauses for minutes while it synthesises, so it defaults to `300` seconds of silence; `--timeout` or a `timeout` in your config file always wins, and applies to connecting too. **Config file** (`bx config path` to see location): - Linux: `~/.config/brave-search/config.json` - macOS: `~/Library/Application Support/brave-search/config.json` - Windows: `%APPDATA%\brave-search\config.json` ```json { "api_key": "BSA...", "base_url": "https://api.search.brave.com" } ``` Use `--config ` to load a different config file (e.g. one for dev, one for prod): ```bash bx --config ~/.config/brave-search/dev.json web "test" ``` **Security tip:** Prefer the env var or config file over `--api-key`, which is visible in process listings. Use `bx config set-key` without an argument to enter the key interactively, avoiding shell history. Saving a key writes a new file and renames it into place: mode `0600` on Linux and macOS, with the default directory `~/.config/brave-search` kept `0700` (a directory you pass via `--config` is left as it is). Because the file is *replaced* rather than written in place, a symlink, hard link, or bind mount at the config path does not survive a save, and neither do extended attributes or ACLs on the old file — deliberate, so a key is never written through an indirection `bx` does not control. On Windows no ACL is set and the file inherits whatever `%APPDATA%\brave-search` grants — restrict that directory yourself if the machine has other users. ### Local Proxy You can point `bx` at a local reverse proxy instead of the Brave API directly. This is useful for sandboxed environments where credentials are managed outside the sandbox. Allowed localhost forms: - `http://127.0.0.1[:][/path]` - `http://[::1][:][/path]` - `http://localhost[:][/path]` (resolved and rewritten to a literal loopback IP) ```bash # Point bx at a local proxy on port 8080 with path prefix /brave BRAVE_SEARCH_BASE_URL=http://127.0.0.1:8080/brave bx "your query" # Or in config.json # { "base_url": "http://127.0.0.1:8080/brave" } ``` The proxy receives requests at the configured base URL (e.g. `http://127.0.0.1:8080/brave/res/v1/web/search?q=...`) and can inject the `X-Subscription-Token` header before forwarding to `api.search.brave.com`. Since `bx` still requires an API key, set a placeholder: `BRAVE_SEARCH_API_KEY=unused`. Only loopback addresses are allowed for `http://` URLs — non-loopback IPs and arbitrary hostnames are rejected. The `localhost` hostname is resolved at startup and rewritten to a literal IP to prevent DNS rebinding. ## Commands | Command | Description | Output Shape | |---------|-------------|--------------| | `context` | **RAG/LLM grounding** — pre-extracted web content | `.grounding.generic[]` → `{url, title, snippets[]}` | | `answers` | **AI answers** — OpenAI-compatible, streaming | `.choices[0].delta.content` (stream) | | `web` | Full web search — all result types | `.web.results[]`, `.news.results[]`, etc. | | `news` | News articles with freshness filters | `.results[]` → `{title, url, age}` | | `images` | Image search (up to 200 results) | `.results[]` → `{title, url, thumbnail.src}` | | `videos` | Video search with duration/views | `.results[]` → `{title, url, video.duration}` | | `places` | Local place/POI search (200M+ POIs) | `.results[]` → `{title, postal_address, contact}` | | `suggest` | Autocomplete/query suggestions | `.results[]` → `{query}` | | `spellcheck` | Spell-check a query | `.results[0].query` | | `pois` | POI details by ID | (use IDs from `places`) | | `descriptions` | AI-generated POI descriptions | `.results[].description` | | `config` | Manage configuration | `set-key`, `show-key`, `show`, `path` | ## Usage Examples ```bash # Default subcommand is context bx "tokio spawn async task example" # RAG grounding with token budget bx context "Python TypeError cannot unpack non-iterable NoneType" --max-tokens 4096 --threshold strict # AI answer (streaming) bx answers "explain Rust lifetimes with examples" # AI answer (non-streaming) bx answers "compare SQLx and Diesel for Rust database access" --no-stream | jq . # Research mode (multi-step, thorough) bx answers "what changed in React 19 vs React 18?" --enable-research # Web search with site: scoping bx web "site:docs.rs axum middleware" | jq '.web.results[].url' # News from the past day bx news "npm security advisory" --freshness pd | jq '.results[].title' # Image search bx images "system architecture diagram microservices" | jq '.results[].thumbnail.src' # Autocomplete bx suggest "how to implement" --count 10 | jq '.results[].query' # Local places bx places --location "San Francisco CA US" -q "coffee" | jq '.results[].title' # Location-aware web search bx web "restaurants near me" --lat 37.7749 --long -122.4194 --city "San Francisco" # Filter result types bx web "rust" --result-filter "web,discussions" ``` ## Extra Parameters and Custom Endpoints ### `--extra KEY=VALUE` Pass additional API parameters not covered by named flags. Repeatable. Merged into the JSON body (POST endpoints) or query string (GET endpoints). Warns if the key collides with an existing flag. For POST endpoints, values are auto-typed: integers and floats become JSON numbers, `true`/`false` become JSON booleans, everything else stays a string. GET values are always strings. ```bash bx web "rust" --extra custom_param=value bx web "rust" --extra count=5 # overrides --count (with warning) bx images "sunset" --extra safesearch=off ``` ### `--endpoint PATH` Override the default API path. For internal or beta Brave endpoints. Must start with `/`. ```bash bx web "test" --endpoint /res/v2/web/search ``` ## Answers Endpoint The `answers` command supports two modes: ### Simple Mode Pass a question directly: ```bash bx answers "explain Rust lifetimes with examples" # streaming (default) bx answers "compare SQLx and Diesel" --no-stream # single JSON response bx answers "what changed in React 19?" --enable-research # deep research bx answers "Hello" --model brave # faster model ``` `--enable-citations`, `--enable-entities`, and `--enable-research` all require streaming, so none combines with `--no-stream`. Citations and entities are not supported in research mode, but are fine together. Rejected combinations exit 2, nothing sent: ```bash bx answers "what changed in React 19?" --enable-research --enable-citations # error: the argument '--enable-research' cannot be used with '--enable-citations' ``` ### Stdin Mode Pass `-` to read a full JSON request body from stdin: ```bash echo '{"messages":[{"role":"user","content":"review this code for security issues"}],"model":"brave-pro"}' | bx answers - cat request.json | bx answers - ``` ## Exit Codes | Code | Meaning | Agent action | |------|---------|-------------| | 0 | Success | Process results | | 1 | The request was made; the result is unusable | Fix query/parameters | | 2 | Usage error — nothing was sent | Fix the CLI arguments, config, or input | | 3 | Auth/permission error (401/403) | Check API key or plan: `bx config show-key` | | 4 | Rate limited (429) | Retry after delay | | 5 | Server/network error | Retry with backoff | | 127 | npm launcher could not start bx | Reinstall, or check platform support | ## Error Handling Errors are printed to stderr with a human-readable summary, recovery hints, and the full JSON error body: ``` error: rate limited (429) — Request rate limit exceeded for plan. hint: retry after a short delay, or upgrade plan for higher rate limits {"type":"ErrorResponse","error":{"code":"RATE_LIMITED","status":429,...}} ``` Exit codes are differentiated — see [Exit Codes](#exit-codes) above. The line between 1 and 2 is whether a request was made: 2 means bx could not act on what it was given and never contacted the API, so retrying without changing the command will not help. ## Building from Source ```bash # Debug build cargo build # Release build (~2 MB) cargo build --release ``` ### Static binaries via Docker Requires Docker, GNU Make, and a POSIX shell (`sh`/`bash`). Produces fully static, cross-compiled binaries. On Windows, run these targets from Git Bash or WSL. Running `make` from native `cmd`/PowerShell can fail because the `Makefile` uses POSIX utilities (`grep`, `sed`, shell redirection). ```bash make linux-amd64 # → dist/bx--linux-amd64 make linux-arm64 # → dist/bx--linux-arm64 make darwin-amd64 # → dist/bx--darwin-amd64 make darwin-arm64 # → dist/bx--darwin-arm64 make windows-amd64 # → dist/bx--windows-amd64.exe make windows-arm64 # → dist/bx--windows-arm64.exe make dist-all # → all of the above ``` Both macOS binaries **require macOS 11.7.1 or later** — zig sets that deployment target and `MACOSX_DEPLOYMENT_TARGET` does not override it. The `smoke-darwin` job asserts it, so a toolchain bump that raises the floor fails CI. ## Testing ```bash # End-to-end API smoke suite (requires bash + jq + BRAVE_SEARCH_API_KEY/BRAVE_API_KEY) make test # Unit tests (no network/API key required) make test-unit ```