open_binary and the headless bridge
open_binary binds one target to one provider without a Ghidra import, and the first Ghidra-backed call pays for a headless import, auto-analysis, and a private socket.
Your agent opened ledger-arm64-O1 over MCP in under two seconds and then waited several seconds for list_procedures. Nothing was wrong. The open call did not import the file into Ghidra. The first question did, and every question after it came back in milliseconds.
When you finish this section, you can read the server log of a session and name the process that the first query started. You can also predict which calls will be slow.
open_binary
"method": "tools/call",
"params": {
"name": "open_binary",
"arguments": {
"path": "$CAPTURE/fixtures/ledger-arm64-O1",
"provider_id": "ghidra"
}
}
…
"structuredContent": {
"result": {
"path": "$CAPTURE/fixtures/ledger-arm64-O1",
"format": "mach-o",
"kind": "executable",
"sha256": "01b5d08b…",
"architecture": "arm64"
}
}The handler calls session.open with the path and the provider id, then returns five fields: path, format, kind, sha256, and architecture. To fill them rea reads the magic bytes: 7f 45 4c 46 for ELF, MZ for DOS and PE, and four Mach-O values. It hashes the whole file in 64 KiB chunks. A file with no header needs an explicit format. With format: "raw-binary" you also give architecture and entry_point, and the result adds entry_point. rea refuses a WebAssembly module here and sends it to inspect_wasm_artifact. For every other target, rea selects the provider you asked for, and binary_session records that choice.
The selection waits for the provider's readiness checks. On the capture Mac, the server log shows about 1.6 s for the first open_binary and about 0.4 s for the second. The design docs allow the wait: a readiness check can wait for the commands that it owns. Release 6.5.0 answered the same calls in 36 ms and 1 ms. Neither version imports the target at this step.
"open": true,
…
"analysis_provider_binding": {
"provider": {
"id": "ghidra",
"name": "Ghidra",
"version": "12.1.4"
},
"selection_source": "request",
"analysis_profile": {
…
"digest": "3fc72c9b…"
}
},
…
"server_identity": {
"package": {
"name": "rea-agents",
"version": "6.7.0",
…
},
…
"sdk": {
"server": "2.3.1",
…
},
"negotiated_protocol_version": "2025-06-18",
…selection_source: "request" says the agent chose Ghidra. The profile digest is the one every Ghidra record of this target carries. The docs state the limit of the open call in one sentence:
The first Ghidra-backed call
03 tools/call open_binary ok -> 10-mcp-03-open-binary-O1.json (…)
04 tools/call binary_session ok -> 10-mcp-04-binary-session.json (…)
05 tools/call list_procedures ok -> 10-mcp-05-list-procedures.json (…)
06 tools/call procedure_assembly ok -> 10-mcp-06-procedure-assembly-grade.json (…)
07 tools/call xrefs ok -> 10-mcp-07-xrefs-grade.json (…)
08 tools/call list_strings ok -> 10-mcp-08-list-strings.json (…){"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"open_binary","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"list_procedures","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"procedure_assembly","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"xrefs","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}
{"level":30,"time":…,"application":"rea","mode":"mcp","layer":"server","tool":"list_strings","durationMs":…,"status":"ok","msg":"MCP tool execution completed"}The capture kit records the pattern in its README. The first Ghidra-backed call after open_binary takes 5 to 7 s. Every later call on the same target takes under 100 ms. The difference is the Ghidra start. GhidraClient.callTool awaits start() before it queues any request, so the first call pays for the whole sequence in #startWithin:
- Create a private runtime root named
rea-ghidra-and a random suffix under the session root. - Allocate the endpoint, a Unix socket on macOS and Linux.
- Copy the target into the root as
target-01b5d08b08fb.bin, hashing while copying, and refuse a digest mismatch. - Mint a 32-byte random token and a run id.
- Write
session.jsonwithtransport,endpoint_path,token,run_id,target_sha256,provider_version, andprofile_digest. - Start Java and wait for the bridge to accept a connection.
Headless import: a Ghidra run with no window that imports one file into a project and runs the default analyzers. On macOS and Linux, rea does not run the analyzeHeadless shell script. It asks Ghidra's LaunchSupport.jar for the environment and the JVM arguments, then runs java itself. The heap is -Xmx2G unless GHIDRA_HEADLESS_MAXMEM or GHIDRA_MAXMEM is set. The command adds -Djava.awt.headless=true and names the class ghidra.Ghidra with ghidra.app.util.headless.AnalyzeHeadless.
The docs give the reason for the direct start. On Apple platforms the shell wrappers hide their environment from ownership checks. With the wrappers in place, rea could not cancel the process group safely during startup.
The headless arguments come in one fixed order:
- the project root and the project name
rea-project -importwith the copied target-readOnlyand-deleteProject-logand-scriptloginto the runtime root, and-scriptPathat the bridge directory-postScript ReaGhidraNoReturnFix.java-postScript ReaGhidraBridge.javawith the path ofsession.json, last
Some targets and settings need more arguments. A raw binary, a DOS file, or a language override adds a loader and a processor. Seeds and disabled analyzers add pre-scripts. The fixtures of this manual need none of them.
The bridge on the Java side
ReaGhidraBridge.java is a Ghidra script that runs after the import and the analyzers. Its run() reads session.json and deletes it, then requires an imported program. It checks that the Ghidra version equals provider_version and compares the program's own SHA-256 with target_sha256 in constant time. It then ends the script transaction, initializes the decompiler, and serves. On a Unix socket it binds the path, sets the mode to rw-------, and accepts one client.
Each request is one JSON line, { id, token, method, params }. Each reply is { id, ok: true, result } or { id, ok: false, error: { code, message } }, and rea parses it with a strict schema. list_procedures was request number 2 on that socket, after the ping of the handshake, and it took the Java side a few milliseconds. The seconds before it were the import and the analyzers.
Deadlines and the close
rea allows 330,000 ms for the start, set by REA_GHIDRA_STARTUP_TIMEOUT_MS. On the capture Mac the start took 5 to 7 s, and the client limit is the one to watch. The pinned client SDK 2.3.1 waits 60,000 ms by default, so it can cancel before the REA startup deadline.
The docs record about 64 s on a Linux fixture and about 163 s on Windows. Set the client deadline from your own first run on your own machine.
close_binary takes a fraction of a second in the log. The client sends shutdown to the bridge with a 10 s limit, the bridge disposes the decompiler, and -deleteProject removes the project. The next open_binary on ledger-arm64-O2 took about 0.4 s. Its first analyze_function again took seconds, because every new provider session imports the target again into a new temporary project.
rea (rea-agents) with the Ghidra provider rea-agents 6.7.0, Ghidra 12.1.4 · 27dc42d · 2026-10-18 · Edition 2026.10