Introduction
binscout is headless, agent-first binary analysis. Open a binary — ELF, PE, Mach-O, LE, or flat raw bytes — and get functions, control-flow graphs, cross-references, strings, symbols, and IR out.
There are two interfaces over the same engine:
binscout, a JSON CLI: every command prints exactly one JSON value to stdout.binscout-mcp, an MCP server over stdio with 13 tools, one per capability.
Annotations (function renames, free-text comments) live in a redb
sidecar next to the binary. The analyzed binary is never written.
Start with Installation, then the CLI reference.
Installation
Requires a Rust toolchain (stable).
From crates.io:
cargo install binscout
From the Git repository (latest master):
cargo install --git https://github.com/mimi1vx/binscout
Or from a checkout:
git clone https://github.com/mimi1vx/binscout
cd binscout
cargo install --path .
This installs two binaries: binscout (the JSON CLI) and binscout-mcp
(the MCP server). Verify with:
binscout --help
CLI reference
binscout [--db PATH] <command> <binary> [args]
--db overrides the sidecar database (default <binary>.rsdbg.redb).
See JSON protocol for the output contract.
| Command | Example | Output |
|---|---|---|
load | binscout load fw.elf | project info; caches the analysis snapshot |
functions | binscout functions fw.elf --query main --limit 50 | function rows, renames resolved; --query is a case-insensitive substring over the resolved name, --caller/--callee keep call-graph neighbors of the function containing that address, --range LO HI keeps entries in the inclusive bounds; page with --offset/--limit |
func | binscout func fw.elf 0x400000 | blocks with instructions, edges, calls |
disasm | binscout disasm fw.elf 0x400000 32 | decoded instructions (addr = hex or decimal, then byte count); the valid prefix plus error_at for the first bad opcode, ranges cap at 1 MiB |
cfg | binscout cfg fw.elf 0x400000 | block nodes plus (from, to) edges |
xrefs | binscout xrefs fw.elf --to 0x400000 --kind code | cross-references filtered by --to/--from/--kind (code, data, string), paged with --offset/--limit behind a {"total", "xrefs"} envelope |
strings | binscout strings fw.elf --pattern SAVE --min-len 6 | filtered strings by address (--pattern substring or regex: expression, --min-len, --section, --include-code; executable-memory hits hidden by default; paged by --start/--limit, --offset deprecated) |
symbols | binscout symbols fw.elf | imports and exports |
sections | binscout sections fw.elf | section table: name, vaddr, in-memory/file sizes, permissions |
ir | binscout ir fw.elf 0x400000 32 | P-code-inspired lift of the range (error_at on trailing bad bytes, ranges cap at 1 MiB) |
stats | binscout stats fw.elf | storage stats: binary size, sidecar DB size, analysis counts |
annotate | binscout annotate fw.elf 0x400000 --rename main --comment "entry" | read/write annotation at an address |
Paged lists cap at 1024 rows. Addresses accept 0x-hex or decimal
strings. func/cfg snap any address inside the function to its
containing entry. annotate without flags reads; --rename "" clears
the rename, omitting --comment leaves the comment alone. rename only
accepts known function entries; comment accepts any address.
sections rows carry name, vaddr (the flat virtual address the loader
mapped the section at), size (in-memory bytes), file_off/file_size
(locating the raw bytes in the file), and readable/writable/executable.
On PE, vaddr = IMAGE_BASE + RVA: .text at RVA 0x1000 in a
0x400000-based image lives at 0x401000, with file_off pointing at its
PointerToRawData. Raw blobs have no section table and report [].
JSON protocol
Every command prints exactly one JSON value to stdout and exits 0.
A runtime failure prints a single {"error": "..."} value and exits 1,
so stdout is always valid JSON. (Clap usage errors for unknown
flags/subcommands still go to stderr with exit 2.)
Real example — opening the repo’s tests/fixtures/raw-x64.bin:
$ binscout load tests/fixtures/raw-x64.bin
{"annotations":0,"arch":"x86_64","binary":"tests/fixtures/raw-x64.bin","blocks":1,"content_hash":13197063251454239466,"db_path":"tests/fixtures/raw-x64.rsdbg.redb","entry":null,"entry_hex":null,"format":"raw","functions":1,"insns":3,"reason":null,"size":3,"strings":0,"supported":true,"version":"0.2.1","xrefs":0}
functions and xrefs paginate with --offset/--limit; strings
pages with --start/--limit (--offset is a deprecated alias that
warns). Limits cap server-side at 1024 rows. xrefs returns a
{"total", "xrefs"} envelope where total is the filter-matched count
before paging, so large result sets page without scanning blind.
Its per-row offset field is the byte offset of the
hit, not the page position. disasm and ir decode at most 1 MiB per
call and report trailing bad bytes in error_at instead of failing,
so the valid prefix is always usable.
MCP tools
binscout-mcp serves 13 tools over stdio (newline-delimited JSON-RPC).
Each takes binary plus an optional db path and returns one JSON
document as text; failures are tool errors. Attach it to any MCP client:
{
"mcpServers": {
"binscout": { "command": "/path/to/binscout-mcp" }
}
}
| Tool | Parameters | Does |
|---|---|---|
project_info | binary | Open a binary and report arch, format, entry, and analysis counts |
functions | binary, query/caller/callee/range_lo/range_hi, offset/limit | List functions with rename-resolved names, filtered (query case-insensitive substring; caller/callee addresses resolving to call-graph neighbors; range_lo + range_hi bounding entries) and paged |
func | binary, addr | Full detail of one function: blocks with instructions, edges, calls |
disasm | binary, addr, len | Disassemble len bytes at addr with the binary architecture; the valid prefix plus error_at for the first bad opcode (ranges cap at 1 MiB) |
cfg | binary, addr | Control-flow graph of one function: block nodes plus block edges |
xrefs | binary, to/from/kind, offset/limit | Cross-references filtered by endpoint and kind, paged behind a total match count |
strings | binary, pattern/min-len/section/include_code, start/limit | Extracted strings ordered by address, filtered and paged; executable-memory hits hidden unless pattern, section, or include_code is given (offset is a deprecated alias of start) |
symbols | binary | Imports and exports of the binary |
sections | binary | Section table: name, virtual address (IMAGE_BASE + RVA on PE), in-memory/file sizes, permissions |
ir | binary, addr, len | Lift len bytes at addr to P-code-inspired IR (error_at on trailing bad bytes, ranges cap at 1 MiB) |
stats | binary | Storage stats: binary size, sidecar DB size, and analysis counts |
rename | binary, addr, name | Rename a function in the project DB (binary untouched); empty name clears |
comment | binary, addr, comment | Comment any address in the project DB (binary untouched); empty comment clears |
Addresses are 0x-hex or decimal strings. kind is one of code,
data, or string. Calls are serialized behind one lock because redb
allows a single open handle per file per process.
Architecture
One engine, two interfaces. Data flows one way — loader, disassembler,
IR, analysis, database — and both the CLI (binscout) and the MCP server
(binscout-mcp) are thin adapters over it.
- Loader (
loader): sniffs ELF, PE, Mach-O, and LE, falling back to flat raw bytes when nothing matches: unrecognized input opens as x86-64 at base 0, LE images analyze as 32-bit x86. Explicit base/architecture mapping goes throughopen_raw. Produces segments, sections, imports/exports, and printable strings (ASCII plus UTF-16LE/BE over the ASCII subset). Inputs over 512 MiB are rejected before parsing. - Disassembler (
disasm): x86_64 and 32-bit x86 via iced-x86, AArch64, 32-bit ARM, and Thumb via yaxpeax-arm. Each instruction becomes anInsnwith its bytes, mnemonic, operand strings, and resolved branch/data targets. - IR (
ir): lifts one instruction to one P-code-inspiredIrInsn(COPY,LOAD,BRANCH,CALL, …). Unknown mnemonics becomeNOPwith a warning instead of failing, so analysis proceeds over partial lifts. - Analysis (
analysis): recursive descent from the entry point and export addresses over decoded instructions, plus a prologue scan for functions unreachable from those seeds. Builds per-function CFGs, a callgraph, and code/data/string cross-references. - Database (
db): caches the snapshot and the user annotations in aredbsidecar; see Sidecar format. A content hash gates reuse — a changed binary is re-analyzed.
Sidecar format
Analysis results and annotations persist in a redb database next to the
binary: <binary>.rsdbg.redb by default, overridden per-invocation with
--db PATH (CLI) or the db parameter (MCP tools). The analyzed binary
itself is only ever read.
On open, the stored content hash is compared against the file; a mismatch means the binary changed and it is re-analyzed (annotations carry over by address). A sidecar written by an older schema version behaves the same way: annotations carry over and the binary re-analyzes. Row values are JSON.
| Table | Key | Value |
|---|---|---|
meta | string | schema version, path, hash, size, arch, format, entry |
binaries | 0 | snapshot metadata |
functions | entry address | function row (name, callees) |
blocks | block key | block row (address range, successors) |
xrefs | xref key | cross-reference row |
strings | row index | extracted-string row |
annotations | address | rename and/or comment |
Block rows store address ranges plus per-instruction boundaries, not rendered instructions: the open path re-decodes each recorded address from the binary, so snapshots stay small no matter how large the disassembly grows.
Delete the sidecar to drop cached analysis and annotations; it is rebuilt
on the next load.
Size and upkeep
binscout stats <binary> reports the binary size (size), the sidecar
size (db_size, 0 before the first load), and the analysis counts
(functions/blocks/insns/xrefs/strings/annotations), so the
storage cost of a snapshot stays visible. Snapshots store block address
ranges rather than rendered instructions, so sidecars stay proportional
to the binary; CI caps the synthetic fixtures at 5 MiB to catch snapshot
bloat.
save rewrites every table, so repeated re-analysis can leave free pages
behind. There is no separate vacuum step: redb has no VACUUM command.
Call binscout::db::compact_db (wrapping redb::Database::compact) to
reclaim the space when a sidecar grows unexpectedly.
Never commit sidecars. Keep them out of version control:
*.rsdbg.redb
Contributing
The full guide lives in CONTRIBUTING.md. The short version — the gate for any change:
cargo test # full suite (unit + CLI/MCP e2e over fixtures)
cargo clippy --all-targets -- -D warnings
cargo fmt --check
cargo doc --no-deps # warning-free (`#![warn(missing_docs)]` is on)
Work is tracked with br/bd issues; file bugs and feature requests as
issues first and keep changes small and scoped. Commits follow
Conventional Commits: feat:
bumps minor, fix: bumps patch. New behavior needs a test; new public
API needs rustdoc. The project is dual-licensed MIT OR
Apache-2.0 — new files carry no headers, the license applies repo-wide.