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.
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.
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:
That's the whole project.
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 <---------+
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
{
"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.
$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/
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.
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
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.
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) |
MIT. See LICENSE.