ghidra-cli 0.1.10

Rust CLI to run Ghidra headless for reverse engineering with Claude Code and other agents
Documentation
# Ghidra-CLI Open Source Release Plan (Daemon-Only Architecture)

> Historical planning document: this does not reflect the current implementation.
> Current architecture is direct CLI-to-Java bridge (`GhidraCliBridge.java`) as documented in `README.md` and `AGENTS.md`.

## Overview

Prepare ghidra-cli for open source release with a **daemon-only architecture**. Binary analysis is slow enough that a persistent Ghidra process (via daemon) is always preferable to spawning new processes per command.

**Key architectural change**: Remove HeadlessExecutor as an execution path. All query operations MUST go through the daemon, which maintains a persistent GhidraBridge connection to Ghidra.

## Planning Context

### Decision Log

| Decision | Reasoning Chain |
|----------|-----------------|
| Daemon-only for queries | Binary analysis takes 5-30+ seconds cold start -> persistent daemon amortizes this -> always faster for real workflows -> user confirmed this approach |
| Remove HeadlessExecutor fallback | Fallback creates confusion about which path is used -> daemon is always better -> remove fallback, require daemon explicitly |
| Use IPC layer over legacy RPC | IPC uses local sockets (faster, no port conflicts) -> cross-platform (Unix sockets + Windows named pipes) -> already implemented in src/ipc/ |
| Wire CommandQueue to GhidraBridge | Queue already exists with caching -> handler.rs already routes to bridge -> just need to connect the pieces |
| Keep standalone commands local | Config, setup, doctor, version don't need Ghidra -> run locally without daemon |

### Rejected Alternatives

| Alternative | Why Rejected |
|-------------|--------------|
| Keep HeadlessExecutor as fallback | Creates two code paths to maintain -> daemon is always better -> simpler to require daemon |
| Auto-start daemon | Adds complexity -> explicit start is clearer -> user knows daemon is running |
| Remove daemon RPC entirely | Some users may have scripts using RPC -> deprecate but keep for now |

### Constraints & Assumptions

- **User-specified**: Daemon-only architecture for query operations
- **Technical**: GhidraBridge and bridge.py already functional
- **Technical**: IPC protocol and transport already implemented
- **Pattern**: handler.rs already translates IPC Commands to bridge calls

## Current State Analysis

### What Already Works

```
✓ GhidraBridge (src/ghidra/bridge.rs) - persistent TCP to Ghidra
✓ bridge.py - Python server inside Ghidra with command handlers
✓ IPC protocol (src/ipc/protocol.rs) - typed Command enum
✓ IPC transport (src/ipc/transport.rs) - cross-platform sockets
✓ IPC client (src/ipc/client.rs) - high-level client API
✓ IPC server (src/daemon/ipc_server.rs) - accepts connections
✓ Handler (src/daemon/handler.rs) - routes commands to bridge
✓ Daemon lifecycle (start/stop/status/ping)
```

### What Needs Wiring

```
✗ CommandQueue.execute_command() is stubbed (returns TODO message)
✗ main.rs uses daemon_rpc (legacy) not ipc (new)
✗ Fallback to HeadlessExecutor still exists
✗ Query operations bypass daemon entirely
```

## Milestones

> All file paths are relative to repository root

### Milestone 1: Fix Cargo.toml Repository URL

**Files**: `Cargo.toml`

**Requirements**:
- Update repository URL from localhost to GitHub

**Acceptance Criteria**:
- URL matches `https://github.com/akiselev/ghidra-cli`

**Code Changes**:
```diff
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,7 +5,7 @@ edition = "2021"
 authors = ["Alexander Kiselev"]
 description = "Rust CLI to run Ghidra headless for reverse engineering with Claude Code and other agents"
 license = "GPL-3.0"
-repository = "http://127.0.0.1:62915/git/akiselev/ghidra-cli"
+repository = "https://github.com/akiselev/ghidra-cli"
```

---

### Milestone 2: Wire IPC Client in main.rs

**Files**: `src/main.rs`

**Requirements**:
- Replace `daemon_rpc::DaemonClient` with `ipc::client::DaemonClient`
- Route query commands through IPC when daemon is running
- Return error (not fallback) when daemon required but not running

**Acceptance Criteria**:
- `ghidra query functions` uses IPC when daemon running
- `ghidra query functions` errors with "start daemon" message when daemon not running
- No fallback to HeadlessExecutor

**Code Intent**:
- In `run_with_daemon_check()`: Replace `daemon_rpc::DaemonClient::connect(port)` with `ipc::client::DaemonClient::connect(socket_path)`
- Add match on command type: query-based commands REQUIRE daemon, others can run standalone
- Remove the `else { run(cli) }` fallback for query commands
- Add helpful error message: "This command requires the daemon. Start with: ghidra daemon start --project <name>"

---

### Milestone 3: Implement CommandQueue Execute

**Files**: `src/daemon/queue.rs`

**Requirements**:
- Implement `execute_command()` to actually execute commands via GhidraBridge
- Replace the TODO stub with real execution

**Acceptance Criteria**:
- Commands submitted to queue are executed via bridge
- Results are cached and returned

**Code Intent**:
- Change `execute_command()` to take a reference to `GhidraBridge`
- Translate `Commands` enum to bridge operations
- Call `bridge.send_command()` for each operation type
- Parse JSON response and return formatted result

---

### Milestone 4: Add XRefs Support to Handler

**Files**: `src/daemon/handler.rs`, `src/ipc/protocol.rs`

**Requirements**:
- Ensure XRefs commands are handled in the daemon
- Handler routes XRefsTo/XRefsFrom to bridge

**Acceptance Criteria**:
- `ghidra query xrefs --to main` works via daemon
- Returns JSON array of cross-references

**Code Intent**:
- Verify `Command::XRefsTo` and `Command::XRefsFrom` exist in protocol.rs
- Add handling in `handler.rs` for these commands
- Call `bridge.xrefs_to()` / `bridge.xrefs_from()`

---

### Milestone 5: Add --to Parameter for XRefs Query

**Files**: `src/cli.rs`, `src/main.rs`

**Requirements**:
- Add `--to` parameter to query subcommand
- Pass target to daemon when querying xrefs

**Acceptance Criteria**:
- `ghidra query xrefs --to main --project x --program y` parses correctly
- Target is sent to daemon in IPC request

**Code Intent**:
- Add `to: Option<String>` to QueryArgs in cli.rs
- In main.rs query handling, include target in IPC command

---

### Milestone 6: Deprecate HeadlessExecutor

**Files**: `src/ghidra/headless.rs`, `src/ghidra/mod.rs`

**Requirements**:
- Mark HeadlessExecutor as deprecated
- Remove direct calls from main.rs
- Keep module for potential future use (import/analyze operations)

**Acceptance Criteria**:
- No query operations use HeadlessExecutor
- Compile succeeds with deprecation warnings only

**Code Intent**:
- Add `#[deprecated(note = "Use daemon for query operations")]` to HeadlessExecutor
- Remove/comment out direct HeadlessExecutor usage in main.rs handle_* functions
- Route through daemon instead

---

### Milestone 7: Fix Compiler Warnings

**Files**: Multiple (same as original plan)

**Requirements**:
- Remove unused imports
- Handle dead code appropriately

**Acceptance Criteria**:
- `cargo build` produces no warnings (or only deprecation warnings for HeadlessExecutor)

**Code Intent**:
- Remove unused imports from handler.rs, queue.rs, etc.
- Keep IPC layer code (now actually used!)
- Remove truly dead methods

---

### Milestone 8: Add E2E Tests for Daemon Queries

**Files**: `tests/e2e.rs`

**Requirements**:
- Test query operations through daemon
- Test XRefs query

**Acceptance Criteria**:
- Tests start daemon, run queries, stop daemon
- All tests pass

**Code Intent**:
- Add test helper to start/stop daemon
- Add `test_daemon_query_functions()`
- Add `test_daemon_query_xrefs()`
- Add `test_daemon_required_error()` - verify error when daemon not running

---

### Milestone 9: Expand README.md

**Files**: `README.md`

**Requirements**:
- Document daemon-first architecture
- Explain why daemon is required for queries
- Quick start with daemon workflow

**Acceptance Criteria**:
- README explains daemon requirement
- Includes daemon workflow example

**Code Intent**:
- Expand README with:
  - Overview explaining persistent Ghidra benefit
  - Quick start: `ghidra daemon start`, then queries
  - Architecture section explaining daemon design
  - All 9 sections from original plan

---

## Milestone Dependencies

```
M1 (Cargo.toml) ----+
                    |
M2 (IPC Client) ----+----> M4 (XRefs Handler) ----> M5 (--to param)
                    |
M3 (Queue Execute) -+----> M6 (Deprecate Headless)
                    |
                    +----> M7 (Warnings) ----> M8 (Tests) ----> M9 (README)
```

## Architecture After Changes

```
CLI Command
[Command Type?]
    ├─ Daemon Control (start/stop) → Handle locally
    ├─ Config/Setup/Doctor → Handle locally
    └─ Query/Decompile/etc → REQUIRES DAEMON
                         [Daemon Running?]
                         ├─ NO → Error: "Start daemon first"
                         └─ YES → IPC Client
                              Send Command (local socket)
                              IPC Server receives
                              handler.rs routes
                              GhidraBridge.send_command()
                              bridge.py in Ghidra
                              Response back
                              Format & display
```

## Key Benefits

1. **Faster queries**: Ghidra stays loaded, no 5-30s startup per command
2. **Simpler code**: One execution path, not two
3. **Better UX**: Consistent behavior, clear daemon requirement
4. **Easier debugging**: All queries go through same path