Zion Boggan
repos/Claude Dispatch
zionboggan.com ↗

Claude Dispatch

HMAC-signed, file-system-mediated job dispatch between two agent sessions on different hosts. Built around the use case of two Claude Code sessions running on separate machines and needing to hand work to each other without either one having to drive the other interactively.

1 commits First commit Apr 16, 2026 Last commit Apr 16, 2026 (2 months ago)
Python 65.1%Markdown 34.9%
Files 9 entries
README.md

claude-dispatch

HMAC-signed, file-system-mediated job dispatch between two agent sessions on different hosts. Built around the use case of two Claude Code sessions running on separate machines and needing to hand work to each other without either one having to drive the other interactively.

The transport is a shared filesystem (NFS, SMB, anything mounted on both sides). The integrity story is HMAC-SHA256 signing of the task envelope. The execution story is a per-side watcher that polls the inbox, verifies the signature, optionally waits for a human ack, then spawns a headless agent worker. Results land back as JSON files on the originating side.

There are no public-facing ports, no broker process, and no network code beyond an optional outbound Discord webhook for human notifications.

Why this exists

If you run agent sessions on more than one machine, you eventually want them to be able to delegate work to each other. A Claude Code session on your dev box can hand a "go pull this repo and run the test suite on the GPU box" job to a peer session running on the GPU box, get back the result as a structured file, and continue. The two sessions never need each other's terminals open or attached.

The pieces this needs:

  • A shared place to drop signed task envelopes
  • A way to verify the sender is who they say
  • A bounded executor on the receiving side that can't run away
  • A way to halt the whole thing instantly when something is wrong
  • An audit log

That's the whole project.

Architecture

   node A                                              node B
+---------+                                          +---------+
|  agent  |                                          |  agent  |
| session |                                          | session |
+----+----+                                          +----+----+
     |                                                    |
     | dispatch-send --to b --request "..."               |
     v                                                    v
+---------+   shared filesystem (NFS, SMB, etc.)   +---------+
|  inbox  |<------------------------------------- >|  inbox  |
+---------+   a-to-b/ , b-to-a/                   +---------+
     ^                                                    ^
     |                                                    |
+---------+                                          +---------+
| watcher |  verify HMAC, gate on ack, spawn exec   | watcher |
+----+----+                                          +----+----+
     |                                                    |
     v                                                    v
+---------+                                          +---------+
|  exec   |  spawn headless agent, capture, log     |  exec   |
+----+----+                                          +----+----+
     |                                                    |
     +--> done/<id>.result.json + done/<id>.log <---------+

Components

bin/
  dispatch_lib.py     shared helpers: HMAC sign/verify, task envelope,
                      lane resolution, killswitch check, append-only log
  dispatch_watcher.py per-side polling loop: verifies inbox, promotes
                      to pending-ack or processing, spawns exec with a
                      concurrency cap, cleans up markers
  dispatch-send       CLI: enqueue a signed task to the other side's inbox
  dispatch-exec       headless executor: reads a task, spawns the
                      configured agent binary, captures, writes done/
  dispatch-ack        CLI: approve a pending-ack task
  dispatch-watch-a    wrapper that runs the watcher with --side a
  dispatch-watch-b    wrapper that runs the watcher with --side b

Task envelope (v2)

{
  "id": "<uuid>",
  "from": "<node-id>",
  "to":   "<node-id>",
  "created": "<ISO-8601 UTC>",
  "priority": "low | normal | high",
  "request": "<free text>",
  "require_ack": false,
  "require_dangerous": false,
  "timeout_s": 600,
  "max_output_bytes": 2000000,
  "schema": 2,
  "hmac": "<hex sha256 hmac>"
}

The HMAC covers id | from | to | created | priority | request. Tampering with any of those invalidates the signature; the watcher moves bad-HMAC tasks to rejected/ and logs.

require_ack=true parks the task in pending-ack/ until a human or an automation drops an <id>.ack file next to it. require_dangerous=true runs the executor in bypassPermissions mode; default is acceptEdits.

Directory layout under $DISPATCH_ROOT

$DISPATCH_ROOT/
  keys/hmac.key             shared secret (chmod 600)
  KILLSWITCH                if this file exists, no new exec spawns
  session-log.jsonl         append-only event log
  heartbeats/<node>.json    last-write liveness for each side
  a-to-b/
    inbox/         pending-ack/    processing/    done/    rejected/
  b-to-a/
    inbox/         pending-ack/    processing/    done/    rejected/

Setup

export DISPATCH_ROOT=/mnt/shared/dispatch
export DISPATCH_NODE_A=devbox
export DISPATCH_NODE_B=gpubox

mkdir -p "$DISPATCH_ROOT/keys"
openssl rand -hex 32 > "$DISPATCH_ROOT/keys/hmac.key"
chmod 600 "$DISPATCH_ROOT/keys/hmac.key"

mkdir -p "$DISPATCH_ROOT/${DISPATCH_NODE_A}-to-${DISPATCH_NODE_B}"/{inbox,pending-ack,processing,done,rejected}
mkdir -p "$DISPATCH_ROOT/${DISPATCH_NODE_B}-to-${DISPATCH_NODE_A}"/{inbox,pending-ack,processing,done,rejected}
mkdir -p "$DISPATCH_ROOT/heartbeats"

On node A:

DISPATCH_SIDE=devbox bin/dispatch-watch-a

On node B:

DISPATCH_SIDE=gpubox bin/dispatch-watch-b

Both watchers should run under systemd (or your supervisor of choice) for liveness.

Usage

Send a task from A to B:

DISPATCH_FROM=devbox bin/dispatch-send \
  --to gpubox \
  --request "Run the smoke test suite on this branch and report numbers."

dispatch-send prints the task id and the inbox path. The watcher on B verifies the HMAC, decides whether to require an ack, and (if auto-exec) spawns the executor.

The result lands at:

$DISPATCH_ROOT/devbox-to-gpubox/done/<id>.result.json
$DISPATCH_ROOT/devbox-to-gpubox/done/<id>.log

Killswitch

echo "stopping for reason X" > "$DISPATCH_ROOT/KILLSWITCH"

Both watchers refuse to spawn new exec processes while the file exists. Running exec processes are not interrupted; they finish or hit their timeout. Remove the file to resume.

Knobs

Environment variables read by the watcher and exec:

Var Default Meaning
DISPATCH_ROOT - required, shared filesystem path
DISPATCH_NODE_A a node A identifier
DISPATCH_NODE_B b node B identifier
DISPATCH_POLL_SEC 3 watcher poll interval
DISPATCH_MAX_PARALLEL 2 concurrent exec processes per side
DISPATCH_AGENT_BIN claude executor command
DISPATCH_FROM - default sender id for dispatch-send
DISPATCH_SIDE - this node's id (for dispatch-ack)

License

MIT. See LICENSE.