ghidra-cli 0.1.10

Rust CLI to run Ghidra headless for reverse engineering with Claude Code and other agents
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
# Plan: Replace Python Bridge with Java GhidraScript Socket Server

## Overview

Replace the current three-layer architecture (Rust CLI → Rust Daemon → Python bridge inside Ghidra JVM) with a two-layer architecture (Rust CLI → Java GhidraScript socket server inside Ghidra JVM). The Java GhidraScript runs via `analyzeHeadless -postScript`, starts a TCP socket server that keeps the JVM alive, and handles all 49 commands directly via the Ghidra Java API. The Rust side collapses: the persistent daemon process is eliminated, replaced by a thin "launcher" that starts `analyzeHeadless` if not running, writes a port+PID file, and then the CLI connects directly to the Java bridge's TCP socket.

**Chosen approach**: Single `.java` GhidraScript file (auto-compiled by `analyzeHeadless`), TCP localhost with dynamic port + port/PID file, collapsed single-layer IPC (no Rust daemon process), clean break migration.

## Planning Context

### Decision Log

| Decision | Reasoning Chain |
|----------|----------------|
| GhidraScript .java over Extension .zip | Extension requires Gradle build infra + versioned distribution packaging → adds build complexity for no runtime benefit → GhidraScript is auto-compiled by `analyzeHeadless` at startup → matches current Python pattern where scripts are written to disk and referenced via `-postScript` → simpler maintenance |
| Collapse to single IPC layer | Current flow: CLI → UDS → Daemon → TCP:18700 → Python bridge. Daemon exists to: (a) manage bridge process lifecycle, (b) provide lock files, (c) lazy-start bridge. With Java bridge: (a) `analyzeHeadless` IS the daemon process, (b) lock files move to port/PID file, (c) CLI launcher starts `analyzeHeadless` if port file missing → Rust daemon process becomes pure passthrough with no logic → eliminate it |
| TCP localhost + port/PID file over Unix domain socket | Java 16+ supports `UnixDomainSocketAddress` but: Ghidra bundles its own JDK and not all Ghidra distributions include JDK 16+ → TCP `ServerSocket(0)` on localhost is universally supported → dynamic port avoids fixed-port conflicts → port written to `~/.local/share/ghidra-cli/bridge-{hash}.port` → PID written to `bridge-{hash}.pid` for killing hung bridges → same security model (localhost only) |
| Single .java file with inner classes | Multiple .java files risk classpath issues with `analyzeHeadless` script compilation → single file with static inner classes keeps all handler code together → file is ~2000-3000 lines which is manageable → `analyzeHeadless` compiles single script files reliably |
| Clean break over dual-mode | Maintaining both Python and Java bridges doubles test surface → Python bridge is the source of complexity we're eliminating → feature branch with all E2E tests passing before merge provides safety → CHANGELOG documents breaking change |
| Sequential command processing (no threading) | Ghidra headless is single-threaded for program access → concurrent writes to Program cause data corruption → Python bridge was already single-connection sequential → Java bridge processes one command at a time on accept loop → analysis queue handles concurrent import requests by queuing |
| Embed .java in binary via include_str! | Current pattern: 13 Python scripts embedded via `include_str!()` in `bridge.rs`, written to `~/.config/ghidra-cli/scripts/` at runtime → same pattern for single Java file → no separate installation step beyond `ghidra-cli` binary itself |
| Port file as liveness indicator | fslock-based locking was complex and had edge cases → port file + PID file is simpler: if port file exists AND PID is alive AND TCP connect succeeds → bridge is running → if any check fails, clean up stale files and start fresh |
| 300s read timeout for analysis ops | Current bridge uses 300s read timeout (bridge.rs:240) → analysis operations (especially initial auto-analysis) can take minutes → keep same timeout → individual query commands complete in <1s but timeout provides safety net |

### Rejected Alternatives

| Alternative | Why Rejected |
|-------------|-------------|
| Ghidra Extension (.zip) packaging | Requires Gradle build infrastructure, module.manifest, extension.properties → adds build system complexity for distribution → GhidraScript auto-compilation achieves same result with zero build tooling |
| Unix domain sockets in Java | Requires Java 16+ `UnixDomainSocketAddress` → Ghidra may bundle older JDK → TCP localhost is universally supported and equally secure for loopback-only binding |
| Keep Rust daemon as process manager | With Java bridge handling its own lifecycle, Rust daemon becomes a passthrough that adds latency and complexity → thin launcher function in CLI achieves same lifecycle management |
| ghidra_bridge (existing library) | External dependency on Jython RPC library → still has Python layer → less control over protocol and error handling → custom Java bridge is simpler and eliminates Python entirely |
| PyGhidra JPype embedding | Requires Python + JPype installation → adds Python dependency back → ghidra-cli's goal is minimal deps (just Ghidra + Java) |
| Dual mode with --bridge flag | Doubles test matrix → Python bridge is what we're removing → clean break is simpler to reason about |
| Fixed port per project (hash-based) | Port collisions between projects with similar hashes → dynamic port with port file is collision-free → slight complexity of reading port file is worth guaranteed uniqueness |

### Constraints & Assumptions

- **Ghidra 10.0+**: Minimum supported version (same as current). `AutoImporter`, `DecompInterface`, `FunctionManager` APIs stable across 10.x-12.x.
- **Java 17+**: Required by Ghidra itself. `ServerSocket`, `Gson` (bundled by Ghidra) available.
- **analyzeHeadless**: Compiles `.java` GhidraScript files automatically. Script receives `currentProgram`, `state`, `monitor` as instance fields.
- **Gson bundled**: Ghidra includes `com.google.gson` in classpath. No external JSON library needed.
- **Single-threaded program access**: Ghidra `Program` objects are not thread-safe for writes. All command handling is sequential (same as current Python bridge).
- **Existing E2E tests validate migration**: If Java bridge produces identical JSON responses to Python bridge, all existing E2E test files pass unchanged. The Rust-side IPC protocol changes (direct TCP instead of UDS→TCP) but the test harness connects via CLI binary which handles this transparently.
- **`GhidraScript.getScriptArgs()`**: Returns command-line arguments passed after script name. Used to pass port file path.

### Known Risks

| Risk | Mitigation | Anchor |
|------|-----------|--------|
| Ghidra API differences across versions (10.x vs 11.x vs 12.x) | `AutoImporter.importByUsingBestGuess()` signature changed in 12.x → Java bridge uses try/catch with reflection fallback for import, same pattern as Python bridge | `src/ghidra/scripts/bridge.py:880-888` uses 6-arg variant |
| `analyzeHeadless` compiles .java with errors | Java compilation errors surface in stderr → bridge.rs already captures stderr for diagnostics → keep this pattern → add specific "Java compilation failed" error detection |
| TCP port exhaustion on systems with many projects | Dynamic port from `ServerSocket(0)` draws from ephemeral range (49152-65535) → 16K+ ports available → stale port files cleaned on next launch attempt |
| Ghidra JVM heap exhaustion with large binaries | Same risk as current Python bridge → not a new risk → Ghidra's default heap settings apply |
| `analyzeHeadless` calls `System.exit()` after script | GhidraScript `run()` method blocks in accept loop → `analyzeHeadless` waits for script completion → `System.exit()` only called after `run()` returns (on shutdown command) |

## Invisible Knowledge

### Architecture

```
BEFORE (3 layers):
  ghidra-cli (Rust) --[UDS]--> daemon (Rust/tokio) --[TCP:18700]--> bridge.py (Python in Ghidra JVM)

AFTER (2 layers):
  ghidra-cli (Rust) --[TCP:dynamic]--> GhidraCliBridge.java (Java in Ghidra JVM via analyzeHeadless)

Lifecycle:
  1. CLI reads port file (~/.local/share/ghidra-cli/bridge-{hash}.port)
  2. If missing or stale: launch analyzeHeadless + GhidraCliBridge.java
  3. GhidraCliBridge writes port file, enters accept loop
  4. CLI connects via TCP, sends JSON command, reads JSON response
  5. On shutdown: GhidraCliBridge deletes port/PID files, run() returns, analyzeHeadless exits
```

### Data Flow

```
CLI Command (e.g., `ghidra functions --limit 10`)
  │
  ├─ Parse CLI args → Command enum
  ├─ Read port file → TCP port (or launch bridge if missing)
  ├─ Connect TCP localhost:port
  ├─ Send: {"command":"list_functions","args":{"limit":10}}\n
  ├─ Recv: {"status":"success","data":{"functions":[...],"count":N}}\n
  ├─ Format output (table/json/csv)
  └─ Display

Bridge Lifecycle:
  analyzeHeadless project_dir project_name -import binary -postScript GhidraCliBridge.java /path/to/portfile
    │
    ├─ GhidraCliBridge.run() called by analyzeHeadless
    ├─ Opens ServerSocket(0), writes port to file, writes PID to file
    ├─ Prints ready signal to stdout
    ├─ Accept loop: read command JSON → dispatch to handler → write response JSON
    ├─ "shutdown" command → break accept loop
    ├─ Cleanup: delete port/PID files
    └─ run() returns → analyzeHeadless exits normally
```

### Why This Structure

- **Single .java file**: `analyzeHeadless` compiles GhidraScript files individually. Multi-file compilation requires extension packaging. Single file with inner classes avoids this while keeping code organized.
- **Port file + PID file**: Replaces the fslock + info file + UDS socket triple. Simpler liveness detection: read PID, check `kill -0`, verify TCP connect.
- **No Rust daemon process**: The Java bridge IS the daemon. `analyzeHeadless` is the process manager. Rust CLI is a thin client that launches the bridge if needed.

### Invariants

1. **One bridge per project**: Port file path includes project hash → only one bridge instance per project directory.
2. **Sequential command processing**: Single accept loop, one connection at a time. Ghidra `Program` objects are not thread-safe for mutation.
3. **Import queuing**: If a program is being analyzed, import commands queue the new binary and return immediately with "queued" status. Analysis proceeds in order.
4. **Port file lifecycle**: Created AFTER ServerSocket.bind() succeeds, deleted BEFORE run() returns. Stale files detected via PID liveness check.
5. **Protocol compatibility**: JSON wire format `{"command":"...","args":{...}}` → `{"status":"success|error","data":{...},"message":"..."}` is identical to current Python bridge.

### Tradeoffs

- **Single large .java file vs extension with multiple classes**: Chose single file for zero-build-tooling simplicity at cost of a large source file (~2500 lines).
- **TCP vs UDS**: Chose TCP for Java cross-platform compatibility at cost of port file management overhead.
- **Clean break vs gradual migration**: Chose clean break for code simplicity at cost of no fallback path.

## Milestones

### Milestone 1: Java GhidraScript Bridge — Core Server + Basic Commands

**Files**:
- `src/ghidra/scripts/GhidraCliBridge.java` (NEW)

**Flags**: `needs-rationale`, `complex-algorithm`

**Requirements**:
- GhidraScript that extends `ghidra.app.script.GhidraScript`
- `run()` method: parse script args for port file path, bind `ServerSocket(0)` on localhost, write port + PID files, print ready signal to stdout, enter accept loop
- Accept loop: read newline-delimited JSON commands from socket, dispatch to handler, write JSON response, handle client disconnect gracefully
- Shutdown command: break accept loop, delete port/PID files, return from `run()`
- Core command handlers (direct ports from bridge.py):
  - `ping` — health check
  - `shutdown` — stop server
  - `program_info` — program metadata (name, format, language, image base, address range)
  - `list_functions` — enumerate functions with limit/filter support
  - `decompile` — decompile function at address or by name
  - `list_strings` — enumerate string data
  - `list_imports` — enumerate external symbols
  - `list_exports` — enumerate export entry points
  - `memory_map` — enumerate memory blocks with permissions
  - `xrefs_to` / `xrefs_from` — cross-reference queries
  - `import` — import binary via `AutoImporter.importByUsingBestGuess()`
  - `analyze` — trigger `AutoAnalysisManager` re-analysis
  - `list_programs` — enumerate project domain files
  - `open_program` — switch active program
  - `program_close` — close current program
  - `program_delete` — delete program from project
  - `program_export` — export program info as JSON
- Address resolution helper: parse hex address or resolve function name
- JSON serialization via Gson (bundled by Ghidra)
- Error handling: all handlers return `{"status":"error","message":"..."}` on failure
- `currentProgram` null checks on all program-dependent handlers

**Acceptance Criteria**:
- `analyzeHeadless /tmp/test TestProject -import /bin/ls -scriptPath . -postScript GhidraCliBridge.java /tmp/test.port` starts server
- Port file contains valid integer port
- PID file contains process PID
- `echo '{"command":"ping"}' | nc localhost $(cat /tmp/test.port)` returns `{"status":"success","data":{"message":"pong"}}`
- `echo '{"command":"list_functions","args":{"limit":5}}' | nc ...` returns JSON with functions array
- `echo '{"command":"shutdown"}' | nc ...` causes clean exit, port+PID files deleted

**Tests**:
- **Test files**: Manual validation with `analyzeHeadless` + `nc`/`curl` during development; formal E2E tests run in Milestone 4
- **Test type**: Manual integration
- **Backing**: Bootstrap — Java bridge must work standalone before Rust integration
- **Scenarios**:
  - Normal: start server, send commands, get correct JSON responses
  - Edge: send malformed JSON, send unknown command, send command with no program loaded
  - Error: binary not found on import, function not found on decompile

**Code Intent**:

- New file `src/ghidra/scripts/GhidraCliBridge.java`: Single GhidraScript class extending `ghidra.app.script.GhidraScript`
- `run()` method: get port file path from `getScriptArgs()[0]`, create `ServerSocket(0, 1, InetAddress.getByName("127.0.0.1"))`, write port to file, write PID to file via `ProcessHandle.current().pid()`, print ready signal `---GHIDRA_CLI_START---` / JSON / `---GHIDRA_CLI_END---` to stdout, enter accept loop
- `handleRequest(String line)` method: parse JSON with Gson, extract "command" and "args", dispatch to handler method, return JSON response string
- `resolveAddress(String addrStr)` helper: try `currentProgram.getAddressFactory().getAddress()`, fall back to function name lookup
- Handler methods: `handlePing()`, `handleProgramInfo()`, `handleListFunctions(JsonObject args)`, `handleDecompile(JsonObject args)`, `handleListStrings(JsonObject args)`, `handleListImports()`, `handleListExports()`, `handleMemoryMap()`, `handleXrefsTo(JsonObject args)`, `handleXrefsFrom(JsonObject args)`, `handleImport(JsonObject args)`, `handleAnalyze(JsonObject args)`, `handleListPrograms()`, `handleOpenProgram(JsonObject args)`, `handleProgramClose()`, `handleProgramDelete(JsonObject args)`, `handleProgramExport(JsonObject args)`
- Each handler follows same pattern as Python equivalent: null-check currentProgram, call Ghidra Java API, build Gson JsonObject response

**Code Changes**: _To be filled by Developer_

---

### Milestone 2: Java Bridge — Extended Command Handlers

**Files**:
- `src/ghidra/scripts/GhidraCliBridge.java` (MODIFY — add remaining handlers)

**Flags**: `conformance`

**Requirements**:
- Port all remaining Python command handlers to Java methods in GhidraCliBridge:
  - **Find**: `find_string`, `find_bytes`, `find_function`, `find_calls`, `find_crypto`, `find_interesting` (from `find.py`)
  - **Symbols**: `symbol_list`, `symbol_get`, `symbol_create`, `symbol_delete`, `symbol_rename` (from `symbols.py`)
  - **Types**: `type_list`, `type_get`, `type_create`, `type_apply` (from `types.py`)
  - **Comments**: `comment_list`, `comment_get`, `comment_set`, `comment_delete` (from `comments.py`)
  - **Graph**: `graph_calls`, `graph_callers`, `graph_callees`, `graph_export` (from `graph.py`)
  - **Diff**: `diff_programs`, `diff_functions` (from `diff.py`)
  - **Patch**: `patch_bytes`, `patch_nop`, `patch_export` (from `patch.py`)
  - **Disasm**: `disasm` (from `disasm.py`)
  - **Stats**: `stats` (from `stats.py`)
  - **Script**: `script_run`, `script_python`, `script_java`, `script_list` (from `script_runner.py`)
  - **Batch**: `batch` (from `batch.py`)
- All handlers produce identical JSON output structure to their Python equivalents
- Transaction management: handlers that modify program state (symbol_create, comment_set, patch_bytes, etc.) must use `currentProgram.startTransaction()` / `endTransaction()`

**Acceptance Criteria**:
- Each handler returns same JSON structure as corresponding Python handler
- Write operations (symbol create, comment set, patch) wrapped in transactions
- `find_bytes` correctly handles hex pattern search across memory blocks
- `graph_callers`/`graph_callees` correctly traverse call graph to specified depth
- `decompile` timeout set to 30 seconds (matching Python: `decompiler.decompileFunction(func, 30, monitor)`)

**Tests**:
- **Test files**: Manual validation during development; formal E2E tests in Milestone 4
- **Test type**: Manual integration
- **Backing**: Behavioral parity with Python bridge
- **Scenarios**:
  - Normal: each handler returns expected data for sample binary
  - Edge: symbol operations on non-existent symbols, patch at invalid address
  - Error: find with empty pattern, graph on function with no calls

**Code Intent**:

- Add command dispatch entries to `handleRequest()` for all new commands
- Find handlers: `handleFindString(args)` — iterate defined data matching pattern; `handleFindBytes(args)` — use `Memory.findBytes()` with hex pattern; `handleFindFunction(args)` — iterate functions matching name pattern; `handleFindCalls(args)` — get xrefs to named function; `handleFindCrypto()` — scan for known crypto constants (AES S-box, SHA constants); `handleFindInteresting()` — heuristic scan for security-relevant functions
- Symbol handlers: use `currentProgram.getSymbolTable()` API — `getSymbols()`, `createLabel()`, `removeSymbolSpecial()`, `getSymbol()`
- Type handlers: use `currentProgram.getDataTypeManager()` — `getAllDataTypes()`, `getDataType()`, `addDataType()`, `apply()` via `DataUtilities.createData()`
- Comment handlers: use `currentProgram.getListing().getCodeUnitAt()` — `getComment()`, `setComment()` with `CodeUnit.EOL_COMMENT` etc.
- Graph handlers: recursive traversal of `function.getCalledFunctions()` / `function.getCallingFunctions()` with depth limit
- Diff handlers: compare two programs' function lists by name/size/signature
- Patch handlers: use `currentProgram.getMemory().setBytes()` within transaction; NOP uses language-specific NOP byte(s)
- Disasm handler: use `currentProgram.getListing().getInstructionAt()` and iterate
- Stats handler: aggregate counts (functions, strings, imports, exports, memory blocks, defined data)
- Script handlers: `script_run` — use `GhidraScriptUtil` to find and run scripts; `script_list` — enumerate script directories
- All write operations wrapped in `int txId = currentProgram.startTransaction("description"); try { ... } finally { currentProgram.endTransaction(txId, true); }`

**Code Changes**: _To be filled by Developer_

---

### Milestone 3: Rust Side — Replace Daemon with Direct Bridge Connection

**Files**:
- `src/ghidra/bridge.rs` (REWRITE — replace Python bridge management with Java bridge management)
- `src/daemon/mod.rs` (REWRITE — eliminate daemon process, replace with launcher logic)
- `src/daemon/handler.rs` (DELETE or REWRITE — direct TCP replaces IPC→bridge delegation)
- `src/daemon/ipc_server.rs` (DELETE — no more UDS IPC server)
- `src/daemon/process.rs` (REWRITE — replace fslock/info file with port/PID file management)
- `src/daemon/state.rs` (DELETE — no daemon state needed)
- `src/daemon/cache.rs` (DELETE or keep if result caching desired)
- `src/daemon/queue.rs` (DELETE — analysis queuing moves to Java side)
- `src/daemon/handlers/*.rs` (DELETE — all handler delegation removed)
- `src/ipc/client.rs` (REWRITE — connect via TCP to Java bridge instead of UDS to daemon)
- `src/ipc/protocol.rs` (MODIFY — simplify to match Java bridge JSON protocol)
- `src/ipc/transport.rs` (SIMPLIFY — TCP only, remove UDS/named pipe abstraction)
- `src/ghidra/bridge.rs` (REWRITE — replace Python bridge management with Java bridge TCP client)
- `src/ghidra/scripts.rs` (MODIFY — embed GhidraCliBridge.java instead of Python scripts)
- `src/lib.rs` (MODIFY — update module structure)
- `src/main.rs` (MODIFY — remove daemon foreground mode, update command routing)
- `src/cli.rs` (MODIFY — remove `daemon start/stop` subcommands, simplify)
- `Cargo.toml` (MODIFY — remove `interprocess`, `fslock` deps; may remove `tokio` if fully sync)

**Flags**: `error-handling`, `needs-rationale`

**Requirements**:
- `bridge.rs` rewrite:
  - `ensure_bridge_running(project_path)`: check port file, verify PID alive, verify TCP connect. If any fail, start new bridge.
  - `start_bridge(project_path, mode)`: spawn `analyzeHeadless` with `-postScript GhidraCliBridge.java`, wait for ready signal on stdout, verify port file created
  - `send_command(port, command, args)`: TCP connect, send JSON, read JSON response, disconnect
  - `kill_bridge(project_path)`: read PID file, send shutdown command (graceful), fall back to kill PID (forced)
  - Port/PID file management: `~/.local/share/ghidra-cli/bridge-{md5_hash}.port`, `bridge-{md5_hash}.pid`
- CLI command flow:
  - `ghidra import <binary>` → ensure_bridge_running → send "import" command
  - `ghidra functions` → ensure_bridge_running → send "list_functions" command
  - `ghidra daemon stop` → read PID/port → send "shutdown" command → verify exit
  - `ghidra daemon status` → check port file + PID + TCP connect → report status
- Remove Python-specific code:
  - Remove `find_headless_script()` pyghidraRun preference logic
  - Remove `install_pyghidra()` from setup
  - Remove all `include_str!("scripts/*.py")` embeds
- Add Java-specific code:
  - `include_str!("scripts/GhidraCliBridge.java")` for embedding
  - Write `.java` file to `~/.config/ghidra-cli/scripts/` on bridge start
  - Always use `analyzeHeadless` (no pyghidraRun)

**Acceptance Criteria**:
- `ghidra import tests/fixtures/sample_binary --project test` starts bridge if needed, imports binary, returns success
- `ghidra functions --project test --program sample_binary` connects to running bridge, returns function list
- `ghidra daemon status` reports bridge running/stopped
- `ghidra daemon stop` gracefully stops the Java bridge
- No `tokio` runtime needed if all I/O is synchronous TCP
- Port file created on bridge start, deleted on bridge stop
- PID file allows `ghidra daemon stop` to kill hung bridges

**Tests**:
- **Test files**: `tests/daemon_tests.rs`, `tests/command_tests.rs` (existing, should pass)
- **Test type**: E2E integration
- **Backing**: Existing test suite validates behavioral parity
- **Scenarios**:
  - Normal: full command lifecycle (import → analyze → query → shutdown)
  - Edge: bridge already running (reuse), bridge crashed (restart), stale port file (cleanup)
  - Error: Ghidra not installed, binary not found, invalid project path

**Code Intent**:

- Rewrite `src/ghidra/bridge.rs`:
  - Remove `GhidraBridge` struct with `Child`, `TcpStream`, `AtomicBool`
  - New functions: `ensure_bridge_running(project_path) -> Result<u16>` (returns port), `start_bridge(project_path, ghidra_dir, mode) -> Result<u16>`, `send_command(port, command, args) -> Result<Value>`, `stop_bridge(project_path) -> Result<()>`
  - `start_bridge()`: write embedded Java script to disk, build `analyzeHeadless` command, spawn process, read stdout for ready signal, return port from port file
  - `send_command()`: `TcpStream::connect(("127.0.0.1", port))`, write JSON line, read JSON line, parse response
  - Port file path: `get_data_dir()?.join(format!("bridge-{}.port", md5_hash(project_path)))`
  - PID file path: same pattern with `.pid` extension
- Rewrite `src/daemon/mod.rs`: remove `DaemonState`, `DaemonConfig`, `run()` async function. Replace with `ensure_bridge(project_path, ghidra_dir)` that calls `bridge::ensure_bridge_running()`
- Rewrite `src/daemon/process.rs`: remove `acquire_daemon_lock()`, `DaemonInfo`. New functions: `read_port_file()`, `write_port_file()`, `read_pid_file()`, `write_pid_file()`, `is_pid_alive()`, `cleanup_stale_files()`
- Delete: `src/daemon/ipc_server.rs`, `src/daemon/state.rs`, `src/daemon/queue.rs`, `src/daemon/handlers/` directory
- Rewrite `src/ipc/client.rs`: remove `DaemonClient` with async reader/writer. New `BridgeClient` with sync `TcpStream`
- Simplify `src/ipc/transport.rs`: remove UDS/named pipe abstractions, keep only TCP helper functions
- Simplify `src/ipc/protocol.rs`: protocol now matches Java bridge JSON format directly: `{"command":"...", "args":{...}}` → `{"status":"...", "data":{...}, "message":"..."}`
- Modify `src/main.rs`: remove `--foreground` daemon mode, update command dispatch to use `bridge::ensure_bridge_running()` + `bridge::send_command()`
- Modify `src/cli.rs`: keep `daemon start/stop/status` as convenience commands but implement via bridge management (not separate process)
- Modify `Cargo.toml`: remove `interprocess`, `fslock` dependencies. Evaluate removing `tokio` if all bridge I/O is synchronous.

**Code Changes**: _To be filled by Developer_

---

### Milestone 4: Setup/Doctor Commands + Python Removal

**Files**:
- `src/ghidra/setup.rs` (MODIFY — remove `install_pyghidra()`, simplify setup)
- `src/main.rs` (MODIFY — update `handle_doctor()`, `handle_setup()`)
- `src/ghidra/scripts/bridge.py` (DELETE)
- `src/ghidra/scripts/find.py` (DELETE)
- `src/ghidra/scripts/symbols.py` (DELETE)
- `src/ghidra/scripts/types.py` (DELETE)
- `src/ghidra/scripts/comments.py` (DELETE)
- `src/ghidra/scripts/graph.py` (DELETE)
- `src/ghidra/scripts/diff.py` (DELETE)
- `src/ghidra/scripts/patch.py` (DELETE)
- `src/ghidra/scripts/disasm.py` (DELETE)
- `src/ghidra/scripts/stats.py` (DELETE)
- `src/ghidra/scripts/script_runner.py` (DELETE)
- `src/ghidra/scripts/batch.py` (DELETE)
- `src/ghidra/scripts/program.py` (DELETE)
- `src/ghidra/scripts.rs` (MODIFY — remove Python script embeds, add Java script embed)

**Flags**: `needs-rationale`

**Requirements**:
- `ghidra setup`:
  1. Check Java 17+ (keep existing `check_java_requirement()`)
  2. Download + extract Ghidra (keep existing `install_ghidra()`)
  3. Remove PyGhidra installation step entirely
  4. Verify `analyzeHeadless` script exists and is executable
  5. Write GhidraCliBridge.java to scripts directory (verify Java compilation by doing a dry-run compile if possible)
- `ghidra doctor`:
  1. Check Java version (keep)
  2. Check Ghidra installation (keep)
  3. Remove PyGhidra check
  4. Add: verify GhidraCliBridge.java can be found/written
  5. Add: verify no stale port/PID files
- Delete all 13 Python scripts from `src/ghidra/scripts/`
- Update `src/ghidra/scripts.rs` to embed only `GhidraCliBridge.java`

**Acceptance Criteria**:
- `ghidra setup` installs Ghidra without any Python/PyGhidra steps
- `ghidra doctor` reports Java, Ghidra, and bridge script status (no Python checks)
- All Python `.py` files removed from source tree
- `cargo build` succeeds with no references to deleted Python files

**Tests**:
- **Test files**: `tests/command_tests.rs` (existing doctor/setup tests)
- **Test type**: E2E integration
- **Backing**: Existing tests validate doctor/setup commands
- **Scenarios**:
  - Normal: setup with valid Ghidra, doctor reports all green
  - Edge: Ghidra not installed (doctor reports error), stale files present (doctor warns)
  - Error: Java not installed, wrong Java version

**Code Intent**:

- Modify `src/ghidra/setup.rs`: delete `install_pyghidra()` function entirely (lines 244-345). Remove all references to Python venv, pip, PyGhidra wheel.
- Modify `src/main.rs` `handle_setup()`: remove PyGhidra installation call. Add step to verify `analyzeHeadless` exists in Ghidra install.
- Modify `src/main.rs` `handle_doctor()`: remove PyGhidra version check. Add bridge script presence check. Add stale port/PID file detection.
- Modify `src/ghidra/bridge.rs`: replace all `include_str!("scripts/*.py")` embeds (lines ~391-403) with single `include_str!("scripts/GhidraCliBridge.java")`. Update script writing logic to write only the Java file.
- Modify `src/ghidra/scripts.rs` if it references Python scripts: update or remove as needed.
- Delete all 13 `.py` files from `src/ghidra/scripts/`

**Code Changes**: _To be filled by Developer_

---

### Milestone 5: E2E Test Validation + CI

**Files**:
- `tests/common/mod.rs` (MODIFY — update `DaemonTestHarness` for new bridge architecture)
- `tests/common/helpers.rs` (MODIFY — update helper functions if needed)
- `tests/daemon_tests.rs` (MODIFY — adapt daemon lifecycle tests)
- `tests/e2e.rs` (MODIFY — verify smoke tests pass)
- `tests/batch_tests.rs` (MODIFY — if DaemonTestHarness interface changes)
- `tests/command_tests.rs` (MODIFY — if DaemonTestHarness interface changes)
- `tests/comment_tests.rs` (MODIFY — if DaemonTestHarness interface changes)
- `tests/diff_tests.rs` (MODIFY — if DaemonTestHarness interface changes)
- `tests/disasm_tests.rs` (MODIFY — if DaemonTestHarness interface changes)
- `tests/find_tests.rs` (MODIFY — if DaemonTestHarness interface changes)
- `tests/graph_tests.rs` (MODIFY — if DaemonTestHarness interface changes)
- `tests/output_format_integration.rs` (MODIFY — if DaemonTestHarness interface changes)
- `tests/patch_tests.rs` (MODIFY — if DaemonTestHarness interface changes)
- `tests/program_tests.rs` (MODIFY — if DaemonTestHarness interface changes)
- `tests/query_tests.rs` (MODIFY — if DaemonTestHarness interface changes)
- `tests/script_tests.rs` (MODIFY — if DaemonTestHarness interface changes)
- `tests/stats_tests.rs` (MODIFY — if DaemonTestHarness interface changes)
- `tests/symbol_tests.rs` (MODIFY — if DaemonTestHarness interface changes)
- `tests/type_tests.rs` (MODIFY — if DaemonTestHarness interface changes)
- `tests/unimplemented_tests.rs` (MODIFY — if DaemonTestHarness interface changes)
- `.github/workflows/test.yml` (MODIFY — remove Python/PyGhidra CI setup if present)

**Requirements**:
- Update `DaemonTestHarness` to work with new bridge architecture:
  - Instead of starting a Rust daemon process, start `analyzeHeadless` with Java bridge
  - Or: use `ghidra import` which now auto-starts the bridge
  - Socket path environment variable replaced with port file path
- All existing E2E test files must pass
- CI workflow removes any Python setup steps

**Acceptance Criteria**:
- `cargo test` passes all existing tests (with Ghidra installed)
- `cargo test --test daemon_tests` validates bridge start/stop/restart
- `cargo test --test query_tests` validates all data query commands
- `cargo test --test command_tests` validates doctor/setup/version
- CI workflow runs without Python dependencies

**Tests**:
- **Test files**: All existing test files in `tests/`
- **Test type**: E2E integration
- **Backing**: Existing test suite is the validation gate
- **Scenarios**:
  - Full regression: every existing test passes
  - New: bridge restart recovery, stale port file cleanup

**Code Intent**:

- Modify `tests/common/mod.rs` `DaemonTestHarness`:
  - `new()`: instead of spawning `ghidra daemon start --foreground`, use `ghidra import` to start bridge, or spawn `analyzeHeadless` directly
  - Replace `socket_path` field with `port` field (read from port file)
  - Update `GHIDRA_CLI_SOCKET` env var to `GHIDRA_CLI_PORT` or equivalent
  - `Drop` impl: send shutdown command via TCP, verify process exit, cleanup port/PID files
- Verify `tests/common/helpers.rs` `ghidra()` builder works with new bridge connection
- Update `tests/daemon_tests.rs`: adapt tests that reference daemon-specific concepts (daemon start/stop → bridge start/stop)
- Update `.github/workflows/test.yml`: remove any `pip install pyghidra` or Python venv setup steps

**Code Changes**: _To be filled by Developer_

---

### Milestone 6: Documentation

**Delegated to**: @agent-technical-writer (mode: post-implementation)

**Source**: `## Invisible Knowledge` section of this plan

**Files**:
- `CLAUDE.md` (MODIFY — update navigation index for new architecture)
- `AGENTS.md` (MODIFY — update architecture description)
- `src/daemon/README.md` (REWRITE — document new bridge-based architecture)
- `CHANGELOG.md` (MODIFY — document breaking change)

**Requirements**:
- CLAUDE.md: update file references (remove Python script references, add Java bridge)
- AGENTS.md: update architecture section to reflect single-layer IPC
- src/daemon/README.md: document new bridge lifecycle, port/PID file management, command protocol
- CHANGELOG.md: document breaking change — Python bridge removed, Java bridge replaces it, setup no longer installs PyGhidra

**Acceptance Criteria**:
- CLAUDE.md is tabular index only
- src/daemon/README.md describes new architecture with ASCII diagram
- CHANGELOG.md has breaking change entry
- No references to Python bridge in documentation

## Milestone Dependencies

```
M1 (Core Java Bridge) ──→ M2 (Extended Handlers) ──→ M3 (Rust Rewrite) ──→ M4 (Setup + Python Removal) ──→ M5 (E2E Tests)
                                                                                                              │
                                                                                                              v
                                                                                                          M6 (Docs)
```

- M1 and M2 are sequential (M2 extends M1's file)
- M3 depends on M1+M2 (Rust side needs Java bridge to exist)
- M4 depends on M3 (can't delete Python until Rust no longer references it)
- M5 depends on M3+M4 (tests validate the complete migration)
- M6 depends on M5 (document after validation)

**Parallelization note**: M1 and early M3 exploration can overlap — Rust-side design can be planned while Java handlers are being written. But M3 implementation depends on M1+M2 being complete for integration testing.