Ghidra RPC for Reverse Engineering

Reverse engineering assistant powered by Ghidra. Use for binary analysis, decompilation, vulnerability research, auditing compiled code.

Sby Skills Guide Bot
SecurityAdvanced
1308/14/2026
Claude CodeCursorWindsurfCopilotCodex
#ghidra#reverse-engineering#binary-analysis#decompilation#vulnerability-research

Recommended for


name: ghidra-rpc description: > Reverse engineering assistant powered by Ghidra. Use for binary analysis, decompilation, vulnerability research, auditing compiled code, renaming symbols, annotating disassembly, cross-references, or any RE task - even without explicit mention of Ghidra.

Ghidra RPC

A CLI daemon that exposes Ghidra reverse engineering capabilities over a Unix domain socket. You interact with it by running ghidra-rpc commands via the bash tool - every command outputs JSON to stdout.

Running Commands

All ghidra-rpc commands must be run with uv run from the ghidra-rpc skill directory (the directory containing this SKILL.md file):

cd <skill-dir> && uv run ghidra-rpc <command> ...

Prerequisites Check

Before using any commands, verify the setup:

  1. Is the daemon running? Run uv run ghidra-rpc status --project <path>. If not running, the user needs to start it in a separate terminal: uv run ghidra-rpc start --project /path/to/project.gpr (blocking, human-only). The --headless flag skips the GUI. For non-blocking / automated startup use start --detach --headless or restart --headless. Both commands accept --timeout SECS (default: 60 s headless, 180 s GUI) and log daemon output to /tmp/ghidra-rpc-<hash>.log.
  2. GUI mode: verify the correct project is active. The daemon passes the --project path directly to GhidraRun so Ghidra opens the requested project immediately. After start --detach (GUI mode), confirm the right project is loaded before issuing any analysis commands:
    uv run ghidra-rpc list-project-programs --project /path/to/project.gpr
    
    Cross-check the returned binary names against what you expect. If the names don't match (or a warning field appears in the response), stop and ask the user to open the correct project in Ghidra's Project window before proceeding.
  3. GUI mode: open the binary in CodeBrowser. In GUI mode the daemon discovers programs via the running CodeBrowser tool. If the binary is in the project but not currently open in CodeBrowser, list-binaries will return empty and all commands will fail with "Binary not found". Use list-project-programs to see what's stored in the project, then double-click the program in Ghidra's Project window to open it.
  4. Is GHIDRA_INSTALL_DIR set? The daemon will fail loudly if this is missing.

Once the daemon is running, all commands below work automatically. If the daemon dies, commands will attempt auto-restart from the saved session.

Command Reference

Every command accepts --project <path> (or reads GHIDRA_RPC_PROJECT env var). All output is JSON. Exit code 0 = success, 1 = error.

Project Management

| Command | Description | Example | |---------|-------------|---------| | ghidra-rpc start --project <gpr> [--headless] [--detach] [--ghidra-install-dir DIR] | Start daemon (blocking by default; --detach backgrounds it) | ghidra-rpc start -p /tmp/re.gpr --headless --detach | | ghidra-rpc stop --project <gpr> | Stop the daemon | ghidra-rpc stop -p /tmp/re.gpr | | ghidra-rpc status --project <gpr> | Check if daemon is running | ghidra-rpc status -p /tmp/re.gpr |

status output: {running, socket, mode, mode_source, project, binaries}. binaries is the loaded-binary list when the daemon is running, null when stopped (one-shot health check without a second list-binaries call). mode_source is "running" when the daemon is live, "session" when the mode is from the saved session only (daemon stopped), and null if no session exists. | ghidra-rpc restart --project <gpr> [--headless] [--timeout SECS] [--ghidra-install-dir DIR] | Restart daemon in background (--headless to override mode) | ghidra-rpc restart -p /tmp/re.gpr --headless | | ghidra-rpc list-binaries --project <gpr> | List binaries loaded in daemon | ghidra-rpc list-binaries -p /tmp/re.gpr | | ghidra-rpc list-project-programs --project <gpr> | List all programs stored in the project repo (reads project folder, not CodeBrowser live state; returns warning if the active Ghidra project doesn't match) | ghidra-rpc list-project-programs -p /tmp/re.gpr | | ghidra-rpc save [binary] --project <gpr> | Save program(s) to disk (auto-save also runs after every write) | ghidra-rpc save -p /tmp/re.gpr |

Loading & Analysis

| Command | Description | Example | |---------|-------------|---------| | ghidra-rpc load <path> -p <gpr> [--no-analyze] [--analysis-timeout SECS] | Import & analyze a binary | ghidra-rpc load /usr/bin/target -p /tmp/re.gpr | | ghidra-rpc metadata <binary> -p <gpr> | Binary metadata (arch, bits, format) | ghidra-rpc metadata target -p /tmp/re.gpr |

load flags:

  • --no-analyze - skip auto-analysis entirely (fast; useful when you only need the listing or plan to run analysis later via Ghidra GUI).
  • --analysis-timeout SECS - abort auto-analysis after the given wall-clock budget (best-effort); the binary is saved with whatever analysis completed in time.

Listing

| Command | Description | Output shape | |---------|-------------|--------------| | ghidra-rpc functions <binary> [--offset N] [--limit N] [--address-min ADDR] [--address-max ADDR] [--with-body] | List functions (paginated); --address-min/max for server-side range filter; --with-body adds body range fields | {functions: [{name, address, signature[, body_min, body_max, body_size]}], count, total, offset} | | ghidra-rpc imports <binary> | List imports | {imports: [{name, address, library}], count} | | ghidra-rpc exports <binary> | List exports | {exports: [{name, address}], count} | | ghidra-rpc relocations <binary> [--address ADDR] [--limit N] | List relocation table entries | {relocations: [{address, type, symbol, bytes, status}], count, total} | | ghidra-rpc list-calling-conventions <binary> | List valid calling convention names for the architecture | {conventions: [str], default, count} |

Decompilation

| Command | Description | Output shape | |---------|-------------|--------------| | ghidra-rpc decompile <binary> <func> [--timeout SECS] | Decompile to pseudo-C (default 120 s) | {name, address, signature, c_code} | | ghidra-rpc basic-blocks <binary> <func> [--limit N] | Get basic blocks (CFG) of a function | {name, address, blocks:[{start,end,size,instructions,successors,predecessors}], num_blocks, edges} | | ghidra-rpc pcode <binary> <func> [--high] [--timeout SECS] [--limit N] | Get P-code (Ghidra IR); raw or high SSA form (--high) | {name, address, mode, ops:[{address,seq,opcode,output,inputs}], count, truncated} | | ghidra-rpc search-decompiled <binary> <regex> [--class NAME] [--case-sensitive] [--limit N] [--max-scan N] [--timeout SECS] [--socket-timeout SECS] | Regex-search decompiled C across many functions in one call (skips one-RPC-per-function decompile+grep loops) | {matches:[{function,address,matching_lines:[{line,text}]}], count, functions_searched, functions_total, truncated} |

<func> can be a function name or hex address (e.g., main or 0x401000). If the name is ambiguous, the error message lists matches so you can use the address instead.

search-decompiled is especially useful on multidex Android projects, where xrefs-to on a method only sees callers within the same DEX program (see the Android APK/DEX section) — use it in place of a symbols+decompile+grep loop, e.g. search-decompiled app.apk targetF1 --class com::example::Foo. --max-scan (default 5000) bounds how many functions get decompiled when --class isn't given, so a search on a 50k+-function binary can't run away; raise --socket-timeout (default 1800s) alongside it if you do widen the scan.

Search

| Command | Description | Output shape | |---------|-------------|--------------| | ghidra-rpc find-bytes <binary> <pattern> [--limit N] [--address ADDR] | Search for byte pattern with wildcards (e.g. "55 8b ?? 83 ec") | {pattern, matches: [{address, context_hex}], count, truncated} | | ghidra-rpc strings <binary> <query> [--limit N] | Search strings (substring) | {strings: [{address, value, type}], count} | | ghidra-rpc symbols <binary> <query> [--limit N] [--offset N] | Search symbols | {symbols: [{name, address, type}], count, total} |

symbols normalizes literal spaces and underscores as equivalent before matching, so a query pasted straight from strings output (real spaces) still finds the corresponding label even though Ghidra auto-generates labels from string content by turning each space into _ (and leaving every other character, including non-ASCII/CJK text, untouched).

Cross-References

| Command | Description | Output shape | |---------|-------------|--------------| | ghidra-rpc xrefs-to <binary> <target> [--limit N] [--all-binaries] | Who references this? | {xrefs: [{from_address, from_function, type}], count} | | ghidra-rpc xrefs-from <binary> <target> [--limit N] [--no-stack] | What does this reference? (--no-stack hides Stack[-0x...] entries) | {xrefs: [{to_address, to_function, type}], count} |

Ghidra's reference manager is per-binary: a call whose caller and target live in two different loaded binaries is invisible to a single-binary xrefs-to lookup (most commonly hit on multidex Android projects, where a method in one classesN.dex is called from another, but this isn't DEX-specific — it applies to any project with more than one binary loaded in the daemon). Pass --all-binaries to also search every other currently loaded binary for a symbol with the same fully-qualified name and merge in real callers found there; each merged entry carries a binary field so you can tell which binary it came from.

Navigation (GUI mode only)

| Command | Description | Output shape | |---------|-------------|--------------| | ghidra-rpc goto <binary> <target> [function\|address] | Navigate GUI | {address, success} |

Memory & Disassembly

| Command | Description | Output shape | |---------|-------------|--------------| | ghidra-rpc memory-map <binary> | List all memory segments (name, address range, size, rwx flags, type) | {segments: [{name, start, end, size, read, write, execute, initialized, type, source_name}], count} | | ghidra-rpc write-bytes <binary> <address> <hex> | Write raw bytes to an address; does NOT auto-redisassemble | {address, length, hex, verified} | | ghidra-rpc read-bytes <binary> <address> <length> | Read raw bytes; LENGTH accepts decimal or 0x hex | {address, length, hex, hexdump} | | ghidra-rpc read-pointers <binary> <address> <count> [--pointer-size N] | Read COUNT pointers and resolve each to its symbol (vtables, import/jump tables, RTTI arrays) | {address, count, pointer_size, endian, pointers:[{index, offset, slot_address, value, target_address, target_name, target_kind}]} | | ghidra-rpc list-vtable <binary> <address> [--count N] [--pointer-size N] | Dump a C++ vtable's slots as resolved methods; ADDRESS may be a symbol name. Without --count, stops at the next vftable symbol / first non-function pointer (see stopped_reason) | {vtable_address, symbol, count, pointer_size, stopped_reason, slots:[{index, offset, slot_address, target_address, target_name, target_kind}]} | | ghidra-rpc disassemble <binary> <address> [--count N] [--with-instructions] | Disassemble N instructions (default 20); returns listing text, which already carries address/bytes/mnemonic/operands/comment — pass --with-instructions only when you need the machine-parseable array (~4× the output); warning field if start address had no instruction | {address, count, listing[, instructions][, warning]} | | ghidra-rpc assemble <binary> <address> <instr> [<instr> ...] | Assemble instruction text at address (Ghidra SLEIGH assembler) | {address, bytes_written, hex, instructions:[{address,bytes,mnemonic,operands,length}]} |

Modifications

| Command | Description | Output shape | |---------|-------------|--------------| | ghidra-rpc create-function <binary> <address> [--name NAME] | Create a function at an address (auto-detects body from flow) | {name, address, size, body} | | ghidra-rpc delete-function <binary> <target> | Remove a function definition (bytes unchanged; address reverts to undefined) | {address, name, deleted} | | ghidra-rpc rename-function <binary> <target> <new_name> [--namespace NS] | Rename function; --namespace moves it into a namespace | {address, old_name, new_name, verified[, old_namespace, new_namespace]} | | ghidra-rpc rename-symbol <binary> <address> <new_name> [--create] | Rename symbol; --create makes a new label if none exists | {address, old_name, new_name, created, verified} | | ghidra-rpc create-label <binary> <address> <name> | Create-or-rename a label at any address (upsert) | {address, name, old_name, action, created, verified} | | ghidra-rpc set-comment <binary> <address> --comment <text> [--type TYPE] | Set comment (comment text is --comment, matching set-bookmark) | {address, comment_type, comment, verified} | | ghidra-rpc batch-rename <binary> --json \|--json-file FILE [--mode function\|label] | Rename 40+ functions/labels in one transaction; per-item error reporting | {results:[{ok, index, address, old_name, new_name}], count, ok_count, error_count} | | ghidra-rpc batch-set-comment <binary> --json \|--json-file FILE | Set comments at many addresses in one transaction | {results:[{ok, index, address, comment_type, comment}], count, ok_count, error_count} | | ghidra-rpc set-signature <binary> <target> <signature> | Set function signature | {address, old_signature, new_signature, verified} | | ghidra-rpc set-data-type <binary> <address> <type> | Define data type in listing | {address, data_type, length, value} | | ghidra-rpc retype-variable <binary> <func> <variable> <type> [--timeout SECS] | Retype decompiler variable | {function, variable, old_type, new_type, verified} | | ghidra-rpc rename-variable <binary> <func> <variable> <new_name> [--timeout SECS] | Rename decompiler variable | {function, variable, new_name, verified} | | ghidra-rpc batch-edit-variable <binary> <func> --json \|--json-file FILE [--timeout SECS] | Rename and/or retype many locals in ONE decompiler snapshot — avoids the auto-name renumbering that breaks chained single edits. Each op: {variable\|storage, new_name?, data_type?} (identify by name or storage string; at least one of new_name/data_type) | {function, results:[{ok, index, variable, storage, old_name, new_name, old_type, new_type, verified}], count, ok_count, error_count, verified_count} | | ghidra-rpc set-calling-convention <binary> <target> <convention> | Change a function's calling convention | {address, name, old_convention, new_convention, verified} | | ghidra-rpc set-thunk <binary> <thunk> <target> | Mark a function as a thunk (forwarding wrapper) | {thunk_address, thunk_name, target_address, target_name, verified} | | ghidra-rpc set-flow-override <binary> <address> <override> | Override instruction flow type (NONE/BRANCH/CALL/CALL_RETURN/RETURN) | {address, override, old_override, verified} | | ghidra-rpc get-processor-context <binary> <address> [--register REG] | Inspect ISA context register values (e.g. TMode on ARM) | {address, registers:{name:value}} | | ghidra-rpc set-processor-context <binary> <address> <register> <value> [--end ADDR] | Set ISA context register over a range — fixes ARM Thumb mis-classification | {address, end, register, value, verified} | | ghidra-rpc create-namespace <binary> <name> [--parent NS] | Create or look up a namespace | {name, path, id, created} | | ghidra-rpc list-namespaces <binary> [--limit N] | List all namespaces with symbol counts | {namespaces:[{name,path,id,type,symbol_count}], count} |

Comment types: plate, pre, post, eol (default), repeatable.

Signature input is sanitised automatically: trailing semicolons are stripped, whitespace is trimmed, and inline calling conventions (__thiscall, __fastcall, __stdcall, __cdecl, __vectorcall, __pascal) are extracted from the string and applied via the proper Ghidra API. So you can paste a C prototype directly (e.g. void __thiscall Foo::Bar(int x); works without manual cleanup).

Type expressions for set-data-type, retype-variable, and struct field types:

  • Built-ins: byte, char, int, uint, short, long, float, double, void, string, unicode
  • Pointer: char *, void *, int **
  • Array: char[11], int[4]
  • Any type in the program's data type manager by name or path: MyStruct, /POSIX/size_t

Order of operations — set signatures/types before naming locals. Each of set-signature, set-data-type, retype-variable, and rename-variable re-runs the decompiler, which re-numbers the remaining auto-named locals (uVar1, dVar2, …). So chaining single-variable edits is fragile: after renaming dVar1, the old dVar2 may now be dVar1. Do all signature/this-typing first, then name locals — or, better, use batch-edit-variable to apply every rename and retype against a single decompiler snapshot in one call (it also lets you address a variable by its stable storage string instead of the volatile auto name).

Bookmarks

| Command | Description | Output shape | |---------|-------------|--------------| | ghidra-rpc set-bookmark <binary> <address> [--type TYPE] [--category CAT] [--comment TEXT] | Create/update a bookmark at an address | {address, type, category, comment, action} | | ghidra-rpc list-bookmarks <binary> [--type TYPE] [--category CAT] [--address ADDR] [--limit N] | List bookmarks (all, by type, category, or at address) | {bookmarks: [{address, type, category, comment}], count, total} | | ghidra-rpc remove-bookmark <binary> <address> [--type TYPE] [--category CAT] | Remove a bookmark at an address | {address, type, removed} |

Bookmark types: Note (default), Warning, Error, Info, Analysis.

--category is a case-insensitive substring filter. On heavily auto-analyzed binaries (large native programs, and especially DEX/Dalvik — 50k+ functions is normal), Ghidra generates many Analysis-type bookmarks (commonly category Address Table) that can bury a handful of your own; filter them out with --category (or --type to isolate your own type, e.g. Note) instead of paging through the unfiltered list.

Bookmarks are visible in the Ghidra GUI's Bookmarks window. Use them to persistently mark interesting locations, track analysis progress, or flag findings for human review.

Function Tags

| Command | Description | Output shape | |---------|-------------|--------------| | ghidra-rpc tag-function <binary> <target> --tag TAG | Add a tag to a function for classification | {address, name, tag, all_tags} | | ghidra-rpc untag-function <binary> <target> --tag TAG | Remove a tag from a function | {address, name, tag, removed, all_tags} | | ghidra-rpc list-tags <binary> | List all defined tags with use counts | {tags:[{name,count}], count} | | ghidra-rpc functions-by-tag <binary> --tag TAG [--limit N] | List functions with a specific tag | {tag, functions:[{name,address,signature}], count, total} |

Tags are string labels (e.g. crypto, vuln-sink, analyzed, needs-review) visible in Ghidra's Function Tags window. Use them to classify functions and track analysis progress.

Data-Type Authoring

| Command | Description | Output shape | |---------|-------------|--------------| | ghidra-rpc create-struct <binary> <name> TYPE FIELD [TYPE FIELD ...] [--if-not-exists\|--or-replace] | Create a named struct; --if-not-exists is idempotent, --or-replace rebuilds it | {name, path, size, fields:[...], already_existed} | | ghidra-rpc create-struct <binary> <name> --field OFFSET TYPE NAME [--field ...] | Create a struct laying fields at explicit byte offsets (OFFSET is decimal or 0x hex); gaps between fields are auto-padded with undefined bytes (no manual pad fields). Overlaps are rejected. | {name, path, size, fields:[...], already_existed} | | ghidra-rpc create-union <binary> <name> TYPE FIELD [TYPE FIELD ...] [--if-not-exists\|--or-replace] | Create a named union; all fields share offset 0 | {name, path, size, fields:[...], already_existed} | | ghidra-rpc modify-struct <binary> <struct_name> --field-offset N\|--field-name NAME [--new-type TYPE] [--new-name NAME] | Retype, rename, or re-comment a struct field | {name, path, size, fields:[...], modified_field:{...}} | | ghidra-rpc clear-data-range <binary> <start> <end> | Reset inclusive byte range to undefined | {start, end, cleared} | | ghidra-rpc apply-data-type-range <binary> <start> <end> <type> [--clear] | Stamp a type repeatedly across an inclusive range; --clear clears the region atomically before stamping | {start, end, data_type, type_size, applied_count, cleared} | | ghidra-rpc list-labels <binary> <address> [--end ADDR] [--limit N] | List symbols at an address or in a range, including data type at each location | {labels:[{address,name,type,source,is_primary,data_type,data_length}], count, total} | | ghidra-rpc create-enum <binary> <name> NAME VAL [NAME VAL ...] [--size 1|2|4|8] [--if-not-exists|--or-replace] | Create a named enum in the DTM | {name, path, size, values:[{name,value}], already_existed} | | ghidra-rpc modify-enum <binary> <name> [--add NAME:VAL ...] [--remove NAME ...] | Add/remove individual entries from an existing enum | {name, path, size, values:[{name,value}]} | | ghidra-rpc set-equate <binary> <addr> <equate_name> <value> [--operand-index N] [--enum-path PATH] | Attach a named scalar constant to an instruction operand; auto-links to enum if found | {address, operand_index, equate_name, value, enum_linked, verified} | | ghidra-rpc list-equates <binary> [address] [--limit N] | List all equates in the program or only those applied at an address | {equates:[{name,value[,operand_index,address]}], count, total} | | ghidra-rpc list-data-types <binary> [--category CAT] [--query SUBSTR] [--limit N] | List types in the DTM; category: struct, enum, union, typedef, pointer, array, all | {data_types:[{name,path,category,size}], count, total} |

Version Tracking & Diff

| Command | Description | Output shape | |---------|-------------|--------------| | ghidra-rpc version-track <old> <new> [--changed-only] [--limit N] [--min-similarity F] | Match functions between two versions; find what changed. Results deduplicated per source function. | {matched:[{source_name,destination_name,similarity,confidence,correlator}], unmatched_source, unmatched_destination, summary:{source_functions_total, changed_functions, identical_functions, ...}} | | ghidra-rpc function-diff <bin1> <func1> <bin2> <func2> [--mode decompile\|disassembly] | Unified diff of a function between two versions; normalises auto-generated names (FUN_*, DAT_*, local_*, param_*) to suppress relocation noise | {is_identical, diff, raw_code1, raw_code2, func1_address, func2_address} | | ghidra-rpc match-function <bin1> <func> <bin2> [--threshold F] | Find best match for a function in another binary using BSim + correlators | {source:{name,address}, candidates:[{name,address,similarity,confidence,correlator}], count} | | ghidra-rpc decompile-all <binary> [--limit N] [--offset N] [--timeout SECS] [--socket-timeout SECS] | Bulk decompile all functions; for export and external diff tools | {functions:[{name,address,signature,c_code}], count, total, offset, errors} |

decompile-all's default --socket-timeout (1800s) assumes a bulk sweep can take much longer than any single function's --timeout; raise it further for binaries with tens of thousands of functions (e.g. large DEX/APK programs).

For the full patch-diff workflow see docs/flows/patch-analysis.md.

Error Handling

Error responses look like:

{"id": "...", "ok": false, "error": "FunctionNotFound", "message": "Function 'foo' not found."}

Common errors:

  • DaemonNotRunning: Daemon isn't started. Tell the user to run ghidra-rpc start.
  • ValueError: Bad argument (ambiguous name, invalid address). The message usually tells you what to do - e.g., use a more specific name or an address.
  • RuntimeError: Ghidra-level error. For GUI-only commands in headless mode, the error says so explicitly.

All CLI output is JSON, including usage errors. A malformed invocation (unknown option, invalid --type choice, unknown subcommand, missing required argument) is also reported as {"ok": false, "error": "...", "message": "..."} on stdout, with a nonzero exit code — never a plain-text Click usage string. Scripted callers can always json.loads() stdout unconditionally, without special-casing argument errors.

Write Operations & Persistence

Write operations (rename, create-label, create-function, delete-function, set-comment, batch-rename, batch-set-comment, set-signature, set-data-type, retype-variable, rename-variable, batch-edit-variable, set-calling-convention, set-thunk, set-flow-override, set-processor-context, create-struct, create-union, modify-struct, create-enum, modify-enum, set-equate, set-bookmark, clear-data-range, apply-data-type-range, write-bytes, assemble, tag-function, untag-function, create-namespace) use Ghidra transactions internally. Every write is automatically saved to the project database on disk after the transaction commits, so changes survive daemon restarts and are visible when the project is reopened in the Ghidra GUI.

Every write response includes a verified boolean that confirms whether the change was read back successfully after committing.

You can also save explicitly at any time:

ghidra-rpc save              # saves all loaded programs
ghidra-rpc save <binary>     # saves one program

On clean shutdown (stop or Ctrl+C), all programs are saved automatically.

Patch / Diff Analysis Quick Start

# 1. Load both versions
ghidra-rpc load /path/to/binary_v1 -p project.gpr
ghidra-rpc load /path/to/binary_v2 -p project.gpr

# 2. Find changed functions (summary counts all changes; matched list is filtered)
ghidra-rpc version-track binary_v1 binary_v2 --changed-only

# 3. Diff a specific changed function
ghidra-rpc function-diff binary_v1 <func> binary_v2 <func>

Full workflow — correlator details, unmatched analysis, bulk decompile: read docs/flows/patch-analysis.md.

Android APK / DEX Analysis

Ghidra has built-in APK and DEX support (Dalvik processor + FileFormats loaders) — no extension install needed. load a .dex or .apk directly and the usual commands (decompile, functions, strings, xrefs-*, …) work on the Dalvik program, which decompiles to Java-like pseudocode. Note the multi-dex caveat: load app.apk imports only the primary classes.dex.

For the full workflow — multi-dex extraction, class-qualified :: symbols and ambiguous methods, the DEX string-vs-xrefs-to gotcha, minified apps, and manifest/.so extraction — read docs/flows/android-apk.md.

Typical Workflow

  1. User starts daemon: cd <skill-dir> && uv run ghidra-rpc start --project /path/to/project.gpr --headless For automation / non-blocking start: uv run ghidra-rpc start --project /path/to/project.gpr --headless --detach
  2. Load binary: uv run ghidra-rpc load /path/to/binary -p /path/to/project.gpr Response includes short_name (usable alias in all subsequent commands). (add --no-analyze to skip analysis, --analysis-timeout SECS to cap it)
  3. Get overview: metadata, functions, imports, exports
  4. Investigate: decompile interesting functions, xrefs-to to trace callers
  5. Annotate: rename-function, set-comment to document findings
  6. Search: strings for hardcoded values, symbols for specific patterns

FAQ / Common Gotchas

create-label properly replaces DEFAULT (auto-analysis) symbols: create-label creates a USER_DEFINED label and sets it as the primary symbol at the address. If a DEFAULT symbol (e.g. DAT_00418138) already exists, it is demoted to secondary. The decompiler will then use the new name. rename-symbol renames in place but requires an existing symbol; create-label is preferred for annotating addresses regardless of existing symbols.

Annotating an address with no existing symbol (rename-symbol fails with "No symbol found"): rename-symbol requires a symbol to already exist at the address. Auto-analysis only places DAT_ symbols where it detects data; inside arrays or dense struct regions many addresses have no symbol. Use create-label instead - it creates a new USER_DEFINED label if none exists, or renames the existing one:

ghidra-rpc create-label <binary> 0x0040e4ac topPtrErrorMsg

You can also add --create to rename-symbol for the same upsert behaviour.

Defining and applying a struct across a memory region: Full workflow to stamp a struct across a repeated data table:

# 1. Create the struct type in the DTM (--if-not-exists makes the call idempotent)
ghidra-rpc create-struct binary ErrorEntry int errorNumber "char *" ptrErrorMsg --if-not-exists

# 2. Stamp the struct across the range with --clear to atomically clear + apply
#    (23 × 8-byte ErrorEntry structs, both endpoints inclusive)
ghidra-rpc apply-data-type-range binary 0x0040e4a8 0x0040e55f ErrorEntry --clear

After this, each address 0x0040e4a8, 0x0040e4b0, ... has type ErrorEntry. Without --clear, apply-data-type-range skips positions where existing data conflicts and reports them in the errors list. Use clear-data-range first if you need finer control over which region is cleared.

Discovering what labels exist at an address or range:

# Single address
ghidra-rpc list-labels binary 0x0040e4a8

# Range
ghidra-rpc list-labels binary 0x0040e4a8 --end 0x0040e55f --limit 50

Using the binary name after load: load returns both a full key (binary) with a hash suffix and a short_name (the original filename). Use short_name in all subsequent commands - it works because commands match binary names by substring:

# Load response: {"binary": "/WinHelloCPP.exe-85cbcc", "short_name": "WinHelloCPP.exe", ...}
ghidra-rpc decompile WinHelloCPP.exe main        # short_name works
ghidra-rpc decompile WinHelloCPP main            # even without extension, if unambiguous

If two binaries share the same stem, use the full key to disambiguate.

Trailing semicolons in signatures: Ghidra's parser rejects C prototypes that end with ;. ghidra-rpc strips trailing semicolons automatically, so you can paste verbatim.

list-binaries is empty but I loaded a binary: In GUI mode, the binary must be open in CodeBrowser. Use list-project-programs to see what's stored in the project repo, then open it in CodeBrowser's Project window.

Daemon log file: When started in the background (start --detach or restart), logs are written to /tmp/ghidra-rpc-<hash>.log (same directory and stem as the Unix socket). If the daemon fails to start or become responsive, check that file first:

tail -50 /tmp/ghidra-rpc-*.log

The timeout error message always prints the exact log path.

GHIDRA_INSTALL_DIR not found after backgrounding: Environment variables may be lost when daemonising (nohup, cron, systemd units, etc.). Fix options, in order of preference:

  1. Pass --ghidra-install-dir /path/to/ghidra to start or restart - the value is persisted in the session file and forwarded to every subsequent restart automatically.
  2. Export GHIDRA_INSTALL_DIR in your shell profile (~/.bashrc, ~/.zshrc) so it survives login sessions.
  3. Use start --detach --headless from the same terminal where the env var is set.

Session file location: By default the session file (.ghidra-rpc-<hash>.json) is written alongside the .gpr project file, keeping all state self-contained. To use a custom directory (e.g. a shared location or a RAM-disk), set GHIDRA_RPC_STATE_DIR:

export GHIDRA_RPC_STATE_DIR=/tmp/ghidra-sessions

Changes not visible in the Ghidra GUI: Make sure you open the project copy (e.g. basic_code-501243 in the Project window), not the raw binary via File → Open. Also ensure you are not running two separate Ghidra instances on the same project (causes lock conflicts). Changes made by ghidra-rpc are auto-saved to disk and will be visible the next time the program is opened in the GUI.

restart without a prior session: Pass --headless so restart can create a fresh headless session instead of requiring a prior start.

Decompiler timeout on certain functions: Some functions trigger expensive decompiler analysis paths and time out even with simple assembly. Use decompile --timeout 300 (or higher) for stubborn functions. The default timeout is 120 s (increased from 60 s to handle large firmware functions). The retype-variable and rename-variable commands also accept --timeout since it triggers an internal decompilation pass.

decompile returns bad-instruction warnings — use pcode --high as fallback: When decompile produces output like "Bad instruction data" or fails to decode a function body (common on ARM Thumb code in regions that auto-analysis mis-classified as data), try pcode --high instead:

ghidra-rpc pcode binary 0x03288102 --high

The P-code engine re-decodes bytes from the function object's context rather than from the listing-level disassembler, so it often succeeds where decompile fails. High P-code also reveals all CALL / CALLIND / BRANCHIND targets and data flow, making it useful for tracing arguments even without valid decompilation. Fix the root cause with set-processor-context (see below).

disassemble starts from the wrong address (B2 warning): If disassemble is called at an address with no instruction (e.g. an address that is mid-instruction or undefined), the response now includes a warning field explaining that disassembly started from the next available instruction. Check for warning in the JSON response when the listing looks wrong.

ARM Thumb mis-classification — set-processor-context is the fix (F1/F5): When ARM auto-analysis classifies Thumb code as data, the listing disassembler decodes it as 32-bit ARM (TMode=0) and produces garbage. The full recovery workflow:

# 1. Diagnose: check TMode at the affected address
ghidra-rpc get-processor-context binary 0x03288100 --register TMode
# Expected: {"TMode": 0}  ← this is the problem

# 2. Clear the bad data/code classification
ghidra-rpc clear-data-range binary 0x03288100 0x032883ff

# 3. Set Thumb mode for the range
ghidra-rpc set-processor-context binary 0x03288100 TMode 1 --end 0x032883ff

# 4. Verify disassembly now shows Thumb-2 instructions
ghidra-rpc disassemble binary 0x03288100 --count 20

# 5. Re-create the function
ghidra-rpc create-function binary 0x03288100

If create-function fails because a bad stub still exists at the address, use delete-function first to remove it.

Bad stubs blocking re-creation — use delete-function (F2): If create-function 0x03288103 (wrong parity) left a stub, and now create-function 0x03288102 (correct address) fails with "already exists at overlapping address", delete the bad stub first:

ghidra-rpc delete-function binary 0x03288103
ghidra-rpc create-function  binary 0x03288102

Batch rename / comment 40+ functions without round-trip overhead (F7): Use batch-rename and batch-set-comment to apply many annotations in a single server round-trip and Ghidra transaction:

# Rename functions from a JSON file
ghidra-rpc batch-rename binary --json-file renames.json

# Or inline
ghidra-rpc batch-rename binary --json \
  '[{"target":"sub_401234","new_name":"init_uart"},{"target":"0x400200","new_name":"isr_handler"}]'

# Label-mode (symbol/label rename by address)
ghidra-rpc batch-rename binary --mode label --json \
  '[{"address":"0x0333afcc","new_name":"g_debugStr"}]'

# Batch comments
ghidra-rpc batch-set-comment binary --json \
  '[{"address":"0x03288102","comment":"Thumb ISR entry","comment_type":"plate"}]'

All items that succeed are committed in one transaction; failed items are reported per-item in results[].ok=false and do not roll back the successes.

Concurrent write operations are serialised: The daemon serialises all command handler invocations internally to prevent Ghidra transaction conflicts. You do not need to serialize write commands externally — parallel writes are queued automatically. This is slightly slower than concurrent execution but eliminates silent partial failures.

Reporting Bugs and Missing Features

While using ghidra-rpc you may encounter bugs, errors, or missing capabilities. Please report them - real-world usage drives continuous improvement.

At the end of any analysis session where you hit a problem, save a brief report:

cat > /tmp/ghidra-rpc-issues-$(date +%Y%m%d-%H%M%S).md << 'EOF'
## ghidra-rpc Issue Report

**Date:** $(date)
**Binary:** <name of binary analysed>

### Bugs encountered
<!-- Describe: command used, error message / unexpected output, expected behaviour -->

### Missing features
<!-- Describe what you needed but couldn't do with the current command set -->

### Suggestions
<!-- Any UX improvements or new commands that would have helped -->
EOF

Then inform the user:

"I saved a ghidra-rpc issue report to /tmp/ghidra-rpc-issues-<timestamp>.md. If you'd like to submit it, run: cat /tmp/ghidra-rpc-issues-*.md"

The report is written to /tmp so it stays local to the machine and is lost on reboot - it is never sent anywhere automatically.

Further Documentation

For detailed guidance on specific workflows:

  • Installation & setup: read docs/install.md
  • Quick start tutorial: read docs/quickstart.md
  • Troubleshooting: read docs/troubleshooting.md
  • Binary audit workflow: read docs/flows/binary-audit.md
  • Vulnerability research: read docs/flows/vulnerability-research.md
  • Patch/diff analysis: read docs/flows/patch-analysis.md
  • Android APK / DEX analysis: read docs/flows/android-apk.md
Related skills