Skip to content
← All writing·AI Systems·May 10, 2026·10 min

Replacing Telegram with a Signed HTTP Transport for Hermes and OpenClaw

How I wired Hermes, task-worker, and OpenClaw together with signed HTTP requests, local webhooks, OpenAI-compatible chat completions, and a small broker service.

I recently finished wiring Hermes, task-worker, and OpenClaw together without using Telegram as the inter-agent transport. The goal was simple: Hermes should be able to delegate work, OpenClaw should execute it, and the result should flow back into Hermes through a clean local HTTP path.

The final setup uses a small task-worker service as the broker. Hermes signs an outbound request, task-worker verifies it, forwards the payload to OpenClaw’s OpenAI-compatible chat completions endpoint, then posts the result back into Hermes through a signed webhook route.

This post documents the working shape, the pieces that mattered, and the checks that finally proved the whole loop was alive.

The final topology

In plain terms:

  • Hermes sends delegated work to task-worker.
  • task-worker verifies the Hermes signature.
  • task-worker forwards the accepted task to OpenClaw.
  • OpenClaw returns a standard chat completion response.
  • task-worker extracts the useful completion text.
  • task-worker posts the result back into Hermes through a webhook route.

The important design decision was keeping Hermes and OpenClaw loosely coupled. Neither one needs to know too much about the other. The broker owns transport details, signature checks, forwarding, and result normalization.

The local service map

The working local endpoints were:

Hermes webhook listener: http://127.0.0.1:8644
task-worker:             http://127.0.0.1:9000
OpenClaw gateway:        http://127.0.0.1:18789

OpenClaw accepted requests at:

POST /v1/chat/completions
Authorization: Bearer <OPENCLAW_API_KEY>

That mattered because it let task-worker treat OpenClaw like an OpenAI-compatible gateway. The forwarder did not need a special SDK or transport plugin. It only needed fetch, a bearer token, and a normal chat completions payload.

Hermes needs a webhook route

Hermes has to expose a route where task-worker can send results. In this setup, that route lives under top-level platforms.webhook, and the working callback route name is task-worker-result.

The important shape looks like this:

platforms:
  webhook:
    enabled: true
    extra:
      port: 8644
      secret: "global-fallback-secret"
      routes:
        task-worker-result:
          secret: "<shared webhook secret>"
          prompt: |
            OpenClaw returned a delegated task result.

            Task ID: {task_id}
            Conversation ID: {conversation_id}
            Status: {status}
            Summary: {summary}
            Error: {error}

            Details:
            {details}
          deliver: log

For local routing, Hermes also needed:

security:
  allow_private_urls: true

Without that, the local callback and task routes are likely to fail before your app logic gets a chance to do anything useful.

Signing the Hermes to task-worker hop

The first security boundary is between Hermes and task-worker. Hermes signs the exact JSON bytes it sends using HMAC SHA–256 and places the signature in x-hermes-signature.

That detail is easy to overlook: sign the exact bytes you post. If you sign one JSON representation and send another, even small whitespace or serialization differences can break verification.

The core signing function is small:

import hashlib
import hmac

def sign(body: bytes, secret: str) -> str:
    digest = hmac.new(
        secret.encode("utf-8"),
        body,
        hashlib.sha256,
    ).hexdigest()

    return f"sha256={digest}"

The outbound request then includes the signature:

body = json.dumps(payload).encode("utf-8")

headers = {
    "content-type": "application/json",
    "x-request-id": context.get("parent_session_id", ""),
    "x-hermes-signature": sign(body, TASK_WORKER_SECRET),
}

await client.post(TASK_WORKER_URL, content=body, headers=headers)

The critical secret pair is:

Hermes TASK_WORKER_SECRET == task-worker HERMES_SECRET

If those values differ, task-worker rejects the request with bad_signature.

task-worker as the broker

The broker exposes two endpoints:

POST /task    receives signed delegated work from Hermes
POST /result  receives signed callbacks from OpenClaw, if callback mode is used

The working implementation uses raw-body HMAC verification, stores routing state in memory, forwards accepted tasks to OpenClaw, and relays results back to Hermes.

The forwarder sends a normal chat completions payload:

async function forwardToOpenClaw(envelope) {
  const url = process.env.OPENCLAW_URL;
  const apiKey = process.env.OPENCLAW_API_KEY;

  const body = JSON.stringify({
    model: "openclaw/default",
    messages: [
      {
        role: "system",
        content:
          "You are OpenClaw receiving a delegated task from Hermes through task-worker. " +
          "Read the provided JSON payload, execute the task, and respond concisely.",
      },
      {
        role: "user",
        content: JSON.stringify({
          task_id: envelope.task_id,
          conversation_id: envelope.conversation_id,
          goal: envelope.goal,
          context: envelope.context,
          constraints: envelope.constraints,
          expected_output: envelope.expected_output,
          metadata: {
            trace_id: envelope.trace_id,
            received_at: envelope.received_at,
          },
        }),
      },
    ],
    stream: false,
  });

  const response = await fetch(url, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      authorization: `Bearer ${apiKey}`,
    },
    body,
  });

  if (!response.ok) {
    const text = await response.text().catch(() => "");
    throw new Error(`OpenClaw forward failed: ${response.status} ${text}`);
  }

  return response.json();
}

Once OpenClaw responds, task-worker extracts:

json?.choices?.[0]?.message?.content;

Then it posts that result back to Hermes with the task ID, conversation ID, status, summary, details, error state, and trace ID.

The environment variables that matter

The task-worker environment ended up looking like this:

AGENT_NAME=Claw
HOST=127.0.0.1
PORT=9000

HERMES_SECRET=<same value as TASK_WORKER_SECRET in Hermes>
OPENCLAW_SECRET=<shared secret for /result verification if OpenClaw signs callbacks>

OPENCLAW_URL=http://127.0.0.1:18789/v1/chat/completions
OPENCLAW_API_KEY=<OpenClaw gateway token>

HERMES_WEBHOOK_URL=http://127.0.0.1:8644/webhooks/task-worker-result
HERMES_WEBHOOK_SECRET=<same value as Hermes webhook route secret>

The two most common mistakes are easy to predict:

  1. TASK_WORKER_SECRET and HERMES_SECRET do not match.
  2. OPENCLAW_URL points to the gateway root instead of /v1/chat/completions.

Both produce failures that look like transport problems, but they are really configuration problems.

The systemd environment trap

The hardest bug was not in Hermes, task-worker, or OpenClaw. It was in the runtime environment.

Hermes was running as a systemd --user service, and the .env values I expected were not actually visible to the running hermes-gateway process. The definitive check was not:

systemctl --user show hermes-gateway --property=Environment

The useful check was reading the actual running process environment:

PID=$(systemctl --user show hermes-gateway.service --property=MainPID --value)
tr '\0' '\n' < /proc/$PID/environ | grep -E 'TASK_WORKER|HERMES_WEBHOOK'

The fix was to create a systemd user environment file:

[Service]
EnvironmentFile=/home/larry/.config/systemd/user/hermes-gateway.env

Then reload and restart:

systemctl --user daemon-reload
systemctl --user restart hermes-gateway

After that, the /proc/$PID/environ check confirmed Hermes was running with the expected task-worker and webhook values.

Verification checklist

Once the services were configured, I verified the setup in four passes.

Check the listeners

ss -ltnp | grep -E '8644|9000|18789'
curl http://127.0.0.1:9000/
curl http://127.0.0.1:8644/health

The expected state:

Hermes:     8644
task-worker: 9000
OpenClaw:  18789

Check the Hermes webhook

A signed POST to the Hermes webhook should return something like:

{
  "status": "accepted",
  "route": "task-worker-result",
  "event": "unknown",
  "delivery_id": "..."
}

That confirms task-worker has a valid path back into Hermes.

Check OpenClaw directly

curl -v http://127.0.0.1:18789/v1/chat/completions \
  -H "Authorization: Bearer <OPENCLAW_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openclaw/default",
    "messages": [
      { "role": "user", "content": "Hello from direct OpenClaw test" }
    ],
    "stream": false
  }'

The important proof is a standard response with:

choices[0].message.content

Check the full /task path

The final test is a Hermes-style signed request into task-worker:

SECRET='<TASK_WORKER_SECRET>'

BODY='{
  "task_id": "test-forward-openclaw",
  "goal": "Test Hermes -> task-worker -> OpenClaw -> Hermes round-trip",
  "context": {
    "source_event": "manual_test",
    "parent_session_id": "manual-session",
    "child_role": "tester",
    "child_status": "ok",
    "duration_ms": 1234,
    "emitted_at": "2026-05-05T16:47:00Z"
  },
  "constraints": {
    "timeout_sec": 600,
    "tools_allowed": []
  },
  "expected_output": {
    "format": "json"
  }
}'

DIGEST=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
SIG="sha256=$DIGEST"

curl -v http://127.0.0.1:9000/task \
  -H "content-type: application/json" \
  -H "x-hermes-signature: $SIG" \
  -H "x-request-id: manual-openclaw-test" \
  -d "$BODY"

The working response is:

HTTP/1.1 202 Accepted

That confirms the whole route: signature verification, task acceptance, forward into OpenClaw, and eventual relay back into Hermes.

Why this setup feels better than Telegram transport

Telegram was useful as a quick bridge, but HTTP is easier to reason about in a local agent system. It gives me clear request boundaries, local ports, HMAC verification, standard health checks, and normal service logs.

The new transport also makes each system’s role cleaner:

| Component   | Responsibility                                                  |
| ----------- | --------------------------------------------------------------- |
| Hermes      | Decide when work should be delegated and receive the result     |
| task-worker | Verify, route, normalize, and relay task messages               |
| OpenClaw    | Execute the delegated task through a chat completions interface |
| systemd     | Keep the runtime environment stable and repeatable              |

That separation is the point. Hermes does not need to become an OpenClaw client. OpenClaw does not need to know how Hermes wants results delivered. The broker does the glue work.

Final working status

The final transport is:

Hermes hook
  -> signed POST /task
task-worker
  -> verified HMAC
  -> POST OpenClaw /v1/chat/completions
OpenClaw
  -> chat completion response
task-worker
  -> signed Hermes webhook result
Hermes
  -> logs or handles the delegated result

This replaces Telegram with a signed local HTTP transport while keeping Hermes and OpenClaw loosely coupled. The result is easier to debug, easier to test, and easier to extend into a more durable agent orchestration layer.

More writing

Adjacent essays

DevOps·May 11, 2026·2 min

Kubernetes Flow

Learn how Kubernetes orchestrates containers through its control plane, pods, services, and ingress in this beginner-friendly introduction.

LuCodesRead →
Dracula Theme K8's
DevOps·May 11, 2026·1 min

What is Kubernetes?

Explore how Kubernetes orchestrates containers through intelligent scheduling, self-healing, and automated scaling across distributed infrastructure.

LuCodesRead →