An open API service indexing awesome lists of open source software.

https://github.com/banteg/bn

binary ninja cli for coding agents
https://github.com/banteg/bn

agents binary-ninja codex reverse-engineering

Last synced: 11 days ago
JSON representation

binary ninja cli for coding agents

Awesome Lists containing this project

README

          

# bn

`bn` is a coding agent-first CLI for Binary Ninja. It gives a shell session or tool-calling agent stable commands, structured output, and access to the same live Binary Ninja database you already have open in the GUI.

## Headline Features

- Query live Binary Ninja state from the shell: targets, functions, callsites, decompile text, IL, disassembly, xrefs, types, strings, imports, and reusable bundles.
- Execute Python inside the Binary Ninja process instead of maintaining a separate headless workflow.
- Apply mutations with `--preview`, capture decompile diffs, and verify the live post-state before reporting success.
- Emit structured `json` or `ndjson` output, auto-spill large results to files, and return token counts so agents can budget context intelligently.

## Install

Recommended setup: install the CLI, the Binary Ninja companion plugin, and the bundled Codex skill.

Install the CLI on your PATH:

```bash
uv tool install -e .
```

Install the Binary Ninja companion plugin:

```bash
bn plugin install
```

That links [`plugin/bn_agent_bridge`](/Users/banteg/dev/banteg/bn/plugin/bn_agent_bridge) into your Binary Ninja plugins directory.

Install the bundled Codex skill:

```bash
bn skill install
```

That symlinks [`skills/bn`](/Users/banteg/dev/banteg/bn/skills/bn) into `$CODEX_HOME/skills/bn` by default. Use `--mode copy` if you want a standalone copy instead. Restart Codex to pick up a new or renamed skill.

If the plugin code changes, reload Binary Ninja Python plugins or restart Binary Ninja.

## How It Works

- `bn` has two parts:
- a normal Python CLI that you can run from your shell or agent tool harness
- a Binary Ninja GUI plugin that owns all Binary Ninja API access
- The plugin creates one fixed bridge socket and one fixed registry file.
- The CLI discovers that bridge, connects to it, and forwards commands into the live GUI session.
- Because the Binary Ninja side runs as a GUI plugin, it works with a personal license and does not require a commercial headless license.

## Quick Start

Open a binary or `.bndb` in Binary Ninja, then run:

```bash
bn doctor
bn target list
bn refresh
bn function list
bn decompile sub_401000
```

If exactly one BinaryView is open, target-specific commands can omit `--target` entirely. If multiple targets are open, pass `--target ` from `bn target list`.

## Target Selection

Use `bn target list` to see available targets:

```bash
bn target list
```

Targets can be selected with:

- the `selector` field from `bn target list`
- the full `target_id`
- the BinaryView basename
- the full filename
- the view id
- `active` when you explicitly want the GUI-selected target

In normal use, prefer the `selector` field. For a single open database, this is usually just the `.bndb` basename:

```bash
bn decompile update_player_movement_flags --target SnailMail_unwrapped.exe.bndb
```

Omitting `--target` only works when exactly one target is open. If multiple targets are open, the CLI rejects the command instead of silently falling back to `active`.

## Output Behavior

Every command supports:

- `--format json`
- `--format text`
- `--format ndjson`
- `--out `

Interactive read commands default to `text`. Mutation, setup, and export commands default to `json`.
Add `--format json` when you need stable fields for automation or piping into structured tooling.

Examples:

```bash
bn function list --format ndjson
bn function list --min-address 0x401000 --max-address 0x40ffff
bn function search --regex 'attach|detach'
bn decompile sample_track_floor_height_at_position --out /tmp/floor.json
```

If `--out` is set, the command writes the rendered result to that path and prints a compact JSON envelope with the artifact path, byte size, token count, tokenizer, hash, and summary. Agents can use that envelope to decide whether to read the full artifact, keep a summary, or defer loading it into context.

The only exception is `bn bundle function`, which writes the bundle artifact from inside the bridge and prints the envelope back to the CLI.

`bn function list` and `bn function search` return the full matching set for the selected target or address range. Large results auto-spill to an artifact instead of forcing manual pagination. Spill is token-based and currently triggers above 10,000 tokens. When that happens, stdout stays empty and stderr carries the spill metadata as plain text, including the artifact path and size counts.

## Extraction Commands

Common read-only commands:

```bash
bn target list
bn target info

bn function list
bn function list --min-address 0x401000 --max-address 0x40ffff
bn function search attachment
bn function search --regex 'attach|detach|follow'
bn function info end_track_attachment_follow_state
bn callsites crt_rand --within bonus_pick_random_type
bn callsites crt_rand --within-file /tmp/rng-functions.txt --format ndjson
bn proto get end_track_attachment_follow_state
bn local list end_track_attachment_follow_state
bn refresh

bn decompile end_track_attachment_follow_state
bn il end_track_attachment_follow_state
bn disasm end_track_attachment_follow_state
bn xrefs end_track_attachment_follow_state
bn xrefs field TrackRowCell.tile_type
bn comment get --address 0x401000

bn types --query Player
bn types show Player
bn struct show Player
bn types declare --file /path/to/win32_min.h --preview
bn strings --query follow
bn imports
```

`bn function search` stays case-insensitive substring matching by default. Add `--regex` when you need regular expressions. `bn function list` and `bn function search` both accept `--min-address` and `--max-address` to filter by function start address.

`bn callsites` is the direct-call lane for exact return-address recovery. It reports both the native `call_addr` and the post-call `caller_static`, where `caller_static = call_addr + instruction_length`. Scope it with `--within ` or `--within-file `; the file format is one function identifier per non-empty line, with `#` comments ignored.

Each callsite row also includes:

- `call_index`: zero-based ordinal for matching callsites in the containing function, ordered by `call_addr`
- `within_query`: the original unresolved scope token from `--within` or `--within-file`
- `hlil_statement`: the smallest recoverable HLIL expression or statement containing the call, or `null` when Binary Ninja only exposes a coarse enclosing region
- `pre_branch_condition`: the nearest enclosing pre-call HLIL condition when it can be recovered confidently, otherwise `null`

`hlil_statement` is intentionally local-or-null. If the best available HLIL mapping expands to a broad whole-function or multi-statement blob, `bn callsites` suppresses it instead of returning noisy context.

## Bundles And Python

`bn decompile` is the HLIL-text convenience lane. It is useful for quick function reading, but typed layouts remain authoritative in `bn types show` and `bn struct show`.

Export a reusable function bundle:

```bash
bn bundle function end_track_attachment_follow_state --out /tmp/end_track_attachment_follow_state.json
```

Run Python inside the Binary Ninja process for one-off inspection and BN-native scripting:

```bash
bn py exec --code "print(hex(bv.entry_point)); result = {'functions': len(list(bv.functions))}"

bn py exec --stdin <<'PY'
print(hex(bv.entry_point))
result = {"functions": len(list(bv.functions))}
PY
```

Use `--stdin` or `--script` for multiline Python snippets. Use `--code` for true one-liners only.

```bash
bn py exec --stdin <<'PY'
out = []
for f in bv.functions:
if 0x416000 <= f.start < 0x41C000:
out.append((f.start, f.symbol.short_name))
out.sort()
print("\n".join(f"{addr:#x} {name}" for addr, name in out))
PY
```

Use a quoted heredoc for multiline Python snippets.

When you need counts from BN iterators such as `f.hlil.instructions`, materialize them explicitly with `list(...)` or consume them with `sum(1 for ...)` instead of assuming sequence semantics.

The `py exec` environment includes:

- `bn`
- `binaryninja`
- `bv`
- `result`

Stdout and `result` are both returned. If `result` is not JSON-serializable, `bn` returns `repr(result)` and includes a warning instead of silently stringifying the whole response.

## Mutation Commands

Mutations follow the same target-selection rules as other target-specific commands.

Examples:

```bash
bn symbol rename sub_401000 player_update --preview
bn comment set --address 0x401000 "interesting branch" --preview
bn comment get --address 0x401000
bn proto get sub_401000
bn proto set sub_401000 "int __cdecl player_update(Player* self)" --preview
bn local list sub_401000
bn local rename sub_401000 0x401000:local:StackVariableSourceType:-20:2:12345 speed --preview
bn local retype sub_401000 0x401000:local:StackVariableSourceType:-20:2:12345 float --preview
bn types declare "typedef struct Player { int hp; } Player;" --preview
bn struct field set Player 0x308 movement_flag_selector uint32_t --preview
```

Preview mode applies the change, refreshes analysis, captures affected decompile diffs, and then reverts the mutation.

Non-preview writes only report success after reading the live BN session back and verifying that the requested post-state actually landed. If verification fails, the CLI returns a nonzero exit code and reverts the whole mutation or batch.

After any live type or prototype mutation, do an explicit readback:

```bash
bn proto get sub_401000
bn struct show Player
bn types show Player
bn decompile sub_401000
```

For declaration and struct mutations, preview results also include `affected_types` with before/after layouts and a unified diff. If a field edit is already identical, the result is marked with `changed: false` and a `No effective change detected` message.

For the first few changed functions, `affected_functions` also includes short `before_excerpt` and `after_excerpt` snippets around the first changed HLIL lines.

Mutation results now distinguish:

- `verified`
- `noop`
- `unsupported`
- `verification_failed`

When verification fails, JSON output also includes `requested` and `observed` state for the failed op.

`bn types declare` now uses Binary Ninja's source parser when available. When you pass `--file`, the CLI also forwards the source path so relative includes resolve the same way they would during header import in the GUI.

If a declaration only parses functions or extern variables and introduces no named types to persist, `types declare` returns a verified no-op instead of failing with `No named types found in declaration`.

`bn local list` and `bn function info` return stable `local_id` values for parameters and locals. Prefer those IDs for `bn local rename`, `bn local retype`, and batch manifests; legacy name-based targeting still works for compatibility.

## Batch Manifests

`bn batch apply` accepts a JSON manifest:

```json
{
"target": "SnailMail_unwrapped.exe.bndb",
"preview": true,
"ops": [
{
"op": "rename_symbol",
"kind": "function",
"identifier": "sub_401000",
"new_name": "player_update"
},
{
"op": "set_prototype",
"identifier": "player_update",
"prototype": "int __cdecl player_update(Player* self)"
}
]
}
```

Apply it with:

```bash
bn batch apply manifest.json
```

Batch apply verifies the live session by default. If any op fails to apply or fails post-state verification, the entire batch is reverted.

## Troubleshooting

Check bridge state:

```bash
bn doctor
```

If `bn target list` is empty:

- make sure Binary Ninja is open
- make sure a binary or `.bndb` is open
- make sure the plugin is installed with `bn plugin install`
- reload Binary Ninja plugins or restart Binary Ninja after plugin changes

If `bn doctor` sees a bridge registry but reports `Operation not permitted` under Codex,
the Codex sandbox is blocking the Unix socket that connects to the live Binary Ninja GUI
process. Let Codex run `bn` outside the sandbox by adding this rule to
`~/.codex/rules/default.rules`:

```text
prefix_rule(pattern=["bn"], decision="allow")
```

Restart Codex or reload rules after editing the file. This is only needed for Codex
sandboxed runs; normal shells can use `bn` without that rule.

If multiple targets are open, omitted `--target` is rejected. Pass `--target ` from `bn target list`, or use `--target active` only when you intentionally want the GUI-selected target.

If decompile text still looks stale after a type change, run:

```bash
bn refresh
```

That forces an analysis refresh, but it still may not fully eliminate Binary Ninja's stale `__offset(...)` presentation in every case.

## Development

Run tests with:

```bash
uv run pytest
```

Run the CLI from the repo without installing it globally:

```bash
uv run bn --help
```