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 intoBN_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¶
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:
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 optionallyvariable_render_options=) to the agent call, or tosess.load_agent(...)to set the default for that loaded agent. - Prompts: set
detail_level:(identifier,summary,contents, orcontextual) and optionalrender_params:on the variable in the prompt YAML, for exampleCODE: {name: CODE, required: true, detail_level: summary}. Avariable_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 taggeddatabase.*:database.query,database.update, anddatabase.statedatabase.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 optionsindex.query— inspect existing indexesindex.update— update indexescodemap.query— inspect existing code mapscodemap.update— update code mapsnotebook.update— update notebook entriesdocs— 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:
- Explicit
root=parameter:load_prompt("name", root="/path/to/scripts") __sidekick_script_rootvariable in the caller's globals or locals (any frame in the call stack; set automatically when running scripts from the Sidekick Library panel)SIDEKICK_SCRIPTS_ROOTenvironment variable- 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:
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=:
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
bvas its first positional argument and looks up the active Sidekick session and kernel from it. Abvfrom a different or inactive session raisesRuntimeError. Direct session and kernel access viaget_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/0x401000orbv:function/0x401000/mlil/instruction/0x401024(orbv://<view-prefix>/function/0x401000for views in a project) are produced internally. The Code Maps API is the exception: itsrootsparameter takes canonical ID strings; see that section for how to convert objects. - Binary attribution is automatic. Each entry's
binary_view_idis read from the object's ownBinaryView, which must be open in the workspace. Objects from a view Sidekick does not manage are skipped (or raiseValueErrorwithon_unsupported="raise"). prevent_duplicates=Truemakes add operations re-runnable. With it set, repeat calls toadd_index_objectsskip entries already present (matched on(binary_view_id, object_id)). Useremove_index_entriesto 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, andlast_viewedfields 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 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:
Endpoint types:
sidekick— Proxied through the Sidekick cloud service. No API key or base URL needed.anthropic— Direct Anthropic API. Setapi_key_envto the environment variable holding your key.openai— Direct OpenAI API. Setapi_key_envto the environment variable holding your key.openai-compatible— Any OpenAI-compatible API (Ollama, vLLM, LM Studio, etc.). Setbase_urlandservice_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:
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 |