haunt 0.1.1

Command-line client for the haunt agent's HTTP protocol.
haunt-0.1.1 is not a library.

haunt

100% AI-developed.

A minimal debugger-as-an-HTTP-server. Inject haunt.dll into a target Windows process, drive it from anywhere with curl or the haunt CLI.

Memory read/write, software/hardware/page breakpoints, halt with register snapshot, resume/step/run-to-return — over a flat REST surface with no runtime dependencies on the target.

Layout

crates/
├── core/      haunt-core      platform-agnostic HTTP + protocol
├── windows/   haunt-windows   cdylib → haunt.dll
├── inject/    haunt-inject    CreateRemoteThread(LoadLibraryA)
└── cli/       haunt           haunt.exe

Build

Requires Rust (rustup) and MinGW-w64 on macOS / Linux for cross-compile.

# macOS
brew install mingw-w64

# build everything for Windows x86_64
./build.sh           # haunt.dll only
cargo build --release   # all artifacts

Output ends up in target/x86_64-pc-windows-gnu/release/:

  • haunt.dll — the agent (~400 KB, system DLLs only, no MinGW runtime deps)
  • haunt-inject.exe — loader
  • haunt.exe — CLI client

Quick start

On the Windows target, with all three binaries colocated:

haunt-inject --pid 1234 .\haunt.dll

# same box, talk to the agent
set HAUNT_URL=http://127.0.0.1:7878
haunt ping
haunt modules
haunt exports kernel32.dll
haunt bp set 0x7FF601234000 --kind sw
haunt wait          # long-polls until a breakpoint halts a thread
haunt regs 1        # hit_id
haunt resume 1 --step

Workflows

End-to-end recipes that stitch the primitives together. All commands assume HAUNT_URL is set and the agent is running in the target.

Trace a function call and inspect arguments. Set a breakpoint by name, wait for it to hit, read the register snapshot, then let it continue.

haunt bp set kernel32.dll!CreateFileW --kind sw
haunt wait                              # blocks until a thread halts
haunt regs 3                            # hit_id from wait output; args in rcx/rdx/r8/r9
haunt stack 3                           # backtrace with module!offset resolution
haunt read 0x00000014FE3C0000 64        # dereference a pointer arg
haunt resume 3

bp set accepts either a hex address or module!symbol; the agent resolves the name server-side against its export tables. haunt resolve module!symbol returns the resolved address without setting anything.

Patch a return value without touching code. Halt at the ret, overwrite rax, resume.

haunt bp set 0x7FF601234ABC --kind sw --one-shot
haunt wait
printf 'rax=0\n' | haunt setregs 7
haunt resume 7

Watch a field for writes. Hardware breakpoint with --access w catches any thread modifying the address; breakpoints auto-propagate to new threads.

haunt bp set 0x00007FF6DEADBEEF --kind hw --access w --size 4
haunt wait --timeout 60000
haunt regs 12                           # rip points at the writing instruction
haunt resume 12

Non-halting tripwire. --no-halt records hits without parking the thread — useful for sampling hot paths.

haunt bp set 0x7FF601234000 --kind sw --no-halt
# ...let it run...
haunt bp list                           # hit counts per breakpoint

Protocol

All endpoints are plain HTTP/1.1. The agent binds to 127.0.0.1:7878 (loopback only — not currently configurable). Auth is opt-in via HAUNT_TOKEN: set the variable in the target process's environment before injection, then pass it from the client via Authorization: Bearer <token>.

To drive the agent from another machine, tunnel over SSH:

ssh -L 7878:127.0.0.1:7878 <target-host>
HAUNT_URL=http://127.0.0.1:7878 haunt ping

This also encrypts the wire end-to-end — the protocol has no TLS of its own, and the bearer token would otherwise travel in cleartext.

Memory:

  • GET /memory/read?addr=0x...&len=N[&format=hex|raw]
  • POST /memory/write?addr=0x... (raw body)
  • GET /memory/regions

Breakpoints:

  • POST /bp/set?{addr=0x...|name=module!symbol}&kind=sw|hw|page[&access=x|w|rw|any][&size=N][&halt=true|false][&one_shot=true][&tid=N]
  • POST /bp/clear?id=N
  • GET /bp/list

Halts (parked threads):

  • GET /halts
  • GET /halts/wait?timeout=<ms>
  • GET /halts/<hit_id> — register dump
  • GET /halts/<hit_id>/stack[?depth=N] — rbp-chain backtrace with module+offset
  • POST /halts/<hit_id>/regs — modify registers
  • POST /halts/<hit_id>/resume?mode=continue|step|ret

Introspection:

  • GET /modules
  • GET /modules/<name>/exports
  • GET /memory/regions
  • GET /symbols/resolve?name=module!symbol
  • GET /ping GET /version POST /shutdown

Status

  • x86_64 Windows only for now. The Process trait is platform-agnostic; a Linux implementation is on the roadmap.
  • Breakpoint kinds: software (int3), hardware (DR0–DR3), page (PAGE_GUARD).
  • Hardware breakpoints propagate to new threads via DLL_THREAD_ATTACH (uses SetThreadContext on self — documented-fragile but empirically reliable on modern Windows).
  • panic = "abort"; the agent is audited for unwrap/expect. Memory read/write uses ReadProcessMemory/WriteProcessMemory so invalid addresses surface as errors rather than crashing the host.
  • Stack walking is a plain rbp-chain walk, so it's reliable for code with frame pointers and truncates early on release-mode Windows binaries built with /Oy (which is most of them). A proper RtlVirtualUnwind-based walker is on the roadmap.

License

AGPL-3.0. See LICENSE.

If you want to use haunt in a closed-source or SaaS product without publishing your modifications, you'll need a different arrangement — open an issue.