4.1

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

the open_binary exchangejsonFull log
"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 request and the result. The text copy of the result is cut by the capture kit.

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.

the binding that binary_session reportsjsonFull log
"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",
  …
Cut to the provider binding and the server identity. The capabilities, candidates, and tool availability lists are cut.

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

six calls from the session indextextFull log
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  (…)
Lines 03 to 08 of the index. The time in parentheses is measured by the capture client around each call, changes on every run, and is cut.
the same calls in the server logjsonlFull log
{"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 first five lines of the stderr of rea mcp. durationMs is measured inside the server. The time and durationMs values change on every run and are cut.

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:

  1. Create a private runtime root named rea-ghidra- and a random suffix under the session root.
  2. Allocate the endpoint, a Unix socket on macOS and Linux.
  3. Copy the target into the root as target-01b5d08b08fb.bin, hashing while copying, and refuse a digest mismatch.
  4. Mint a 32-byte random token and a run id.
  5. Write session.json with transport, endpoint_path, token, run_id, target_sha256, provider_version, and profile_digest.
  6. 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
  • -import with the copied target
  • -readOnly and -deleteProject
  • -log and -scriptlog into the runtime root, and -scriptPath at the bridge directory
  • -postScript ReaGhidraNoReturnFix.java
  • -postScript ReaGhidraBridge.java with the path of session.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.

Fig. 4.1one open and three questions over MCPsequence
One open and three questions over MCP Four lifelines: the agent, the rea MCP server, the Ghidra client inside rea, and the Java bridge inside analyzeHeadless. open_binary returns in about 1.6 seconds after the provider readiness checks, with no headless import yet. list_procedures makes the client start: a private runtime root, a Unix socket, a hashed copy of the target, then java with AnalyzeHeadless, -import, -readOnly and -deleteProject, then a ping handshake, then one JSON request line and one reply. The Evidence returns after 5 to 7 seconds. procedure_assembly on the same socket takes under 100 ms. AGENT REA MCP GHIDRA CLIENT JAVA BRIDGE 1 open_binary about 1.6 s of readiness checks, no import yet 2 mach-o executable arm64 3 list_procedures 4 start() rea-ghidra-* root, bridge.sock 5 AnalyzeHeadless -import -readOnly -deleteProject -postScript 6 ping ok analysis_complete true, token checked 7 {id, token, method, params} one JSON line on the socket 8 {id, ok true, result} 13 functions, 4 external 9 Evidence ev_3c6350b2 5 to 7 s, import plus auto-analysis 10 procedure_assembly _grade under 100 ms on the same socket
The open call returns after the readiness checks with no import, the first question waits seconds for the headless import, and the next takes under 100 ms. Time runs down. Solid arrows are calls, dashed arrows are replies. The violet lanes are rea's Ghidra client and the Java bridge inside analyzeHeadless. The times are the ranges that the kit notes record.

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.

Fig. 4.2the private runtime root of one sessiontree
The private runtime root of one Ghidra session One directory named rea-ghidra- plus a random suffix, created by GhidraClient start() and deleted after close_binary. It holds six Ghidra directories, the hashed copy of the target named target-01b5d08b08fb.bin, the session.json descriptor with seven keys, the bridge.sock Unix socket, ownership.json, and the ghidra.log and script.log files. SESSION ROOT · ONE DIRECTORY PER START() rea-ghidra-<random> created by GhidraClient start(), deleted after close_binary project/ home/ tmp/ cache/ config/ data/ rea-project imported with -readOnly -deleteProject · private HOME, tmpdir, caches target-01b5d08b08fb.bin copy of ledger-arm64-O1, hashed while copied · the Ghidra document session.json transport, endpoint_path, token, run_id, target_sha256, provider_version, profile_digest bridge.sock Unix socket bound by the bridge, mode rw-------, one client accepted ownership.json process ownership record for verified cancellation ghidra.log · script.log the -log and -scriptlog files of analyzeHeadless
One session owns one directory with the project, the hashed target copy, the descriptor, the socket, and the logs, and close_binary deletes it. Read top to bottom. Each entry names the file or directory and what writes it.

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