From 658e598bd1f57e0831f9359959a94fb9ca223909 Mon Sep 17 00:00:00 2001 From: Eric Dahl Date: Wed, 23 Sep 2026 06:53:40 -0400 Subject: [PATCH 1/2] feat: add Hermes landing page --- app/hermes/page.tsx | 41 +++++++++++++++++++++++++++++++ app/page.tsx | 4 +++ tests/landing/copy.test.ts | 7 ++++++ tests/landing/hermes-page.test.ts | 33 +++++++++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 app/hermes/page.tsx create mode 100644 tests/landing/hermes-page.test.ts diff --git a/app/hermes/page.tsx b/app/hermes/page.tsx new file mode 100644 index 0000000..c04f0b5 --- /dev/null +++ b/app/hermes/page.tsx @@ -0,0 +1,41 @@ +import Link from "next/link"; + +import { Button } from "@/components/ui/button"; + +const steps = ["Provider webhook", "clawproxy route", "Hermes agent run"]; +const snippet = `hermes config set webhooks.relay_url https://clawproxy.io/api/ingress/hermes +hermes config set webhooks.node_token cpn_your_node_token +hermes kanban dispatch`; + +export const dynamic = "force-dynamic"; + +export default function HermesLandingPage() { + return ( +
+
+
+
+ + clawproxy + Webhook relay for Hermes agent workflows + + +
+
+
+

Webhook relay for Hermes agent workflows

+

Route webhooks into private Hermes runs.

+

Route GitHub, Stripe, Slack, or custom webhooks into Hermes without opening inbound ports. clawproxy accepts the public request, stores it, and delivers it to Hermes when your node is configured with an outbound connection.

+
+
+

delivery target

Hermes stays private. Work still starts instantly.

Public webhooks become reliable inputs for agent workflows, Kanban dispatch, and automation runs.

{steps.map((step, index) => (

{String(index + 1).padStart(2, "0")}

{step}

))}
+
+
{["Trigger Kanban workers from GitHub", "Keep agent boxes private", "Debug webhook delivery"].map((title, index) => (

{String(index + 1).padStart(2, "0")}

{title}

Use clawproxy as the durable public front door for Hermes automation that should stay behind outbound-only network boundaries.

))}
+

Connect Hermes with one public route.

Create a route, copy the node token into Hermes, and let Hermes pull work from clawproxy over an authenticated outbound connection.

setup snippet

{snippet}
+
+
+ ); +} diff --git a/app/page.tsx b/app/page.tsx index 74c8e19..49c3fec 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -163,6 +163,10 @@ export default function Home() {

Works with Hermes Agent, OpenClaw, and any client that can receive events from an authenticated outbound connection. + {' '} + + Using Hermes? +

diff --git a/tests/landing/copy.test.ts b/tests/landing/copy.test.ts index 81fa440..02e56f3 100644 --- a/tests/landing/copy.test.ts +++ b/tests/landing/copy.test.ts @@ -23,4 +23,11 @@ describe('landing page copy', () => { expect(html).toContain('OpenClaw'); expect(html).not.toContain('private OpenClaw nodes'); }); + + test('includes a contextual entry point for Hermes users', async () => { + const html = await landingHtml(); + + expect(html).toContain('href="/hermes"'); + expect(html).toContain('Using Hermes?'); + }); }); diff --git a/tests/landing/hermes-page.test.ts b/tests/landing/hermes-page.test.ts new file mode 100644 index 0000000..942c50e --- /dev/null +++ b/tests/landing/hermes-page.test.ts @@ -0,0 +1,33 @@ +import { JSDOM } from "jsdom"; +import { describe, expect, test } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +async function hermesHtml() { + const hermesPage = await import("@/app/hermes/page"); + return renderToStaticMarkup(hermesPage.default()); +} + +describe("Hermes landing page", () => { + test("targets Hermes agent webhook relay workflows", async () => { + const html = await hermesHtml(); + expect(html).toContain("Webhook relay for Hermes agent workflows"); + expect(html).toContain("Route GitHub, Stripe, Slack, or custom webhooks into Hermes"); + expect(html).toContain("configured with an outbound connection"); + }); + + test("includes a setup snippet and relevant CTAs", async () => { + const html = await hermesHtml(); + const dom = new JSDOM(html); + const document = dom.window.document; + expect(document.querySelector("code")?.textContent).toContain("hermes config set webhooks.relay_url"); + expect(document.querySelector("a[href=\"/dashboard\"]")?.textContent).toContain("Create a route"); + expect(document.querySelector("a[href=\"https://github.com/ericdahl-dev/clawproxy-hermes\"]")?.textContent).toContain("Hermes plugin"); + }); + + test("shows the Hermes-specific delivery path", async () => { + const html = await hermesHtml(); + expect(html).toContain("Provider webhook"); + expect(html).toContain("clawproxy route"); + expect(html).toContain("Hermes agent run"); + }); +}); From 5a0450235abf9fc4964006b4bc8483a62537f16f Mon Sep 17 00:00:00 2001 From: Eric Dahl Date: Wed, 23 Sep 2026 07:03:53 -0400 Subject: [PATCH 2/2] fix: correct Hermes landing setup snippet --- app/hermes/page.tsx | 17 +++++++++++++---- tests/landing/hermes-page.test.ts | 8 +++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/app/hermes/page.tsx b/app/hermes/page.tsx index c04f0b5..40aaaea 100644 --- a/app/hermes/page.tsx +++ b/app/hermes/page.tsx @@ -3,9 +3,18 @@ import Link from "next/link"; import { Button } from "@/components/ui/button"; const steps = ["Provider webhook", "clawproxy route", "Hermes agent run"]; -const snippet = `hermes config set webhooks.relay_url https://clawproxy.io/api/ingress/hermes -hermes config set webhooks.node_token cpn_your_node_token -hermes kanban dispatch`; +const snippet = `# 1. Create a clawproxy route in the dashboard +# Provider URL: https://clawproxy.io/api/ingress// + +# 2. Install the Hermes gateway plugin +hermes plugins install ericdahl-dev/clawproxy-hermes + +# 3. Add these to ~/.hermes/.env (or your profile's .env) +CLAWPROXY_NODE_TOKEN=cpn_your_node_token +CLAWPROXY_HERMES_WEBHOOK_SECRET= + +# 4. Restart the gateway, then check delivery +hermes gateway status`; export const dynamic = "force-dynamic"; @@ -34,7 +43,7 @@ export default function HermesLandingPage() {

delivery target

Hermes stays private. Work still starts instantly.

Public webhooks become reliable inputs for agent workflows, Kanban dispatch, and automation runs.

{steps.map((step, index) => (

{String(index + 1).padStart(2, "0")}

{step}

))}
{["Trigger Kanban workers from GitHub", "Keep agent boxes private", "Debug webhook delivery"].map((title, index) => (

{String(index + 1).padStart(2, "0")}

{title}

Use clawproxy as the durable public front door for Hermes automation that should stay behind outbound-only network boundaries.

))}
-

Connect Hermes with one public route.

Create a route, copy the node token into Hermes, and let Hermes pull work from clawproxy over an authenticated outbound connection.

setup snippet

{snippet}
+

Connect Hermes with one public route.

Create a clawproxy route, copy its generated provider URL and node token from the dashboard, then install the Hermes plugin. A clawproxy route named github-prs delivers to the Hermes webhook route with the same slug.

setup snippet

{snippet}
); diff --git a/tests/landing/hermes-page.test.ts b/tests/landing/hermes-page.test.ts index 942c50e..6544fd0 100644 --- a/tests/landing/hermes-page.test.ts +++ b/tests/landing/hermes-page.test.ts @@ -19,7 +19,13 @@ describe("Hermes landing page", () => { const html = await hermesHtml(); const dom = new JSDOM(html); const document = dom.window.document; - expect(document.querySelector("code")?.textContent).toContain("hermes config set webhooks.relay_url"); + const snippet = document.querySelector("pre code")?.textContent ?? ""; + expect(snippet).toContain("hermes plugins install ericdahl-dev/clawproxy-hermes"); + expect(snippet).toContain("CLAWPROXY_NODE_TOKEN=cpn_your_node_token"); + expect(snippet).toContain("CLAWPROXY_HERMES_WEBHOOK_SECRET="); + expect(snippet).toContain("clawproxy.io/api/ingress//"); + expect(snippet).not.toContain("/api/ingress/hermes"); + expect(snippet).not.toContain("webhooks.relay_url"); expect(document.querySelector("a[href=\"/dashboard\"]")?.textContent).toContain("Create a route"); expect(document.querySelector("a[href=\"https://github.com/ericdahl-dev/clawproxy-hermes\"]")?.textContent).toContain("Hermes plugin"); });