Skip to content

Scripting and Automation with the Sidekick API

The sidekick.api package is the Python interface for scripting and automating Sidekick from Binary Ninja. Use it to:

  • Execute LLM agents and prompts locally, with your own API keys or local models
  • Run BNQL queries and semantic searches against loaded binaries
  • Read and write workspace resources (Indexes, Code Maps, Notebook) programmatically
  • Drive headless analysis pipelines outside the Binary Ninja GUI

Prerequisites

  • Sidekick must be installed and active in Binary Ninja
  • Inside Binary Ninja, default model endpoints and completion pools (smart, fast) are installed automatically on first launch into BN_USER_DIRECTORY/sidekick/config/completion_routing/. They use the Sidekick service as a zero-config backend. To use your own API keys or local models, see Configuring completion pools.
  • Headless scripts use <app_dir>/config/completion_routing/ instead, which is empty unless you populate it. See Outside Binary Ninja.

How to import

import sidekick.api as api

Use this import in Binary Ninja scripts and Library Python entry points when you need Sidekick-managed resources such as indexes, code maps, notebooks, prompts, or agents. Do not import plugin implementation modules such as Vector35_Sidekick; those are internal packaging details rather than the supported scripting surface.

All public symbols are also importable directly from sidekick.api:

from sidekick.api import execute_bnql, render_object, concept, load_prompt, init_runtime

Two execution contexts

The API works in two distinct contexts. Understanding which one you are in determines how you write your scripts.

Inside Binary Ninja (interactive or script execution)

When Sidekick is active and a file is open, the BinaryView already has an associated session and kernel. You pass bv directly to API functions.

# Works in the BN scripting console or a script run from the Library panel
from sidekick.api import execute_bnql

funcs = execute_bnql(bv, "/view/function")
print([f.name for f in funcs])

No setup or teardown is needed. The session lifecycle is managed by Sidekick. Library Python scripts run in this same context: bv is pre-bound, and sidekick.api is the supported way to read or mutate Sidekick workspace resources from those scripts.

Library Python automation recipe

import sidekick.api as api
from binaryninja import MediumLevelILOperation

TAILCALL_OPS = {
    MediumLevelILOperation.MLIL_TAILCALL,
    MediumLevelILOperation.MLIL_TAILCALL_UNTYPED,
    MediumLevelILOperation.MLIL_TAILCALL_SSA,
    MediumLevelILOperation.MLIL_TAILCALL_UNTYPED_SSA,
}

# Reuse the current workspace index or create it on first run.
api.get_index(bv, "tailcalls", create_if_missing=True)

# Collect Binary Ninja objects of any indexable kind. add_index_objects
# accepts the objects directly and derives canonical object IDs for them.
tailcalls = [
    insn
    for func in bv.functions
    if func.mlil is not None
    for insn in func.mlil.instructions
    if insn.operation in TAILCALL_OPS
]

entries = api.add_index_objects(
    bv,
    "tailcalls",
    objects=tailcalls,
    prevent_duplicates=True,
)

print({"index": "tailcalls", "considered": len(entries)})

This is the recommended pattern for Library automations: keep the logic in Python, use the pre-bound bv, and access Sidekick resources through sidekick.api rather than plugin internals. prevent_duplicates=True makes the script safe to re-run as the binary's analysis evolves. The returned list has one entry per indexable object you passed, including any skipped as duplicates, so considered is not a count of newly added entries.

Outside Binary Ninja (headless scripts)

Use the sidekick() context manager to initialize a headless application instance, then open a session for a file or project.

import sidekick.api as api
from pathlib import Path

app_dir = Path("~/.sidekick-headless").expanduser()

with api.sidekick(app_dir) as sk:
    with sk.session("/path/to/target.bndb") as sess:
        funcs = api.execute_bnql(sess.bv, "/view/function", limit=10)
        print([f.name for f in funcs])

Headless sessions have no default completion pools

The smart and fast pools and their endpoints are installed only by the Binary Ninja plugin. A headless app_dir looks for them in <app_dir>/config/completion_routing/, which is empty unless you populate it, so load_prompt and load_agent fail with Completion pool '...' was not found until you provide them. Copy the endpoint.*.yaml and completer_pool.*.yaml files from BN_USER_DIRECTORY/sidekick/config/completion_routing/ into <app_dir>/config/completion_routing/, or write your own. See Configuring completion pools.

api.sidekick(app_dir) creates a SidekickContext. The app_dir stores workspace databases and index data. By default, that directory is preserved on exit so a caller can reuse cached state across runs. Pass cleanup_app_dir=True to remove temporary state on exit when using a scratch directory. sk.session(target) opens a SessionContext for the target file and exposes a bv property for the active BinaryView.

from tempfile import TemporaryDirectory

with TemporaryDirectory(prefix="sidekick-headless-") as tmp:
    with api.sidekick(tmp, cleanup_app_dir=True) as sk:
        with sk.session("/path/to/target.bndb") as sess:
            funcs = api.execute_bnql(sess.bv, "/view/function", limit=10)

Project sessions

Project-backed sessions start without any open files. Open members individually with open_project_file().

with api.sidekick(app_dir) as sk:
    with sk.session(project) as sess:
        bv = sess.open_project_file("libfoo.so.bndb")
        results = api.execute_bnql(sess.binary_view_set, "/view/function")

open_project_file() accepts an exact path_on_disk, a project member name, or an existing BinaryView or FileMetadata from the same project. If a filename matches more than one project member, pass a fuller on-disk path.

For a project on a Binary Ninja Enterprise server, a member that has not been downloaded yet is downloaded before it opens. Sidekick connects to the server with your saved Enterprise login or credentials if it is not already connected. list_project_files() reports exists_on_disk for each member, so you can see which ones are not downloaded.

Enabling semantic search in headless sessions

Semantic search via concept() requires an initialized semantic index. Opt in explicitly.

with api.sidekick(app_dir, semantic_index=True) as sk:
    with sk.session("/path/to/target.bndb") as sess:
        sess.wait_for_semantic_index_ready(timeout_secs=120.0)
        matches = api.concept(sess.bv, "credential handling")

Note

Structural BNQL queries work without semantic indexing. Only concept() and the concept() BNQL function require it. In headless scripts, concept() stays non-blocking and can return [] until indexing is ready, so call sess.wait_for_semantic_index_ready(...) before relying on semantic results.


Querying with BNQL

execute_bnql runs a BNQL query against a BinaryView or BinaryViewSet and returns a list of objects.

from sidekick.api import execute_bnql

# All functions
funcs = execute_bnql(bv, "/view/function")

# Functions that call malloc (also finds callers of imported symbols)
callers = execute_bnql(bv, 'callers("malloc")')

# At most 20 functions (limit caps the result count; ordering is not by name)
funcs = execute_bnql(bv, "/view/function", limit=20)

offset skips that many results (0-indexed), so limit and offset together page through results. time_limit_secs (default 30; pass None for no limit) bounds evaluation time. When the limit is hit, execute_bnql does not raise or report the timeout. It returns whatever results were already collected, which is often an empty list, so raise time_limit_secs or narrow the query if results look missing or truncated.

To get rendered text instead of objects, use render_object on each result:

from sidekick.api import execute_bnql, render_object

funcs = execute_bnql(bv, "/view/function", limit=5)
summaries = [render_object(f, verbosity="summary") for f in funcs]

See Building and Using Indexes and the BNQL reference for full query syntax.


Semantic search with concept()

concept() performs semantic vector search against the binary's index and returns ranked results.

from sidekick.api import concept

results = concept(bv, "file encryption")
for obj, score, similarity in results:
    print(f"{obj.name}  score={score:.3f}  similarity={similarity:.3f}")

Parameters:

Parameter Type Default Description
target BinaryView or BinaryViewSet required The analysis target
subject any required A text string, Binary Ninja object, or rendered text to search for, or a list of these (embeddings are averaged)
object_type str, Binary Ninja object/class, or None None Restrict results to one canonical kind, such as "function", "datavar", "string", "type", "symbol" or "comment". An unknown kind raises ValueError
similarity float 0.45 Minimum cosine similarity threshold (0.0-1.0); values outside this range raise ValueError
limit int >= 1 10 Maximum number of results

Each result is a tuple of (object, score, similarity_score).

Note

concept() requires an active semantic index. Semantic indexing is disabled by default; enable it in Edit > Preferences > Settings (sidekick.semantic_index.enabled). When the index is unavailable, concept() returns an empty list. In headless sessions, pass semantic_index=True to api.sidekick().

You can also use concept() directly inside a BNQL query:

# Functions semantically related to "network communication" that also call send
results = execute_bnql(
    bv,
    'concept("network communication")/function[calls::function[@name == "send"]]',
)

Use concept(...) as the whole predicate; it cannot be combined with in, and, or, or not.


Rendering objects with render_object

LLMs work with text. render_object converts Binary Ninja objects into text at a specified level of detail.

from sidekick.api import render_object

func = bv.get_function_at(0x401000)

name_only = render_object(func, verbosity="identifier")   # name and addr lines
summary   = render_object(func, verbosity="summary")      # name, addr, signature, size_bytes, blocks, ...
full_code = render_object(func, verbosity="contents")     # metadata plus the decompiled listing

The result is text made of key: value metadata lines, followed by a fenced listing when the verbosity includes one. verbosity is case-insensitive, accepts "id" as an alias for "identifier", and also accepts a DetailLevel value. An invalid verbosity raises ValueError.

Verbosity What you get
"identifier" Name and address
"summary" Key attributes as key: value lines (name, address, signature, size, block count, ...)
"contents" Metadata plus the full listing (decompiled HLIL for functions)
"contextual" Contents in an address-free form that adds context such as section names (used for semantic embedding)

options is a dict, or a string (JSON, or key=value,key2=value2 where a bare key means True), of renderer parameters:

render_object(func, "contents", {"view_type": "MLIL", "max_lines": 200})
render_object(func, "contents", "show_line_addresses=false")

Common keys are show_line_addresses (default true), max_line_length, and max_lines. Functions and basic blocks also take view_type (PseudoC, HLIL (default), HLIL_SSA, MLIL, MLIL_SSA, LLIL, LLIL_SSA, or Disassembly). Data variables take view_type (hex or structured), surrounding_bytes, bytes_per_row, show_ascii, and max_array_elements. Keys that do not apply to an object type are ignored.

Auto-rendering of agent and prompt variables

When a non-string Binary Ninja object is passed as a variable, the API renders it automatically at "contents" verbosity. To use a different verbosity:

  • Agents: pass variable_render_verbosity= (and optionally variable_render_options=) to the agent call, or to sess.load_agent(...) to set the default for that loaded agent.
  • Prompts: set detail_level: (identifier, summary, contents, or contextual) and optional render_params: on the variable in the prompt YAML, for example CODE: {name: CODE, required: true, detail_level: summary}. A variable_render_verbosity= argument to a prompt call is ignored.
  • Either: call render_object(obj, verbosity=...) first and pass the resulting string.

Executing prompts

Prompts are single-shot LLM completions. They accept template variables, call the model once, and return a response.

When to use: Summarization, naming, classification, extracting structured information from a piece of code.

Define a prompt in YAML

Prompt files are named prompt.<name>.yaml and must live in a directory the API can resolve (see Scripts root resolution).

# prompt.summarize-function.yaml
version: 1
name: summarize-function
completion_pool_id: fast
max_tokens: 16000
max_completion_tokens: 1024
prompt_template:
  messages:
    - role: user
      content: "Summarize this function in one sentence:\n\n{{CODE}}"
  variables:
    CODE:
      name: CODE
      required: true

max_tokens (default 2048) is the input-token budget for the formatted prompt; exceeding it raises PromptBudgetError. max_completion_tokens (default 1024) caps the response length. Raise them for large inputs such as decompiled functions. A per-variable max_tokens truncates a single variable instead.

Call the prompt from Python

from sidekick.api import load_prompt, render_object

summarize = load_prompt("summarize-function")
code_text = render_object(bv.get_function_at(0x401000), verbosity="contents")
result = summarize(CODE=code_text)
print(result)

Keyword arguments map directly to the variable names defined in the YAML.

Structured output

Add output_schema to receive a parsed dict instead of plain text:

# prompt.extract-summary.yaml
version: 1
name: extract-summary
completion_pool_id: fast
output_schema:
  type: object
  properties:
    summary:
      type: string
    confidence:
      type: number
  required: [summary, confidence]
prompt_template:
  messages:
    - role: user
      content: "Summarize this code and rate your confidence:\n\n{{CODE}}"
  variables:
    CODE:
      name: CODE
      required: true
extract = load_prompt("extract-summary")
result = extract(CODE=render_object(func, verbosity="contents"))
print(result["summary"], result["confidence"])

Executing agents

Agents are autonomous, multi-step actors. They are given a goal, can use tools to query and modify the binary, and run until they decide the task is complete.

When to use: Complex analysis, automated renaming, tasks that require exploring the binary across multiple steps.

Define an agent in YAML

Agent files are named agent.<name>.yaml.

# agent.rename-vars.yaml
version: 1
protocol_id: rename-vars
completion_pool_id: smart
delivery: await  # optional; the default for agent tool calls
uses_tags:
  - database  # grants query and update access to the Binary Ninja database
prompt_template:
  messages:
    - role: system
      content: |
        You are a reverse engineering assistant. Analyze the provided function
        and use the update_database tool to rename any default-named variables
        (like var_18) to something meaningful based on their usage.
    - role: user
      content: "Please rename variables in this function:\n\n{{TARGET_FUNCTION}}"
  variables:
    TARGET_FUNCTION:
      name: TARGET_FUNCTION
      required: true

For agent tool calls, delivery: await waits for the full reply. delivery: notify returns request and conversation handles immediately and delivers the reply later to the requesting chat. The library_agent_run tool inherits this setting unless its call supplies a delivery override. A synchronous Python agent callable still waits for its returned answer, regardless of this tool delivery setting.

The uses_tags field controls which local runtime tools the agent can use:

  • database — matches every tool tagged database.*: database.query, database.update, and database.state
  • database.query — the query tool only (read access to the Binary Ninja database)
  • database.update — the update tool only (write access to the Binary Ninja database)
  • database.state — analysis status and mapping options
  • index.query — inspect existing indexes
  • index.update — update indexes
  • codemap.query — inspect existing code maps
  • codemap.update — update code maps
  • notebook.update — update notebook entries
  • docs — documentation lookup tools

This list shows common tags, not every tag.

Note

Tags match hierarchically: a tag in uses_tags matches any tool tag equal to it or beginning with it followed by a dot. Listing database therefore grants every database.* tool. If you want read-only access, list database.query instead.

When init_runtime is called with allow_updates=False, agents loaded through that runtime are constrained to read-oriented protocol tags, so database becomes database.query and explicit *.update tags are removed.

Initialize a runtime and run the agent

from sidekick.api import init_runtime, execute_bnql

with init_runtime(bv) as sess:
    renamer = sess.load_agent("rename-vars")

    large_funcs = execute_bnql(bv, "/view/function[count(block) > 100]")
    for func in large_funcs:
        print(f"Analyzing {func.name}...")
        renamer(TARGET_FUNCTION=func)

A loaded agent call waits up to timeout_secs (default 300 seconds; pass None to wait indefinitely) and raises TimeoutError if the agent has not finished. It raises RuntimeError if the agent finishes without an assistant reply. With output_schema, it raises OutputSchemaError if the reply contains no parseable JSON. Without output_schema, the call returns the reply text as a str. timeout_secs, requirements, tools, variable_render_verbosity, and variable_render_options are reserved keyword names, so do not use them as template variable names.

init_runtime(path_or_bv) creates a SessionContext. For paths, it opens the file and closes the loaded BinaryView on exit. For an existing BinaryView, it uses the caller-owned view and does not close the file on exit. The session uses the real BinjaSession kernel, so local agents, workspace resources, and service-backed Sidekick chat all share the same workspace.

Local agents loaded through sess.load_agent(...) default to tool_policy="autonomous" because the scripting API has no interactive approval surface. This only affects approval behavior for tools the agent can already see through its uses_tags. Use tool_policy="inherit" to keep the existing kernel/workspace approval policy.

Call the Sidekick service agent

Use load_sidekick_agent() when you want the built-in service-backed Sidekick chat agent — the same agent that powers the Sidekick GUI — instead of a YAML/library agent.

from sidekick.api import init_runtime

with init_runtime("/path/to/target.bndb", update_analysis=False) as sess:
    sidekick = sess.load_sidekick_agent(name="headless-analysis")
    print(sidekick("Analyze this binary and summarize the likely purpose."))
    print(sidekick("Now identify any cryptographic routines."))

load_sidekick_agent() creates a fresh chat process in the workspace and returns a LoadedSidekickAgent callable. Each call sends a user message, waits for the agent to settle, and returns the assistant's response text. Successive calls continue the same chat — context carries across.

name is a display label for the chat (it appears in the workspace's chat list and in the GUI). It is not a spec-file lookup the way load_agent("rename-vars") is — there is no YAML file to resolve.

If the chat errors, does not settle within timeout_seconds (default 300), or produces no assistant reply, the callable raises RuntimeError with the underlying detail. A timeout while submitting the message raises TimeoutError. An empty message raises ValueError, and a session with no Sidekick service client raises RuntimeError. For callers that need the full response envelope (status, pending_approval_count, message_count, the transcript), the agent stores the most recent response on agent.last_result after each call:

sidekick("Continue the analysis.")
status = sidekick.last_result["status"]
pending = sidekick.last_result["pending_approval_count"]

sidekick.chat_id is stable for the chat's lifetime. sidekick.thread_id identifies the thread the chat started on; after a long conversation is compacted, the live thread can change, so read the current thread from sidekick.last_result["thread_id"]. The chat persists in the workspace, so you can save chat_id (not thread_id) and resume the conversation from a later script via sess.call_agent(message, chat_id=saved_id, create_if_missing=False).

Each load_sidekick_agent() returns an independent chat. Multiple agents in the same session run concurrently; only repeat calls to the same agent are serialized (a chat thread accepts one outstanding message at a time).

Tool policy defaults:

Surface Default
SessionContext.load_agent(...) tool_policy="autonomous"
SessionContext.load_sidekick_agent(...) tool_policy="autonomous"
SessionContext.create_chat(...) Inherit the current workspace policy unless tool_policy= is provided

The legacy isolated-kernel runtime has been removed. Local agents loaded through the API now run on the session's real Sidekick/Binary Ninja kernel.

Structured agent output

Add output_schema to an agent definition to receive a parsed dict. Make the system prompt explicitly require JSON matching the schema.

# agent.triage.yaml
version: 1
protocol_id: triage
completion_pool_id: smart
output_schema:
  type: object
  properties:
    verdict:
      type: string
    rationale:
      type: string
  required: [verdict, rationale]
uses_tags:
  - database
prompt_template:
  messages:
    - role: system
      content: "Analyze the target and respond with JSON matching the output schema."
    - role: user
      content: "Triage this function:\n\n{{TARGET_FUNCTION}}"
  variables:
    TARGET_FUNCTION:
      name: TARGET_FUNCTION
      required: true
with init_runtime(bv) as sess:
    triage = sess.load_agent("triage")
    result = triage(TARGET_FUNCTION=func)

print(result["verdict"], result["rationale"])

Scripts root resolution

load_prompt("name") resolves prompt.<name>.yaml from a scripts root directory. Resolution order:

  1. Explicit root= parameter: load_prompt("name", root="/path/to/scripts")
  2. __sidekick_script_root variable in the caller's globals or locals (any frame in the call stack; set automatically when running scripts from the Sidekick Library panel)
  3. SIDEKICK_SCRIPTS_ROOT environment variable
  4. Value set by api.set_default_root("/path/to/scripts")

sess.load_agent("name") resolves in this order: an explicit path= to an agent YAML file, an explicit root=, a Library agent whose name matches, and then agent.<name>.yaml using the scripts root order above. path= and root= cannot be combined.

When running scripts from the Library panel, __sidekick_script_root is injected automatically and points to the script's working copy directory. You do not need to configure a root in that case.

In headless mode (api.sidekick()), the scripts root defaults to the current working directory. You can override it:

with api.sidekick("./app", scripts_root="/path/to/specs") as sk:
    ...

Pass scripts_root=None to disable automatic root configuration.

For standalone scripts or the BN scripting console, set the root explicitly:

import sidekick.api as api

api.set_default_root("/path/to/my/scripts")
prompt = api.load_prompt("summarize-function")

A prompt name may also be an absolute path to the YAML file, but a scripts root must still be resolvable (pass root=, call api.set_default_root(...), or set SIDEKICK_SCRIPTS_ROOT) because load_prompt resolves the root first. To load an agent from an explicit file without any root, use path=:

agent = sess.load_agent(path="/absolute/path/agent.rename-vars.yaml")

Managing workspace resources

The resource APIs take a BinaryView as their first argument and resolve the active session and kernel automatically.

Conventions

The Indexes, Code Maps, and Notebook APIs share a small set of conventions:

  • Session resolution. Every resource function takes bv as its first positional argument and looks up the active Sidekick session and kernel from it. A bv from a different or inactive session raises RuntimeError. Direct session and kernel access via get_session_for_view() / get_kernel_for_view() is only needed for advanced operations.
  • Object IDs are derived, not constructed. The Index APIs accept Binary Ninja objects directly — Functions, IL instructions, basic blocks, data variables, and other indexable types. Canonical object IDs such as bv:function/0x401000 or bv:function/0x401000/mlil/instruction/0x401024 (or bv://<view-prefix>/function/0x401000 for views in a project) are produced internally. The Code Maps API is the exception: its roots parameter takes canonical ID strings; see that section for how to convert objects.
  • Binary attribution is automatic. Each entry's binary_view_id is read from the object's own BinaryView, which must be open in the workspace. Objects from a view Sidekick does not manage are skipped (or raise ValueError with on_unsupported="raise").
  • prevent_duplicates=True makes add operations re-runnable. With it set, repeat calls to add_index_objects skip entries already present (matched on (binary_view_id, object_id)). Use remove_index_entries to drop entries that no longer apply.
  • Identifiers and timestamps are generated on creation. Resource IDs are created automatically when you omit them, and notebook entries populate created, updated, last_touched, and last_viewed fields as needed.

Indexes

from sidekick.api import (
    create_index, get_index, list_indexes,
    update_index, delete_index,
    add_index_objects, update_index_entries, remove_index_entries,
)

# Create
idx = create_index(bv, "interesting-functions", metadata={"owner": "user"})

# List and retrieve
print([r.resource_id for r in list_indexes(bv)])
same_idx = get_index(bv, "interesting-functions")

# Add Binary Ninja objects as entries. The API derives a canonical object_id
# from each object and attributes the entry to bv.
entries = add_index_objects(bv, "interesting-functions", objects=[func_a, func_b])

# Update entry attributes by entry_id (priority is an entry field)
update_index_entries(bv, "interesting-functions", [entries[0].entry_id], {"priority": 10})

# Merge or remove metadata keys instead of replacing the whole metadata dict
update_index_entries(
    bv, "interesting-functions", [entries[0].entry_id],
    metadata_updates={"reviewed": True}, metadata_keys_to_remove=["stale"],
)

# Remove entries
remove_index_entries(bv, "interesting-functions", [entries[1].entry_id])

# Rename or update the index itself
update_index(bv, "interesting-functions", name="priority-functions", index_type="manual")

# Delete
delete_index(bv, "priority-functions")

Indexable object types

add_index_objects accepts the following kinds of Binary Ninja objects:

  • Functions and IL functions (LLIL, MLIL, HLIL)
  • Assembly instructions and IL instructions (LLIL, MLIL, HLIL)
  • Basic blocks and IL basic blocks (LLIL, MLIL, HLIL)
  • Data variables and local variables
  • Strings, types, symbols
  • Segments, sections, regions, components
  • Comments, tags, constants
  • Addresses, function call arguments, and structure/union type members

BinaryView itself, SSA variables, type libraries and their types and objects, platform and architecture objects, external libraries and locations, Sidekick index and index-entry objects, and the address-range, concept, and graph-path query nodes are not indexable.

By default, unsupported objects are silently skipped. Pass on_unsupported="raise" to surface them as ValueError instead.

Re-running indexing scripts

Pass prevent_duplicates=True to add_index_objects so repeat calls skip entries already in the index. Deduplication uses the natural key (binary_view_id, object_id).

add_index_objects(bv, "interesting-functions", objects=funcs, prevent_duplicates=True)

add_index_objects returns the entries it built from your objects, including any skipped as duplicates, so those entry IDs may not exist in the index. To get valid entry IDs for follow-up calls, read them from get_index(bv, name).get_snapshot().

To remove entries that no longer apply on a re-run, compute the set of stale entry_ids and call remove_index_entries(bv, name, entry_ids). There is no need to track the binary view ID yourself; entries already carry it.

Code Maps

from sidekick.api import create_code_map, update_code_map, get_code_map, delete_code_map

code_map = create_code_map(
    bv,
    "parser-map",
    roots=["bv:function/0x401000"],
    predecessor_hops=1,
    successor_hops=2,
)

update_code_map(bv, "parser-map", node_width=96)

resolved = get_code_map(bv, code_map.resource_id)
delete_code_map(bv, resolved.resource_id)

roots is a list of canonical object ID strings, not Binary Ninja objects. To convert a BN object to its canonical ID, use make_id:

from sidekick.binja.entities.object_ids import make_id

main = bv.get_functions_by_name("main")[0]
code_map = create_code_map(bv, "main-map", roots=[make_id(main)])

Passing a Binary Ninja object directly to roots raises TypeError. Use make_id (or hand-build the ID string) to convert first.

Note

Manual graph payload updates (manual_nodes / manual_edges) are not supported in the v1 API. Only auto-config fields can be set through create_code_map and update_code_map.

Notebook

Notebook helpers operate directly on notebook entry resources.

from sidekick.api import (
    list_notebook_entries, get_notebook_entry,
    get_notebook_entry_context,
    create_notebook_entry, update_notebook_entry, delete_notebook_entry,
    update_notebook_entry_context,
    add_notebook_task, update_notebook_task, start_notebook_task,
    complete_notebook_task, delete_notebook_task,
    add_notebook_outcome, list_notebook_outcomes,
    get_notebook_outcome, update_notebook_outcome, delete_notebook_outcome,
    add_notebook_blocker, get_notebook_blocker,
    resolve_notebook_blocker, withdraw_notebook_blocker, delete_notebook_blocker,
)

# Create an entry
entry = create_notebook_entry(
    bv,
    "Investigate custom crypto routine",
    relevant_domains=["crypto"],
)

# Inspect entries
entries = list_notebook_entries(bv)
same_entry = get_notebook_entry(bv, entry.id)
print(same_entry.analysis_context)

# Record structured intermediate analysis state
update_notebook_entry_context(
    bv,
    entry.id,
    set={"scope": {"strategy": "small-exhaustive", "functions": ["sub_4011a0"]}},
)
print(get_notebook_entry_context(bv, entry.id))

# Add and complete a task
task = add_notebook_task(bv, entry.id, "Trace key schedule")
update_notebook_task(bv, entry.id, task.id, status="in_progress")
complete_notebook_task(bv, entry.id, task.id)

# Record a finding
outcome = add_notebook_outcome(
    bv,
    entry.id,
    kind="finding",
    title="Round key update located at sub_4011a0",
    confidence=0.85,
)

print(get_notebook_outcome(bv, entry.id, outcome.id))
print(list_notebook_outcomes(bv, entry.id))
delete_notebook_entry(bv, entry.id)

Prefer omitting entry_id. Sidekick generates an opaque stable ID automatically, and that generated entry.id should be the value you pass to follow-on helpers such as get_notebook_entry, update_notebook_entry, add_notebook_task, and add_notebook_outcome. Supplying your own entry_id is only useful when you need to preserve an external identifier across systems or imports; it should not be treated as the user-facing title.

Notebook entries expose created, updated, last_touched, and last_viewed timestamps. relevant_domains is free-form text; using consistent domain names like "crypto", "networking", or "parser" across entries makes it easier to filter and group them later.

Entries may also carry analysis_context: structured intermediate state for tooling and scripts. Use it for durable machine-readable blocks such as scope, candidate lists, partition rules, or coverage manifests. It is separate from user-facing notebook outcomes and blockers. get_notebook_entry(bv, entry_id) exposes the field directly on the returned resource, and get_notebook_entry_context(...) returns a defensive copy.

The second argument to create_notebook_entry is the entry's description.

Valid entry status values: "active", "closed". To close an entry, pass a resolution: update_notebook_entry(bv, entry.id, status="closed", resolution="achieved", resolution_summary="..."). Valid resolution values: "achieved", "partial", "infeasible", "deferred". Closing without a resolution raises ValueError. Passing status="active" reopens a closed entry and clears its resolution.

Task status values: "pending", "in_progress", "completed", "blocked".

Valid outcome kind values: "finding", "artifact". The legacy values "claim", "summary", and "evidence" are accepted and stored as "finding". Blockers are a separate record type; see below.

Valid outcome status values: "draft", "verified", "rejected". Outcomes also carry an orthogonal important: bool flag that is independent of status.

Blockers record that outside input is needed to proceed. add_notebook_blocker(bv, entry.id, title, needs="...") creates an open blocker. Close it with resolve_notebook_blocker(bv, entry.id, blocker.id, resolution="<the input supplied>") or withdraw_notebook_blocker(bv, entry.id, blocker.id, resolution="<why it no longer applies>"). Use delete_notebook_blocker to remove one created in error. Blocker status values: "open", "resolved", "withdrawn".


Session and kernel access

Most automation scripts do not need these helpers — every resource API resolves the active session and kernel from bv automatically. Reach for these only when you need to call something on the session or kernel directly.

from sidekick.api import get_session_for_view, get_kernel_for_view

session = get_session_for_view(bv)   # BinjaSession
kernel  = get_kernel_for_view(bv)    # Kernel

Both raise RuntimeError if no active Sidekick session exists for the view.


Configuring completion pools

Agents and prompts reference a completion_pool_id in their YAML. A completion pool maps a logical name (like fast or smart) to one or more model endpoints with parameters. If the first endpoint fails, the pool automatically tries the next (failover).

Shipped defaults

Sidekick ships default endpoints and pools that the Binary Ninja plugin installs to your user directory on first launch:

Endpoint Service Description
sidekick sidekick Zero-config. Proxies through the Sidekick cloud service using your existing API key.
ollama openai-compatible Template for local Ollama models. Edit service_model to match your installed model.
vllm openai-compatible Template for self-hosted vLLM deployments. Edit service_model and base_url.
anthropic-claude-sonnet anthropic Direct Anthropic API. Requires ANTHROPIC_API_KEY environment variable.
openai-gpt-4.1 openai Direct OpenAI API. Requires OPENAI_API_KEY environment variable.
Pool Default endpoint Use
smart sidekick Default pool for your own agents and prompts that need multi-step reasoning (used by new Library agent templates)
fast sidekick Suggested pool for lightweight single-shot prompts and classification (temperature 0.3)

The sidekick endpoint works immediately if you have a Sidekick API key configured. For better quality or fully offline use, add your own endpoints as described below.

Customizing

Configure endpoints and pools through Plugins > Sidekick > Configure Automation Models... in Binary Ninja. The configuration is stored as YAML files in:

BN_USER_DIRECTORY/sidekick/config/completion_routing/

Endpoint types:

  • sidekick — Proxied through the Sidekick cloud service. No API key or base URL needed.
  • anthropic — Direct Anthropic API. Set api_key_env to the environment variable holding your key.
  • openai — Direct OpenAI API. Set api_key_env to the environment variable holding your key.
  • openai-compatible — Any OpenAI-compatible API (Ollama, vLLM, LM Studio, etc.). Set base_url and service_model. No API key required for most local servers (api_key_env: null).

Example: adding a local model to your smart pool

First, edit the Ollama endpoint (endpoint.ollama.yaml) with your model name:

name: ollama
service: openai-compatible
service_model: qwen2.5-coder:14b
base_url: http://localhost:11434/v1
api_key_env: null
tokenizer: cl100k_base
max_tokens: 128000
max_completion_tokens: 4096
pricing:
  prompt: 0.0
  cached: 0.0
  completion: 0.0
  reasoning: 0.0
  cache_writing: 0.0

Then add it to the smart pool as the primary endpoint, with sidekick as failover:

group_id: smart
completers:
  - endpoint_id: ollama
    parameters:
      temperature: 0.2
  - endpoint_id: sidekick
    parameters:
      temperature: 0.2

In your agent or prompt YAML:

completion_pool_id: smart

Swapping the underlying model only requires updating the endpoint definition. Agent and prompt YAML files do not need to change.

Tip

Use a fast pool backed by a smaller model for single-shot prompts and classification tasks. Reserve a smart pool backed by a capable model for agents that need multi-step reasoning.


API reference summary

Context management (headless)

Symbol Description
sidekick(app_dir, *, semantic_index=False, cleanup_app_dir=False, scripts_root="", ...) Create a SidekickContext for headless use; preserve app_dir by default or clean it up on exit for scratch runs; scripts_root sets the default agent/prompt lookup directory (defaults to cwd; pass None to disable)
SidekickContext.session(target, ...) Open a SessionContext for a file or project
init_runtime(target, *, update_analysis=True, load_options=None, allow_updates=True, ...) Recommended entry point. Opens or borrows a SessionContext for a path, project, FileMetadata, or BinaryView. When a BinjaSession already exists for the target (e.g. running inside Binary Ninja), it is borrowed; otherwise the runtime bootstraps a headless Sidekick application and closes the loaded file on exit
SessionContext.bv The active BinaryView (raises if none)
SessionContext.views All BinaryView objects in the session
SessionContext.open_project_file(selector, *, update_analysis=True, make_current=True) Open a project member and add it to the session
SessionContext.binary_view_set The BinaryViewSet for the session
SessionContext.wait_for_semantic_index_ready(timeout_secs=None) Block until the semantic index is ready
SessionContext.call_agent(message, *, chat_id=None, thread_id=None, binary_view_id=None, protocol_id="sidekick", create_if_missing=True, timeout_seconds=None, author=None, tool_policy=None) Send a message to a service-backed chat; returns a dict with success, response, chat_id, thread_id, status, message_count, pending_approval_count, messages, and error. The default timeout is 300 seconds
SessionContext.get_chat_history(*, chat_id=None, thread_id=None, include_tool_calls=False, limit=None) Transcript for a chat
SessionContext.get_chat_status(*, chat_id=None, thread_id=None) Execution status of a chat (status, status_detail, pending_approval_count, can_accept_user_message, ...)
SessionContext.list_chats(*, protocol_id=None, limit=None), create_chat(...), rename_chat(...), remove_chat(...) Manage chats in the workspace
SessionContext.list_targets(*, include_metadata=True), focus_target(binary_view_id), ensure_focused_target() List and focus binaries in the session
SessionContext.list_tools(), get_resource(name), list_project_files(pattern=None) Inspect tools and resources; list_project_files applies to project sessions only

Analysis

Symbol Description
execute_bnql(target, query, *, limit=None, offset=None, time_limit_secs=30) Run a BNQL query; returns a list of objects. offset skips results (0-indexed); time_limit_secs bounds evaluation (None for no limit), and a timeout does not raise (it returns whatever was already collected, often an empty list)
concept(target, subject, object_type=None, similarity=0.45, limit=10) Semantic search; returns list[(obj, score, similarity)]
render_object(obj, verbosity="summary", options=None) Render a BN object to text; options is a dict or a JSON / key=value,... string of renderer parameters
DetailLevel Enum: IDENTIFIER, SUMMARY, CONTENTS, CONTEXTUAL

Agents and prompts

Symbol Description
SessionContext.load_agent(name=None, *, path=None, root=None, completion_pool=None, tool_policy="autonomous", variable_render_verbosity="contents", variable_render_options=None, allow_updates=None) Load a local Library-backed or YAML-backed agent bound to the session kernel; returns a LoadedAgent callable, invoked as LoadedAgent(*, timeout_secs=300, variable_render_verbosity=None, variable_render_options=None, **variables). Resolves path=, then root=, then a matching Library agent, then the scripts root
SessionContext.load_sidekick_agent(*, name=None, tool_policy="autonomous", ...) Create a callable service-backed Sidekick chat agent in the session; returns a LoadedSidekickAgent
LoadedSidekickAgent(message, *, timeout_seconds=None) -> str Send a message and return the assistant's reply. Raises RuntimeError on failure (TimeoutError if submitting the message times out). agent.chat_id, agent.thread_id, and agent.last_result expose the underlying chat state
load_prompt(name, *, root=None, completion_pool=None) Load a prompt and return a LoadedPrompt callable
list_agents(root=None) List agent.*.yaml names in the scripts root
list_prompts(root=None) List prompt.*.yaml names in the scripts root
set_default_root(root) Set the default scripts root directory
get_default_root() Get the current default scripts root
get_scripts_dir(root=None) Resolve and return the scripts root as a Path

Session and kernel

Symbol Description
get_session_for_view(bv) Return the BinjaSession for bv
get_kernel_for_view(bv) Return the Kernel for bv

Index resource

In the Index and Code Map resource tables, process_id scopes the lookup or creation to a particular kernel process, such as a chat. Omit it to use the workspace-wide namespace; list_indexes and list_code_maps without it return resources from all processes.

Symbol Description
list_indexes(bv, *, process_id=None) List all index resources
get_index(bv, name_or_id, *, process_id=None, create_if_missing=False) Get an index by name or resource ID
create_index(bv, name, *, process_id=None, metadata=None, index_type=None) Create a new index
update_index(bv, name_or_id, *, process_id=None, name=None, metadata=None, index_type=None) Update index fields
delete_index(bv, name_or_id, *, process_id=None) Delete an index; returns bool
add_index_objects(bv, name_or_id, objects, *, process_id=None, metadata=None, source_id=None, prevent_duplicates=False, on_unsupported="skip") Add BN objects as index entries; returns the list of built IndexEntry objects, including any skipped as duplicates
update_index_entries(bv, name_or_id, entry_ids, attribute_updates=None, *, process_id=None, metadata_updates=None, metadata_keys_to_remove=None) Update entries by ID; attribute_updates["metadata"] replaces all metadata, metadata_updates merges keys, metadata_keys_to_remove drops keys; returns the updated entries
remove_index_entries(bv, name_or_id, entry_ids, *, process_id=None) Remove entries by ID; returns removed count

Code Map resource

Symbol Description
list_code_maps(bv, *, process_id=None) List all code map resources
get_code_map(bv, name_or_id, *, process_id=None, create_if_missing=False) Get a code map
create_code_map(bv, name, *, process_id=None, roots=None, predecessor_hops=None, ...) Create a code map
update_code_map(bv, name_or_id, *, process_id=None, ...) Update code map fields
delete_code_map(bv, name_or_id, *, process_id=None) Delete a code map; returns bool

Notebook entries

Symbol Description
list_notebook_entries(bv) List all notebook entries, sorted by last_touched descending
get_notebook_entry(bv, entry_id) Get an entry by ID
get_notebook_entry_context(bv, entry_id) Get a deep copy of the entry's analysis_context
create_notebook_entry(bv, description, *, relevant_domains=None, entry_id=None) Create an entry; omit entry_id in normal usage so Sidekick generates one
update_notebook_entry(bv, entry_id, *, description=None, status=None, resolution=None, resolution_summary=None, relevant_domains=None) Update entry fields; closing (status="closed") requires resolution; returns bool
update_notebook_entry_context(bv, entry_id, *, set=None, remove=None, clear=False) Update top-level analysis_context blocks; returns bool
delete_notebook_entry(bv, entry_id) Delete an entry; returns bool
add_notebook_task(bv, entry_id, description) Add a task
update_notebook_task(bv, entry_id, task_id, ...) Update a task; returns bool
start_notebook_task(bv, entry_id, task_id) Mark a pending or blocked task in progress; returns bool
complete_notebook_task(bv, entry_id, task_id) Mark a task complete; returns bool
delete_notebook_task(bv, entry_id, task_id) Delete a task; returns bool
add_notebook_outcome(bv, entry_id, kind, title, ...) Add an outcome; kind is "finding" or "artifact"
update_notebook_outcome(bv, entry_id, outcome_id, ...) Update an outcome; returns bool
delete_notebook_outcome(bv, entry_id, outcome_id) Delete an outcome; returns bool
get_notebook_outcome(bv, entry_id, outcome_id) Get an outcome by ID
list_notebook_outcomes(bv, entry_id, *, kind=None, ...) List outcomes with optional filters
add_notebook_blocker(bv, entry_id, title, *, description="", needs="", task_ids=None, references=None, source="user", blocker_id=None) Record a blocker that needs outside input
get_notebook_blocker(bv, entry_id, blocker_id) Get a blocker by ID
resolve_notebook_blocker(bv, entry_id, blocker_id, resolution) Resolve a blocker; the resolution text carries the supplied input; returns bool
withdraw_notebook_blocker(bv, entry_id, blocker_id, resolution) Withdraw a blocker that no longer applies; returns bool
delete_notebook_blocker(bv, entry_id, blocker_id) Delete a blocker; returns bool