ClawTime — Local Installation with Cloudflare Tunnel
ClawTime is a private webchat UI connecting to the OpenClaw gateway via WebSocket. Features: passkey (Face ID/Touch ID) auth, Piper TTS voice, 3D avatar.
Why Cloudflare is required: WebAuthn (passkeys) need HTTPS on a real domain.
http://localhost only works on the same machine — not from a phone on your network.
Architecture
iPhone/Browser → https://portal.yourdomain.com → Cloudflare Tunnel → localhost:3000 (ClawTime) → ws://127.0.0.1:18789 (OpenClaw Gateway)
Prerequisites
- Node.js v22+
cloudflaredCLI:brew install cloudflared- A domain with DNS on Cloudflare (free tier works)
- OpenClaw running:
openclaw status - (Optional) Piper TTS + ffmpeg for voice
Installation Steps
1. Clone & install
cd ~/Projects
git clone https://github.com/youngkent/clawtime.git
cd clawtime
npm install --legacy-peer-deps
2. Set up Cloudflare Tunnel
# Login to Cloudflare
cloudflared tunnel login
# Create named tunnel
cloudflared tunnel create clawtime
# Configure routing
# Edit ~/.cloudflared/config.yml:
~/.cloudflared/config.yml:
tunnel: clawtime
credentials-file: /Users/YOUR_USER/.cloudflared/<tunnel-id>.json
ingress:
- hostname: portal.yourdomain.com
service: http://localhost:3000
- service: http_status:404
Then in Cloudflare DNS dashboard: add a CNAME record:
- Name:
portal→ Target:<tunnel-id>.cfargotunnel.com(Proxied ✅)
3. Configure OpenClaw gateway
The gateway must whitelist ClawTime's origin:
openclaw config patch '{"gateway":{"controlUi":{"allowedOrigins":["https://portal.yourdomain.com"]}}}'
openclaw gateway restart
⚠️ PUBLIC_URL must match this origin exactly — it's used as the WebSocket origin header for device auth.
4. Start ClawTime server
Minimum (no TTS):
cd ~/Projects/clawtime
PUBLIC_URL=https://portal.yourdomain.com \
SETUP_TOKEN=<your-setup-token> \
GATEWAY_TOKEN=<gateway-token> \
node server.js
With Piper TTS:
cd ~/Projects/clawtime
PUBLIC_URL=https://portal.yourdomain.com \
SETUP_TOKEN=<your-setup-token> \
GATEWAY_TOKEN=<gateway-token> \
BOT_NAME="Beware" \
BOT_EMOJI="🌀" \
TTS_COMMAND='python3 -m piper --data-dir ~/Documents/resources/piper-voices -m en_US-kusal-medium -f /tmp/clawtime-tts-tmp.wav -- {{TEXT}} && ffmpeg -y -loglevel error -i /tmp/clawtime-tts-tmp.wav {{OUTPUT}}' \
node server.js
⚠️ TTS Security Note: The {{TEXT}} placeholder is substituted into a shell command.
ClawTime's server must sanitize text before substitution to prevent command injection.
The server should strip or escape shell metacharacters (; | & $ \ ( ) { } < >) from user input before passing it to the TTS command. If you're modifying the TTS pipeline, use child_process.execFile()with argument arrays instead ofchild_process.exec()` with string
interpolation.
5. Start Cloudflare tunnel
cloudflared tunnel run clawtime
6. Register passkey (first time only)
- Open
https://portal.yourdomain.com/?setup=<your-setup-token>in Safari - Follow the passkey (Face ID / Touch ID) prompt
- ❌ Do NOT use private/incognito mode — Safari blocks passkeys there
- ❌ Do NOT use Chrome on iOS — use Safari
After registration, access ClawTime at https://portal.yourdomain.com.
Environment Variables
| Variable | Required | Description |
|---|---|---|
PUBLIC_URL | ✅ | Public HTTPS URL (must match allowedOrigins in gateway config) |
GATEWAY_TOKEN | ✅ | OpenClaw gateway auth token |
SETUP_TOKEN | For registration | Passphrase for ?setup=<token> passkey registration URL |
TTS_COMMAND | For voice | Piper command with {{TEXT}} and {{OUTPUT}} placeholders |
BOT_NAME | No | Display name (default: "Beware") |
BOT_EMOJI | No | Avatar emoji (default: "🌀") |
PORT | No | Server port (default: 3000) |
Storing Tokens Securely (recommended)
Instead of passing tokens as plaintext env vars or in plist files, store them in macOS Keychain:
# Store tokens in Keychain
security add-generic-password -s "clawtime-gateway-token" -a "$(whoami)" -w "YOUR_GATEWAY_TOKEN"
security add-generic-password -s "clawtime-setup-token" -a "$(whoami)" -w "YOUR_SETUP_TOKEN"
Then retrieve them at launch time:
GATEWAY_TOKEN=$(security find-generic-password -s "clawtime-gateway-token" -a "$(whoami)" -w) \
SETUP_TOKEN=$(security find-generic-password -s "clawtime-setup-token" -a "$(whoami)" -w) \
PUBLIC_URL=https://portal.yourdomain.com \
node server.js
This avoids storing secrets in plaintext on disk.
Device Authentication (Critical)
ClawTime authenticates with the OpenClaw gateway using Ed25519 keypair auth.
This is where most installs break — see details in references/device-auth.md.
Quick summary:
- Keypair auto-generated in
~/.clawtime/device-key.jsonon first run - Device ID = SHA-256 of raw 32-byte Ed25519 pubkey (NOT the full SPKI-encoded key)
- Signature payload format:
v2|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce - If device auth fails → delete
~/.clawtime/device-key.jsonand restart
Auto-Start on Boot (macOS launchd)
See references/launchd.md for plist templates for both the server and tunnel.
Managing Services
# Stop server
pkill -f "node server.js"
# Stop tunnel
pkill -f "cloudflared"
# View logs (if backgrounded)
tail -f /tmp/clawtime.log
tail -f /tmp/cloudflared.log
# Restart after code/config changes
pkill -9 -f "node server.js"; sleep 2; # then re-run start command
Getting the Gateway Token
# From macOS Keychain
security find-generic-password -s "openclaw-gateway-token" -a "$(whoami)" -w
# From config file
cat ~/.openclaw/openclaw.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('gateway',{}).get('token',''))"
Passkey Operations
# Reset passkeys (re-register from scratch)
echo '[]' > ~/.clawtime/credentials.json
# Restart server, then visit /?setup=<token>
# Reset device key (new keypair on next restart)
rm ~/.clawtime/device-key.json
Troubleshooting
See references/troubleshooting.md for all common errors and fixes.
See references/device-auth.md for deep-dive on gateway auth issues.