Install
openclaw skills install @robbeverhelst/wakehookInstall, configure, authorize and run wakehook so the user's agent (OpenClaw or Hermes) runs their morning routine when they wake up. wakehook turns Google Health / Fitbit sleep data into a neutral user.awake event and POSTs it to the agent's inbound webhook, which turns it into a wake/agent action. Use when the user wants wake-triggered automations, mentions wakehook, or asks to "do something when I wake up".
openclaw skills install @robbeverhelst/wakehookwakehook is a tiny self-hosted service that watches the user's sleep data
(Google Health / Fitbit), detects the moment they woke up, and POSTs the
user's agent a neutral user.awake event so the morning routine runs itself — at
most once per morning. The agent maps that event into a wake/agent action (its
native pattern), so wakehook stays vendor-neutral.
This works with OpenClaw and Hermes Agent — they both ingest a webhook and template it. Everything below is shared except Step 1, where you pick the agent. Work through the steps in order. Ask the user at the decision points instead of assuming. Do not improvise from the README — these steps are verified.
bun --version; install with curl -fsSL https://bun.sh/install | bash.
wakehook is Bun-only (bun:sqlite); never run it with Node.googlehealth.sleep.readonly scope, plus an OAuth 2.0
client (client id + secret). An API key will NOT work; sleep data needs
user-consented OAuth. Ask the user for the id + secret (reuse their existing
Google project if they have one).wakehook POSTs the raw user.awake event to the agent's inbound webhook; the
agent templates it into a wake. Do the subsection for the user's agent — it gives
both the agent-side config and the matching wakehook subscriber (drop the
subscriber into config.json in Step 3).
In the OpenClaw gateway config, enable hooks and add a mapping:
{
hooks: {
enabled: true,
path: "/hooks", // must not be "/"
token: "<HOOKS_TOKEN>", // any strong random string
mappings: [
{
match: { path: "wakehook" }, // matches POST /hooks/wakehook
action: "wake",
wakeMode: "now",
name: "wakehook",
// action "wake" builds its text from textTemplate (NOT messageTemplate —
// that's only for action "agent"; the wrong one returns HTTP 400):
textTemplate: "You woke at {{wokeAt}} (slept {{session.durationMin}} min).",
},
],
},
}
Gateway address: OpenClaw's docs examples use 127.0.0.1:18789 — confirm the
user's port. /hooks/* is gated by the token in a header (Authorization: Bearer
or x-openclaw-token). wakehook subscriber:
{ "id": "openclaw", "url": "http://<host>:<port>/hooks/wakehook",
"headers": { "Authorization": "Bearer <HOOKS_TOKEN>" } }
In the Hermes webhook config, add a route with a prompt template + secret:
platforms:
webhook:
enabled: true
extra:
port: 8644
routes:
wakehook:
secret: "<ROUTE_SECRET>"
prompt: "The user woke at {wokeAt} (slept {session.durationMin} min). Run the morning routine."
# deliver: telegram # optional: send the result to a chat
Hermes verifies an HMAC signature; wakehook signs with the route secret under the header Hermes expects. wakehook subscriber:
{ "id": "hermes", "url": "http://<host>:8644/webhooks/wakehook",
"secret": "<ROUTE_SECRET>", "signatureHeader": "X-Webhook-Signature", "signatureFormat": "hex" }
On loopback you can skip signing entirely with Hermes's
INSECURE_NO_AUTHand dropsecret/signatureHeader/signatureFormat.
Two modes — confirm which they want; don't assume:
pollWindowOnly),
so a short pollIntervalMs (5 min default) is cheap — wake is detected within
one interval. This path is verified end-to-end.If the user has no preference, use poll.
Create config.json in the working directory. Set timezone to the user's
IANA zone and drop in the subscriber from Step 1 (the OpenClaw or Hermes one):
{
"dbPath": "./wake.sqlite",
"source": "google-health",
"inference": {
"timezone": "Europe/Brussels",
"windowStart": "04:00",
"windowEnd": "11:00",
"minDurationMin": 180,
"supersedeGapMin": 45
},
"google": { "mode": "poll", "pollIntervalMs": 300000, "pollLookbackMin": 720, "pollWindowOnly": true, "pollWindowMarginMin": 30 },
"subscribers": [
{ "id": "openclaw", "url": "http://<host>:<port>/hooks/wakehook", "headers": { "Authorization": "Bearer <HOOKS_TOKEN>" } }
]
}
The
subscribersarray above uses the OpenClaw entry; swap it for the Hermes entry from Step 1 if that's the agent. You can list both to fan out to multiple agents.
No
presetneeded —generic(the raw event) is the default and is what both agents' mappings/routes expect. The subscriber's auth (the OpenClaw bearer or the Hermes secret/signature) must match the agent config from Step 1.If the user chose webhook mode instead: set
"google"to{ "mode": "webhook", "webhookAuthToken": "<random>" }, also putGOOGLE_WEBHOOK_AUTH_TOKENin.env, expose wakehook's/webhookover public HTTPS, and register a Google Health sleep subscription pointing at it. (Unverified.)
Create .env (keep secrets out of config.json):
GOOGLE_CLIENT_ID=<client-id>
GOOGLE_CLIENT_SECRET=<client-secret>
GOOGLE_REDIRECT_URI=http://localhost:8080/oauth/callback
Ensure http://localhost:8080/oauth/callback is an authorized redirect URI on the OAuth client.
bunx wakehook-auth
Prints a Google consent URL — give it to the user to open and approve. It stores
a refresh token in ./wake.sqlite and auto-refreshes forever (one-time step).
If Google returns HTTP 400, the OAuth client is missing the
localhost:8080/oauth/callback redirect URI — add it and retry.
bunx wakehook
(Docker alt: docker run -v wakehook-data:/data --env-file .env ghcr.io/robbeverhelst/wakehook.)
Keep it running (long-lived process / service). In poll mode it polls Google
every pollIntervalMs around the morning window only (set pollWindowOnly: false to poll all day) and, on the morning wake, POSTs the agent once.
By default the Step-1 hook just nudges the agent. To make waking up do
something, shape the behavior on the agent side (the OpenClaw mapping or the
Hermes route prompt) — not in wakehook. wakehook only supplies the trigger; the
actual abilities (calendar, weather, messages, …) come from the agent's own
skills/tools — install/enable those separately.
For OpenClaw the lever is the mapping; for Hermes it's the route prompt (and
deliver:). OpenClaw mapping examples:
A — simple nudge, routine in the prompt (action: "wake"): make
textTemplate the instruction the main session executes.
{
match: { path: "wakehook" },
action: "wake",
wakeMode: "now",
textTemplate: "Good morning — the user just woke ({{wokeAt}}). Run the morning routine: review today's calendar, summarize overnight messages, and give the weather.",
}
B — full routine run on a dedicated agent (action: "agent"): route to an
agent that has the relevant skills, and deliver the result to a chat surface.
{
match: { path: "wakehook" },
action: "agent",
agentId: "morning", // an agent you've set up with calendar/weather/etc. skills
wakeMode: "now",
messageTemplate: "User woke at {{wokeAt}} (slept {{session.durationMin}} min). Produce the morning briefing.",
deliver: true, // send the result back to a chat surface…
channel: "last", // …e.g. Telegram/Discord (defaults to last used)
}
Note: you generally don't trigger a cron from this — wakehook is the real wake signal, so it replaces guessing a fixed time. Just run the task on the event. The capabilities themselves are separate agent skills; this step only wires when and what to ask for.
wakehook POSTs the raw, neutral event; the agent's mapping/route templates it:
POST http://<host>:<port>/<your-agent-route>
Content-Type: application/json
<auth header per Step 1: Authorization: Bearer … (OpenClaw) or X-Webhook-Signature: … (Hermes)>
{ "event": "user.awake", "wokeAt": "2026-06-13T08:04:00Z", "user": "self",
"source": "google-health",
"session": { "start": "...", "end": "2026-06-13T08:04:00Z", "durationMin": 464 } }
The agent turns this into a now wake with the templated text — handle that as
the trigger to run the morning routine. wakehook also sends X-Wake-Event-Id for
idempotency. Fires at most once per morning.
curl -X POST http://localhost:8080/test/replay \
-H 'Content-Type: application/json' -d '{}'
Injects a synthetic wake "now" and delivers it exactly like a real one — confirm the agent maps it and runs the routine.
Authorization bearer
must equal the gateway hooks.token, hooks enabled, and the mapping
exists. Hermes — secret must equal the route secret and signatureHeader /
signatureFormat must match (X-Webhook-Signature / hex), with the route
defined; or use INSECURE_NO_AUTH on loopback.wakehook-auth.hook mapping requires text → an action: "wake" mapping
must use textTemplate (only action: "agent" uses messageTemplate).action: "wake" is set.Source + full docs: https://github.com/robbeverhelst/wakehook