{"id":51416910,"url":"https://github.com/lance0/nbox","last_synced_at":"2026-07-04T20:01:27.685Z","repository":{"id":365527970,"uuid":"1271422620","full_name":"lance0/nbox","owner":"lance0","description":"Terminal UI, CLI, and MCP server for NetBox — fast search, IPAM lookups, and device context.","archived":false,"fork":false,"pushed_at":"2026-06-24T19:13:36.000Z","size":2252,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-24T20:08:10.880Z","etag":null,"topics":["cli","dcim","ipam","mcp","netbox","network-automation","ratatui","rust","terminal","tui"],"latest_commit_sha":null,"homepage":"https://crates.io/crates/nbox","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lance0.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":"ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"ko_fi":"lance0"}},"created_at":"2026-06-16T16:41:12.000Z","updated_at":"2026-06-24T19:13:48.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/lance0/nbox","commit_stats":null,"previous_names":["lance0/nbox"],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/lance0/nbox","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lance0%2Fnbox","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lance0%2Fnbox/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lance0%2Fnbox/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lance0%2Fnbox/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lance0","download_url":"https://codeload.github.com/lance0/nbox/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lance0%2Fnbox/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35133834,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-04T02:00:05.987Z","response_time":113,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","dcim","ipam","mcp","netbox","network-automation","ratatui","rust","terminal","tui"],"created_at":"2026-07-04T20:01:26.843Z","updated_at":"2026-07-04T20:01:27.665Z","avatar_url":"https://github.com/lance0.png","language":"Rust","funding_links":["https://ko-fi.com/lance0"],"categories":[],"sub_categories":[],"readme":"# nbox\n\nTerminal UI and CLI for NetBox — fast search, IPAM lookups, and device context.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/demo.gif\" alt=\"nbox TUI walkthrough — ranked search, a device's interfaces and IP addresses, and the hierarchical prefix tree\" width=\"900\"\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003csub\u003eThe k9s-style TUI: search → device detail → prefix tree. Recorded with \u003ca href=\"https://github.com/charmbracelet/vhs\"\u003eVHS\u003c/a\u003e from \u003ca href=\"docs/demo.tape\"\u003e\u003ccode\u003edocs/demo.tape\u003c/code\u003e\u003c/a\u003e against a throwaway test NetBox — re-render with \u003ccode\u003evhs docs/demo.tape\u003c/code\u003e.\u003c/sub\u003e\n\u003c/p\u003e\n\n[![Crates.io](https://img.shields.io/crates/v/nbox.svg)](https://crates.io/crates/nbox)\n[![CI](https://github.com/lance0/nbox/actions/workflows/ci.yml/badge.svg)](https://github.com/lance0/nbox/actions/workflows/ci.yml)\n[![License](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](LICENSE-MIT)\n[![Ko-fi](https://img.shields.io/badge/Ko--fi-tip-ff5e5b?logo=ko-fi)](https://ko-fi.com/lance0)\n\nAsk the questions you actually ask at the terminal — *what is this IP, where is\nthis device, what owns this prefix?* — from the shell, a k9s-style TUI, or an MCP\nserver for AI agents.\n\n**Status: pre-1.0.** Reads are the default; a narrow safe-write foundation\n(ADR-0001) has landed behind `--allow-writes` + confirmation — seven commands\ntoday (`nbox interface \u003cdevice\u003e \u003cinterface\u003e set description \"…\"`,\n`nbox device \u003cname\u003e set status \u003cvalue\u003e`, `nbox ip reserve \u003cprefix\u003e` /\n`nbox prefix reserve \u003ccidr\u003e` / `nbox ip-range reserve \u003cstart|id\u003e` to allocate\nthe next available IP or child prefix, and `nbox tag add`/`remove \u003ctype\u003e \u003cname\u003e\n\u003ctag\u003e` to manage tags on any object). Broader writes are on the\n[ROADMAP](ROADMAP.md).\n\n## Quick Start\n\nFirst run? Just launch the TUI — with no config it opens a first-run wizard that\ncaptures a profile, test-connects it, and drops you into the app. Paste a token\nand nbox saves it in your user-only `config.toml` (`0600` on Unix, redacted in\n`config show`); or point a profile at an env var instead with `token_env`.\n\n```bash\nnbox                              # first run: guided onboarding, then the TUI\n```\n\nPrefer the shell? Configure a profile by hand:\n\n```bash\nnbox profile add work https://netbox.example.com --token-env NETBOX_TOKEN\nnbox profile use work\nexport NETBOX_TOKEN=...           # env vars override the saved config token\n\n# Look things up from the shell\nnbox search edge01\nnbox device edge01\nnbox ip 10.44.208.55\nnbox prefix 10.44.208.0/24\n```\n\nSee [Installation](#installation) below for setup instructions.\n\n## Features\n\n- **IPAM-aware** — IP → most-specific parent prefix → VLAN → scope resolution, prefix utilization and children, a navigable prefix tree, and `next-ip` / `next-prefix` to preview free addresses and blocks (read-only). To actually claim one, `nbox ip reserve \u003cprefix\u003e` allocates the next available IP from a prefix, `nbox prefix reserve \u003ccidr\u003e` allocates the next available child prefix, and `nbox ip-range reserve \u003cstart|id\u003e` allocates the next available IP from an IP range (safe writes — ADR-0001).\n- **A k9s-style TUI** — a three-pane home (navigation rail → results → live preview), an overview dashboard, a hierarchical prefix tree, cross-object navigation between related objects (every detail's related-object tabs are navigable — open an interface from a device and see its cable path drawn as an A↔Z diagram), fuzzy filters, server-side name filtering on the browse list, recents, and an in-app profile + settings editor. Twelve themes; `NO_COLOR` honored.\n- **Agent-ready** — `nbox serve` is a read-first MCP server (read-only by default): the same lookups exposed as eleven read tools (plus every object as an `nbox://{kind}/{ref}` resource), returning the exact JSON view models the CLI does, so AI agents (Claude Code, Claude Desktop, …) query NetBox safely. Two opt-in write tools (`nbox_plan_write` / `nbox_apply_write`) support local stdio writes with `--local-writes` (profile token) or shared HTTP/OIDC writes with `--allow-writes` + a per-user vault. Being one static binary with zero runtime, it cold-starts MCP-ready in ~9 ms — before a Python NetBox MCP server finishes importing its dependencies ([benchmarks](docs/BENCHMARKS.md)). See [docs/MCP.md](docs/MCP.md); [SKILL.md](SKILL.md) is an installable Agent Skill that drives the CLI on matching requests.\n- **Scriptable** — `-o plain|json|csv`, `--fields`, `--raw`, versioned `--envelope`, and stable exit codes; stdout stays clean for piping, logs go to stderr. Plus structured exports — `nbox export prometheus-sd | address-list | device-inventory` emit fixed shapes for Prometheus SD, firewall/blocklist address lists, and device inventories. See [docs/SCRIPTING.md](docs/SCRIPTING.md).\n- **Fast on repeat** — a small in-memory read cache (per profile, ~30s) keeps TUI back-navigation and chatty agents from re-hitting NetBox; the detail footer shows \"cached Ns ago\" and `nbox_cache_clear` busts it.\n- **Multi-instance** — profiles for several NetBox instances (switch live in the TUI), journals folded into detail lookups, open-in-browser and copy, and tag listing.\n\nSee [docs/FEATURES.md](docs/FEATURES.md) for the full command reference,\n[docs/COMPARISON.md](docs/COMPARISON.md) for how nbox compares to the NetBox web\nUI and raw API calls, and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the\ninternals.\n\n### nbox vs the NetBox web UI vs raw API\n\nnbox doesn't replace NetBox — it's a fast read path into it. When to reach for which:\n\n| Task | nbox | NetBox web UI | `curl` / `pynetbox` |\n|------|------|---------------|---------------------|\n| \"What is this IP?\" (address → prefix → VLAN → scope) | one command / one keystroke | several clicks across pages | several requests + manual joins |\n| Search across object types at once | one ranked, deduped query | per-type search pages | one request per endpoint |\n| Use it over SSH on a jump host | yes — one static binary, no runtime | no (needs a browser) | only if Python/curl are present |\n| Machine output (JSON/CSV, stable exit codes) | built in | no | you build it |\n| Feed an AI agent | built in (`nbox serve`, MCP) | no | you build it |\n| Reserve / allocate / edit (writes) | narrow \u0026 safe behind `--allow-writes` + confirm (ADR-0001) | yes | yes |\n\nFull matrix and a \"when to use each\" guide: [docs/COMPARISON.md](docs/COMPARISON.md).\n\n## Real-World Use Cases\n\n### What is this IP?\n\n```bash\nnbox ip 10.44.208.55\n```\n\nResolves the address, its most-specific parent prefix (VRF-scoped), the prefix's\nVLAN, and its scope (site, location, region, …). Add `--vrf \u003cname\u003e` when the same\naddress exists in several VRFs.\n\n### Where is this device, and what's on it?\n\n```bash\nnbox device edge01\n```\n\nDevice plus its interfaces, IP addresses, cables, VLANs, and services in one\nlookup. In the TUI, the device screen splits these onto `i`/`p`/`c`/`v`/`s` tabs.\n\n### Find the next free address or block\n\n```bash\nnbox next-ip 10.44.208.0/24 --count 4      # next available addresses\nnbox next-prefix 10.0.0.0/8 --length 26    # first free /26\n```\n\nRead-only — nothing is reserved. Both take `--vrf` to scope the prefix.\n\n### Pull data into a script\n\n```bash\nnbox device edge01 --json | jq '.primary_ip4'\nnbox search edge01 -o csv --cols kind,display,url \u003e devices.csv\nnbox prefix 10.44.208.0/24 --json --envelope --raw   # versioned, single-line\n```\n\n### Drive it from an agent\n\n```bash\nclaude mcp add nbox -e NBOX_TOKEN=nbt_xxx.yyy -- nbox serve\n```\n\n`nbox serve` is a read-first MCP server (read-only by default; writes are opt-in\nwith local stdio `--local-writes` or shared HTTP/OIDC `--allow-writes` + caller\n`nbox:write` + a per-user vault); an MCP host launches it as a subprocess and\ngets the same JSON view models the CLI returns. See [docs/MCP.md](docs/MCP.md).\n\n## Installation\n\n### From crates.io (Recommended)\n\nRequires [Rust 1.88+](https://www.rust-lang.org/tools/install):\n\n```bash\n# Install Rust (if not already installed)\ncurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh\nsource ~/.cargo/env\n\n# Install nbox\ncargo install nbox\n```\n\n### Homebrew (macOS/Linux)\n\n```bash\nbrew install lance0/tap/nbox\n```\n\n### Docker (GHCR)\n\nMulti-arch image (amd64/arm64):\n\n```bash\ndocker run --rm ghcr.io/lance0/nbox:latest --help\n```\n\nMount a config or pass `-e NBOX_TOKEN=...` plus a profile (or `--config`) to run\nreal lookups against your NetBox.\n\n### Pre-built Binaries\n\nDownload from [GitHub Releases](https://github.com/lance0/nbox/releases):\n\n| Platform | Target |\n|----------|--------|\n| Linux x86_64 | `nbox-x86_64-unknown-linux-musl.tar.gz` |\n| Linux ARM64 | `nbox-aarch64-unknown-linux-musl.tar.gz` |\n| macOS Apple Silicon | `nbox-aarch64-apple-darwin.tar.gz` |\n| macOS Intel | `nbox-x86_64-apple-darwin.tar.gz` |\n| Windows x86_64 | `nbox-x86_64-pc-windows-msvc.zip` |\n\n```bash\n# Download, verify, and install (Linux x86_64 example)\ncurl -LO https://github.com/lance0/nbox/releases/latest/download/nbox-x86_64-unknown-linux-musl.tar.gz\ncurl -LO https://github.com/lance0/nbox/releases/latest/download/SHA256SUMS\nsha256sum -c SHA256SUMS --ignore-missing   # macOS: shasum -a 256 -c\ntar xzf nbox-*.tar.gz \u0026\u0026 sudo mv nbox /usr/local/bin/\n```\n\n### Quick Install Script\n\n\u003e **Note**: Piping a script from the internet to sh is convenient but bypasses\n\u003e your chance to read it first. Use one of the methods above, or\n\u003e [review the script](scripts/install.sh) before running. It downloads the latest\n\u003e release binary for your OS/arch and falls back to `cargo install` if there's no\n\u003e asset for your platform.\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/lance0/nbox/master/scripts/install.sh | sh\n```\n\n### From Source\n\n```bash\ngit clone https://github.com/lance0/nbox\ncd nbox \u0026\u0026 cargo install --path .\n```\n\n### Build Features\n\n`cargo install nbox` builds the **canonical single binary** — clipboard, the MCP\nserver (stdio + HTTP transport), and update checks are all on by default. There\nare no feature-variant builds to choose between.\n\n| Feature | Default | Description |\n|---------|---------|-------------|\n| `clipboard` | Yes | Copy values with `y` in the TUI (desktop clipboard via `arboard`; SSH/headless fallback via OSC 52) |\n| `http` | Yes | `nbox serve --http` loopback MCP transport (+ OIDC) |\n| `updates` | Yes | GitHub update notifications |\n\nThe API token lives in `config.toml` (`token = \"...\"`, user-only `0600` on Unix)\nor an env var — there is no OS-keyring storage.\n\n```bash\n# Lean stdio-only build (drops all of the above):\ncargo install nbox --no-default-features\n```\n\n### Shell Completions\n\n```bash\n# Bash\nnbox completions bash \u003e ~/.local/share/bash-completion/completions/nbox\n\n# Zsh (add ~/.zfunc to fpath in .zshrc first)\nnbox completions zsh \u003e ~/.zfunc/_nbox\n\n# Fish\nnbox completions fish \u003e ~/.config/fish/completions/nbox.fish\n\n# PowerShell (add to $PROFILE)\nnbox completions powershell \u003e\u003e $PROFILE\n\n# Elvish\nnbox completions elvish \u003e ~/.elvish/lib/nbox.elv\n```\n\nMan pages are available too. `nbox man \u003e nbox.1` writes the top-level page; pass\na directory — `nbox man man/` — to write the full set instead (`nbox.1` plus one\n`nbox-\u003csubcommand\u003e.1` per subcommand, so `man nbox-device` works once installed).\n\n## Usage\n\n```bash\nnbox                              # launch the TUI\nnbox status                       # connection + backend capabilities + NetBox/Django/Python versions\n                                  # + token-validity preflight (NetBox 4.5+)\nnbox search \u003cquery\u003e [--limit N] [--status/--site/--region/--site-group/--location/--tenant/--role/--tag \u003cv\u003e] [--owner \u003cname\u003e] [--owner-group \u003cname\u003e] [--vrf \u003cid|rd|name\u003e] [--cols a,b,c] [--partial]\n                                  # one scope flag at a time; --site is exact, region/site-group/location include descendants where supported.\n                                  # --vrf filters IP/prefix results.\n                                  # --owner/--owner-group filter by user/group name (NetBox 4.5+).\n                                  # Full scope/filter semantics: docs/FEATURES.md\nnbox device \u003cname-or-id\u003e [--journal] [--journal-limit N]\n  device \u003cname\u003e set status \u003cvalue\u003e                    # write: status validated live via OPTIONS; --dry-run | --allow-writes --confirm [--message]\nnbox ip \u003caddress\u003e [--vrf \u003cname\u003e] [--journal]    # --vrf disambiguates duplicates across VRFs\n                                  # shows nat_inside/nat_outside (NetBox 4.6) when set\n  ip reserve \u003ccidr\u003e [--vrf \u003cname\u003e] [--description \"…\"] [--dns-name \"…\"] [--count N]\n                                  # write: reserve the next available IP (POST available-ips); --dry-run | --allow-writes --confirm [--message]\n                                  # --count N: reserve N IPs atomically (one list-body POST, all-or-nothing); any failure exits 1\nnbox prefix \u003ccidr\u003e [--vrf \u003cname\u003e] [--journal]   # includes utilization + children when present\n  prefix reserve [--length L] [--vrf \u003cname\u003e] [--description \"…\"]\n                                  # write: reserve the next available child prefix (POST available-prefixes); --dry-run | --allow-writes --confirm [--message]\nnbox next-ip \u003ccidr\u003e [--count N] [--vrf \u003cname\u003e]        # next available address(es)\nnbox next-prefix \u003ccidr\u003e [--length L] [--vrf \u003cname\u003e]   # available free block(s)\nnbox vlan \u003cvid-or-name\u003e [--site \u003cs\u003e] [--group \u003cg\u003e] [--journal]\nnbox site \u003cname-or-slug\u003e [--journal]\nnbox rack \u003cname-or-id\u003e [--journal]\nnbox rack-group \u003cslug-or-name-or-id\u003e             # NetBox 4.6+\nnbox circuit \u003ccid-or-id\u003e [--journal]              # incl. its A↔Z terminations + path\nnbox virtual-circuit \u003ccid-or-id\u003e                   # incl. its multi-point terminations (4.2+)\nnbox provider \u003cslug-or-name-or-id\u003e\nnbox aggregate \u003ccidr-or-id\u003e [--journal]\nnbox asn \u003cnumber\u003e [--journal]\nnbox ip-range \u003cstart-or-id\u003e [--journal]\n  ip-range reserve [--description \"…\"] [--dns-name \"…\"] [--count N]\n                                  # write: reserve the next available IP in an IP range (POST available-ips); --dry-run | --allow-writes --confirm [--message]\n                                  # --count N: reserve N IPs atomically (one list-body POST, all-or-nothing); any failure exits 1\nnbox tenant \u003cslug-or-name-or-id\u003e\nnbox contact \u003cname-or-id\u003e\nnbox vm \u003cname-or-id\u003e\nnbox vm-type \u003cslug-or-name-or-id\u003e                 # NetBox 4.6+\nnbox cluster \u003cname-or-id\u003e\nnbox interface \u003cdevice\u003e \u003cinterface\u003e\n  interface \u003cdevice\u003e \u003cinterface\u003e set description \"…\"   # write: --dry-run | --allow-writes --confirm [--message]\nnbox tags                         # list tags (slug, name, count)\nnbox tagged \u003ctag\u003e                 # objects carrying a tag, across kinds\n                                  # (NetBox 4.3+; tag = id|name|slug)\nnbox tag add \u003ctype\u003e \u003cname\u003e \u003ctag\u003e   # write: add a tag to any object (PATCH tags array)\nnbox tag remove \u003ctype\u003e \u003cname\u003e \u003ctag\u003e  # write: remove a tag from any object (PATCH tags array)\nnbox journal \u003ckind\u003e \u003cref\u003e         # recent journal entries for an object\n                                  # kinds incl. interface as \u003cdevice\u003e/\u003cname\u003e\n                                  # --journal folds recent entries into a detail lookup (cap 5)\n                                  # --journal-limit N overrides the cap (implies --journal)\nnbox history \u003ckind\u003e \u003cref\u003e         # system audit log (create/update/delete, who + when)\n                                  # /api/core/object-changes/ (NetBox 4.x); distinct from journal\nnbox open \u003ckind\u003e/\u003cref\u003e            # device, ip, prefix, vlan, site, rack, rack-group, circuit, virtual-circuit, provider,\n                                  # aggregate, asn, ip-range, tenant, contact, vm, vm-type, cluster, vrf, route-target,\n                                  # and interface/\u003cdevice\u003e/\u003cname\u003e (the name may contain slashes,\n                                  # e.g. xe-0/0/1)\nnbox raw GET \u003capi-path\u003e           # raw read-only API request (escape hatch)\nnbox export \u003cprometheus-sd|address-list|device-inventory\u003e\n                                  # structured read-only exports (Prometheus SD JSON, firewall/blocklist\n                                  # address list, device inventory) — fixed shapes for scripting\nnbox serve [--http \u003caddr\u003e]        # read-first MCP server for AI agents (read-only by default;\n                                  # --local-writes enables local stdio writes;\n                                  # --allow-writes enables shared HTTP/OIDC writes)\n                                  # --print-config prints the paste-ready mcpServers JSON and exits\nnbox config \u003cinit|path|show|token\u003e    # token: status reports the resolved source (never echoed)\nnbox profile \u003cadd|use|remove|list|show\u003e\nnbox completions \u003cbash|zsh|fish|powershell|elvish\u003e\nnbox man [DIR]                    # man pages: `nbox man \u003e nbox.1` (top-level),\n                                  # or `nbox man man/` for the full per-subcommand set\n```\n\n### Global flags\n\nThese apply to every command:\n\n| Flag | Effect |\n|------|--------|\n| `-o, --output \u003cfmt\u003e` | `plain` (default), `json`, or `csv` (tabular/list results only) |\n| `--json` | Shortcut for `-o json` |\n| `--fields a,b,c` | JSON: keep only these top-level fields |\n| `--raw` | JSON: compact (single line) instead of pretty |\n| `--envelope` | JSON: wrap as `{ schema_version, data }` |\n| `-p, --profile \u003cname\u003e` | Use a specific profile for this invocation |\n| `--config \u003cpath\u003e` | Use an alternate config file |\n| `--log-level \u003cspec\u003e` | `tracing` filter (`info`, `debug`, `nbox=debug`, …) |\n| `--log-file \u003cpath\u003e` | Write logs to this file (and stderr); stdout stays clean |\n| `--no-tui` | Refuse to launch the interactive TUI (a bare `nbox` or `nbox tui` exits `2` instead) |\n\n`-o csv` is for tabular/list results (e.g. `search`); a single object is rejected\n(use `--json` or plain). Custom fields appear as `cf.\u003cname\u003e` rows in plain output\nand a `custom_fields` object in JSON.\n\nLogs go to **stderr** by default (so stdout stays clean for piping and `--json`).\nPoint `--log-file \u003cpath\u003e` (or `log_file` in config) at a file to also capture\nthem there. The level resolves `--log-level` → config `log_level` → `NBOX_LOG` →\n`RUST_LOG` → `warn`. nbox never writes logs to stdout.\n\nExit codes are stable: `0` success, `1` generic error, `2` usage error, `3`\nauth/permission (401/403), `4` not found, `5` ambiguous reference. See\n[AGENTS.md](AGENTS.md) for the full machine-readable surface.\n\n## Keybindings (TUI)\n\n| Key | Action |\n|-----|--------|\n| `/` | search |\n| `:` | command palette |\n| `f` / `F` | filter results / clear filters |\n| `Tab` / `Shift+Tab` | switch pane focus (or cycle detail tabs) |\n| `j` / `k` | move selection / scroll detail (on the nav rail, live-browse the kind) |\n| `g` / `G` | top / bottom |\n| `PgUp` / `PgDn` | page up / down |\n| `Enter` | open selected object |\n| `o` | open in browser |\n| `y` | copy current item label (desktop clipboard, or OSC 52 over SSH/headless terminals) |\n| `R` | related objects (jump between connected objects) |\n| `D` | overview dashboard |\n| `T` | prefix tree (`Space` / `←` / `→` collapse/expand) |\n| `t` | cycle theme |\n| `r` | refresh |\n| `P` / `Ctrl+P` | switch profile (cycle forward / backward) |\n| `S` | open the Config modal (profiles + settings) |\n| `b` / `Esc` | back / clear search |\n| `i p c v s` | device tabs (interfaces / IPs / cables / VLANs / services); `j`/`k` + `Enter` opens a row — interfaces/cables open the interface detail (with its cable-path tab) |\n| `e` | rack elevation |\n| `u` | dismiss update notice |\n| `?` / `F1` | help |\n| `q` / `Ctrl+c` | quit |\n\nThe command palette (`:`) accepts `device`/`ip`/`prefix`/`vlan`/`site`/`vrf \u003cref\u003e`,\n`find \u003cq\u003e` (or bare text), `filter \u003ckey\u003e=\u003cvalue\u003e`, `open`, `copy`, `theme \u003cname\u003e`,\n`profile \u003cname\u003e`, `config`, and `refresh`. The home screen lists recently opened objects (deduped,\nmost-recent-first) when there are no search results — press `Enter` to reopen one.\nSet `[ui].refresh_secs` to auto-refresh the current search on an interval (off by\ndefault).\n\n`P` / `Ctrl+P` (or `profile \u003cname\u003e` in the palette) switches between the profiles\nin your config without restarting: it rebuilds the client for that instance and\nre-probes `/api/status/`, so the header flips to the new profile and its NetBox\nversion. With a single profile the hotkey is a no-op. The cycle is session-only —\nit does not rewrite `active_profile` in your config (use `nbox profile use \u003cname\u003e`\nfor that).\n\n`S` (or `config` in the palette) opens the Config modal to manage profiles\nin-app: list them (active marked), and add / edit / select / delete without\nhand-editing `config.toml`. The add/edit form covers `name`, `url`, `token_env`,\n`auth_scheme`, and `verify_tls`, plus an optional masked token field. A typed\ntoken is saved to `config.toml` (redacted by display commands; config files are\nwritten user-only on Unix); on an edit, `Ctrl+X` clears the stored token. `Ctrl+T`\ntest-connects before you commit; `Enter` saves, `Ctrl+G` saves and switches to\nit. Unlike the quick `P` cycle, selecting or adding-and-using a profile here\n**persists** `active_profile` to your config. Deleting the active or last profile\nis blocked.\n\n`Tab` switches the Config modal to its **Settings** section, a two-pane editor —\ncategories on the left, the selected category's fields on the right:\n\n- **Appearance** — `theme` (cycle with `←`/`→`/Space, applied live).\n- **Behavior** — `refresh_secs` (auto-refresh interval; empty/`0` = off) and\n  `open_browser_command` (a custom browser-open command; empty = OS default).\n- **Logging** — `log_level` (a tracing filter like `nbox=debug`) and `log_file` (a\n  path); both persist and apply on the next launch.\n\n`↑`/`↓` pick a category, `→` enters its fields (`↑`/`↓` move between them, `Esc`\nsteps back), and `Enter` or `Ctrl+S` saves every changed field back to\n`config.toml` (format-preserving — comments and unrelated keys are kept), re-arms\nthe auto-refresh without a restart, and applies the new browser command to the next\nopen. Settings live in `~/.config/nbox/`, so they survive upgrades.\n\n`[ui].open_browser_command`, when set, is what `nbox open` and the TUI `o` action\nrun to open a URL (the URL is appended as a literal final argument, never\nshell-interpolated); leave it empty to use the OS default opener.\n\n## Themes\n\nTwelve built-in themes. Set with `[ui].theme` in the config or press `t` to cycle:\n\n`default`, `kawaii`, `cyber`, `dracula`, `monochrome`, `matrix`, `nord`,\n`gruvbox`, `catppuccin`, `tokyo_night`, `solarized`, `light`.\n\n`light` is the only light-background theme. `NO_COLOR` is honored — when set, the\nTUI renders without color and marks the selection with a `\u003e` cursor.\n\n## Configuration\n\nFirst-time setup needs no hand-edited TOML: launch `nbox` with no config and the\nTUI runs a first-run wizard that captures a profile, test-connects, writes it, and\ncontinues into the app. The same wizard appears when a config exists but has no\nresolvable active profile. You can still configure by hand — the file lives at:\n\n| OS | Path |\n|----|------|\n| Linux / macOS | `~/.config/nbox/config.toml` |\n| Windows | `%APPDATA%\\nbox\\config.toml` |\n\n```toml\nconfig_version = 1\nactive_profile = \"work\"\n\n[ui]\ntheme = \"default\"\nconfirm_writes = true\n# refresh_secs = 30          # TUI auto-refresh interval in seconds (omit/0 = off)\n# open_browser_command = \"\"  # custom browser-open command (empty = OS default)\n\n# Local read cache (optional; on by default). A short in-memory de-dupe window so\n# repeated reads (TUI back-nav, a chatty agent) don't re-hit NetBox. Never serves\n# stale data across a write you made; cleared on profile switch.\n[cache]\nenabled = true             # master switch\nttl_secs = 30              # reuse window in seconds (clamped to 5–300)\n\n[profiles.work]\nurl = \"https://netbox.example.com\"\n# token = \"nbt_...\"              # default TUI paste path; redacted by config show\ntoken_env = \"NETBOX_TOKEN\"\nauth_scheme = \"auto\"          # auto | bearer | token\nverify_tls = true\ntimeout_secs = 15\npage_size = 100\nexclude_config_context = true\n\n# Per-surface backend (optional; omit for all-REST). REST is canonical; GraphQL\n# is an opt-in accelerator for the VRF and route-target views. Search is always REST.\n[profiles.work.api]\nvrf = \"graphql\"               # rest | graphql\nroute_target = \"graphql\"      # rest | graphql\n```\n\n**`token` vs `token_env` — the one thing to get right:**\n\n- `token = \"nbt_…\"` holds the **actual API token**, stored in `config.toml`. This\n  is what the TUI writes when you paste a token; display commands redact it and the\n  file is written user-only (`0600`) on Unix.\n- `token_env = \"NETBOX_TOKEN\"` holds the **name of an environment variable** that\n  holds the token — the secret stays in your shell / CI / systemd unit, never in\n  the file. **Put the variable name here, not the token itself.**\n\nToken sources are resolved in this order:\n\n1. the env var named by the profile's `token_env` (if set \u0026 present)\n2. `NBOX_TOKEN`\n3. the profile's `token = \"...\"`\n4. none\n\nEach source is normalized before it competes — a pasted `Bearer `/`Token ` prefix\nor stray whitespace is stripped (NetBox's \"copy token\" button hands you the full\n`Authorization` header), and nbox adds the scheme itself from `auth_scheme`. Use\nenv vars for CI, SSH, Docker, and anything headless. `nbox config token status`\nshows the active source (never the token). See [docs/CONFIG.md](docs/CONFIG.md)\nfor the full reference.\n\n## MCP Server\n\n`nbox serve` runs a read-first MCP server (read-only by default), stdio by\ndefault. An MCP host (Claude Desktop, Claude Code, …) launches `nbox serve` as a\nsubprocess and speaks JSON-RPC over stdin/stdout; it reuses the same query + view\nlayer as the CLI, so the tools return the same JSON view models. The NetBox URL\nand token come from the active profile / env, and it takes the same `-p`/`--config`\nflags. JSON-RPC goes to stdout; all logging stays on stderr.\n\nThe eleven read tools below are annotated read-only (`read_only_hint = true`). Two\nwrite tools — `nbox_plan_write` / `nbox_apply_write` — are **always registered** (so\nthey show up in `tools/list`), but execution is gated. Local stdio writes require\n`--local-writes` / `[serve].local_writes` and run under the active profile token.\nShared HTTP/OIDC writes require `--allow-writes` / `[serve].allow_writes`, an\nOIDC-authenticated caller carrying `nbox:write`, and a `[serve.vault]` entry mapping\nthe caller's `sub` to a per-user NetBox token. `nbox_apply_write` applies the plan\nthe server stored at plan time — looked up by the `confirm_token` you pass back —\nnot the plan JSON itself, so a forged plan is rejected.\n\n| Tool | What |\n|------|------|\n| `nbox_status` | Connection + backend capabilities + NetBox/Django/Python versions + a token-validity preflight (`token`: `valid`/`invalid`/`unverified`; NetBox 4.5+). |\n| `nbox_search` | Search devices/sites/racks/rack-groups/IPs/prefixes/VLANs/circuits/virtual-circuits/providers/aggregates/ASNs/IP-ranges/tenants/contacts/VMs/VM-types/clusters/VRFs/route-targets; `query` (required), `limit`, `status`, `site`, `region`, `site_group`, `location`, `tenant`, `role`, `tag`, `owner`/`owner_group` (4.5+; user/group by name), `vrf` (id\\|rd\\|name; filters IP/prefix results only). |\n| `nbox_get` | Fetch one object by `kind` (device, ip, prefix, vlan, site, rack, rack_group, circuit, virtual_circuit, aggregate, asn, ip_range, tenant, contact, provider, vm, vm_type, cluster, vrf, route_target, mac, interface) + `ref`; `vrf`/`site`/`group` disambiguate. |\n| `nbox_get_interface` | One interface on a device, with its cable-path trace. |\n| `nbox_next_ip` | Next available address(es) in a prefix (nothing is reserved). |\n| `nbox_next_prefix` | Available free child prefix(es) of a given length, or all free blocks. |\n| `nbox_journal` | Recent journal entries for an object. |\n| `nbox_history` | Change history (system audit log: create/update/delete, who + when) for an object. `/api/core/object-changes/` (NetBox 4.x) — distinct from `nbox_journal` (operator notes). |\n| `nbox_list_tags` | List tags (name, slug, color, usage count). |\n| `nbox_tagged` | Objects carrying a tag, across kinds (NetBox 4.3+); `tag` (id\\|name\\|slug). |\n| `nbox_cache_clear` | Drop nbox's local read cache so the next lookups fetch fresh (read-only w.r.t. NetBox). |\n\nThe same objects are also exposed as MCP **resources** via one template,\n`nbox://{kind}/{ref}` (e.g. `nbox://device/edge01`), for hosts that browse or\nattach resources instead of calling tools — reading one returns the same JSON\nview `nbox_get` does.\n\n### Add it to Claude\n\n**Claude Code** — register the server in one command:\n\n```bash\nclaude mcp add nbox -- nbox serve\n```\n\n**Claude Desktop** — add to your MCP config:\n\n```json\n{\n  \"mcpServers\": {\n    \"nbox\": { \"command\": \"nbox\", \"args\": [\"serve\"] }\n  }\n}\n```\n\nOr skip the hand-editing — `nbox serve --print-config` prints the ready-to-paste\n`mcpServers` object (with the absolute binary path and any `--profile`/`--config`\nor `--local-writes` you passed echoed into `args`); see [docs/MCP.md](docs/MCP.md) for the exact\nconfig-file path per host (Claude Code, Claude Desktop, Cursor).\n\nBoth reuse the same `config.toml` / `NBOX_TOKEN` as the CLI. Prefer driving the CLI\ndirectly? Install nbox as a **Claude Code Agent Skill** — `mkdir -p\n~/.claude/skills/nbox \u0026\u0026 cp SKILL.md ~/.claude/skills/nbox/` — and Claude runs the\n`nbox` subcommands on matching requests.\n\n### HTTP transport and OIDC\n\nStdio is the default. For local clients that prefer HTTP framing, serve the same\ntools at `/mcp` (Streamable HTTP) on a loopback address — the transport is in the\ndefault build (behind the `http` cargo feature, on by default; `--no-default-features`\ngives a lean stdio-only build). It binds loopback only, validates `Origin`/`Host`,\nand takes an optional static bearer:\n\n```bash\nnbox serve --http 127.0.0.1:8080 [--http-token \"$(openssl rand -hex 16)\"]\n```\n\nFor a network-reachable, read-only deployment, run nbox as an OAuth 2.1 resource\nserver: it validates inbound IdP JWTs on `/mcp` and advertises Protected Resource\nMetadata (RFC 9728). Provider-agnostic; configuring OIDC is what lifts the loopback\nrestriction (terminate TLS in front):\n\n```bash\nnbox serve --http 0.0.0.0:8080 \\\n  --oidc-issuer https://idp.example.com --audience https://nbox.example.com\n```\n\nThis is accountability, not per-user RBAC — the last hop to NetBox still uses the\nsingle profile token, so scope that token read-only. An audit log\n(`nbox::audit`) and an optional per-caller rate limit (`--rate-limit`) round it\nout. Full setup, security model, and IdP notes: [docs/MCP.md](docs/MCP.md).\n\nFor local single-user MCP writes, use stdio plus `--local-writes`; the write tools\nreuse the profile token and the MCP host's tool-approval prompt is the human gate.\nFor shared/network writes where NetBox must see the real caller, use\n`--allow-writes` plus `[serve.vault]` to map each caller's OIDC `sub` to a per-user\nNetBox token (via `token_env`). HTTP profile-token writes are intentionally\ndeferred; only a raw escape-hatch MCP tool comes later.\n\n## NetBox Compatibility\n\n- **Requires NetBox 4.2+** (the polymorphic `scope` model for prefixes/VLANs).\n  nbox checks the instance version via `/api/status/` on connect. Full 4.2 / 4.3 /\n  4.5+ matrix: [docs/COMPATIBILITY.md](docs/COMPATIBILITY.md).\n- Targets the NetBox **REST API** (`/api/`) as the canonical integration path.\n- `nbox status --json` and MCP `nbox_status` include a per-surface `api` block\n  (configured vs effective backend), a typed `capabilities` object with\n  version compatibility, REST behavior, and per-surface GraphQL support, **and a\n  token-validity preflight** (`token`: `valid`/`invalid`/`unverified`, the\n  authenticated user on `valid`) via NetBox 4.5's `/api/authentication-check/`.\n- Auto-detects **v2 API tokens** (NetBox 4.5+, `Authorization: Bearer nbt_…`) and\n  legacy **v1 tokens** (`Authorization: Token …`); force one with `auth_scheme`.\n- Optional, read-only **GraphQL** (`/graphql/`) as a **per-surface accelerator**\n  for the VRF and route-target views (`[profiles.\u003cname\u003e.api]`\n  `vrf`/`route_target = \"graphql\"`). nbox probes the schema so NetBox 4.2, 4.3,\n  and 4.5+ filter/pagination differences are handled without hard-coding a\n  version, and **falls back to REST** (with the reason in `status`) when a surface\n  isn't supported. If an effective GraphQL bundle fails at runtime, nbox warns and\n  retries the same detail over REST. **Search is always REST** — NetBox's GraphQL\n  API has no equivalent to REST's full-text `q`, so GraphQL never backs the search\n  surface. REST stays canonical and powers search, identity resolution, detail\n  lookups, raw, journals, and available-IP/prefix operations.\n\n## Troubleshooting\n\n| Symptom | Fix |\n|---------|-----|\n| `no config at … — run nbox config init` | First run: just launch `nbox` for the guided wizard, or `nbox profile add …`. |\n| `error: authentication failed` (exit 3) | The token is missing/invalid. Check `nbox config token status` for the resolved source; paste a token in the TUI, export `NBOX_TOKEN`, or point `token_env` at a set variable. |\n| `403` / permission denied (exit 3) | The token is valid but lacks object permissions, or your NetBox needs `LOGIN_REQUIRED`-aware tokens. Use a token with read access. |\n| TLS / certificate error | For a lab with a self-signed cert, set `verify_tls = false` on that profile (never in production). |\n| `operation timed out` on one search endpoint | Transient; nbox already disables stale keep-alive reuse. Retry, or raise `timeout_secs`. |\n| `ambiguous` (exit 5) | The reference matched several objects — disambiguate (`--vrf` for an IP/prefix, `--site`/`--group` for a VLAN). |\n| NetBox version error | nbox requires NetBox **4.2+**. Check `nbox status`. |\n\nFull list with copy-paste fixes: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md).\n\n## Documentation\n\n- [Features](docs/FEATURES.md) — full command reference\n- [Configuration](docs/CONFIG.md) — config, profiles, token resolution, cache\n- [Compatibility](docs/COMPATIBILITY.md) — NetBox 4.2 / 4.3 / 4.5+ matrix and how nbox adapts\n- [Comparison](docs/COMPARISON.md) — nbox vs the NetBox web UI / raw API, and when to use each\n- [Scripting \u0026 automation](docs/SCRIPTING.md) — JSON/CSV/envelope schemas, exit codes, jq recipes, CI\n- [Continuous Integration](docs/CI.md) — required checks, scheduled gates, local smoke\n- [MCP server](docs/MCP.md) — agent setup, tools, HTTP/OIDC\n- [Architecture](docs/ARCHITECTURE.md) — internal design and module structure\n- [ADRs](docs/adr/) — architecture decision records\n- [Agents](AGENTS.md) — machine-readable surface, output formats, exit codes\n- [Troubleshooting](docs/TROUBLESHOOTING.md) — symptoms and fixes\n- [Known Issues](KNOWN_ISSUES.md) — current limitations\n- [Changelog](CHANGELOG.md) — release history\n- [Roadmap](ROADMAP.md) — planned features\n- [Contributing](CONTRIBUTING.md) — development setup and guidelines\n- [Security](SECURITY.md) — vulnerability reporting and security posture\n\n## License\n\nLicensed under either of [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE) at\nyour option.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flance0%2Fnbox","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flance0%2Fnbox","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flance0%2Fnbox/lists"}