OpenClaw Nerve Cockpit
Skill by ara.so — Hermes Skills collection.
Nerve is a real-time web cockpit for OpenClaw that transforms agent interaction from chat-only to full operational control. It provides voice conversations, multi-agent fleet management, automated kanban boards, workspace/file control, sub-agent sessions, inline charts, and comprehensive usage visibility.
What Nerve Does
Nerve sits between your browser and the OpenClaw Gateway, providing:
- Fleet Control: Manage multiple agents from one interface, each with its own workspace, memory, identity, and skills
- Voice Interface: Push-to-talk, wake word, local Whisper transcription, multilingual commands, multiple TTS providers
- Full Context: Live workspace browser, tabbed editor, memory editing, config editing, skills inspection
- Operations Layer: Session trees, cron scheduling, kanban task boards, review flows, proposal inbox, model overrides
- Rich Output: Charts, diffs, previews, syntax-highlighted code, structured tool rendering, streaming UI
- Observability: Token usage, cost tracking, context meter, agent logs, event logs
Architecture
Browser ─── Nerve (:3080) ─── OpenClaw Gateway (:18789)
│ │
├─ WS ──────┤ proxied to gateway
├─ SSE ─────┤ file watchers, real-time sync
└─ REST ────┘ files, memories, TTS, models
Frontend: React 19, Tailwind CSS 4, shadcn/ui, Vite 7
Backend: Hono 4 on Node.js 22+
Installation
Quick Install (Recommended)
curl -fsSL https://raw.githubusercontent.com/daggerhashimoto/openclaw-nerve/master/install.sh | bash
The installer handles dependencies, clone, build, and launches the setup wizard with guided access modes (localhost, LAN, Tailscale tailnet IP, Tailscale Serve).
Manual Install
git clone https://github.com/daggerhashimoto/openclaw-nerve.git
cd openclaw-nerve
npm install
npm run setup
npm run prod
Install from Next Branch (Latest Features)
curl -fsSL https://raw.githubusercontent.com/daggerhashimoto/openclaw-nerve/master/install.sh | bash -s -- --branch next
Or switch existing install:
cd ~/nerve
git fetch origin
git switch next || git switch -c next --track origin/next
git pull --ff-only
npm install
npm run build
npm run prod
Key Commands
# Development
npm run dev # Start frontend (Vite on :3080)
PORT=3081 npm run dev:server # Start backend (explicit split-port dev)
# Production
npm run prod # Build and run production server
# Management
npm run setup # Interactive setup wizard
npm run update -- --yes # Auto-update with rollback on failure
npm run build # Build frontend and backend
# Utilities
npm run lint # Lint TypeScript files
npm run type-check # TypeScript type checking
Configuration
Nerve uses a .env file in the project root. Key variables:
Core Settings
# Server binding
HOST=127.0.0.1 # Bind to localhost (default)
# HOST=0.0.0.0 # Bind to all interfaces (enables network access)
PORT=3080 # Server port
# OpenClaw Gateway
GATEWAY_URL=http://localhost:18789
GATEWAY_TOKEN= # Optional: if gateway requires auth
# Security (required when HOST=0.0.0.0)
PASSWORD_HASH= # bcrypt hash of UI password
SESSION_SECRET= # Random string for session signing (32+ chars)
TRUSTED_CONNECTION=false # Set true for server-side gateway token injection
Voice Settings
# Voice input
WHISPER_MODEL_PATH=/path/to/whisper/model # Local Whisper model
WAKE_WORD=nerve # Wake word for voice activation
LANGUAGE=en # Transcription language
# TTS Provider (choose one)
TTS_PROVIDER=openai # Options: openai, elevenlabs, edge
OPENAI_API_KEY= # Required if TTS_PROVIDER=openai
ELEVENLABS_API_KEY= # Required if TTS_PROVIDER=elevenlabs
TTS_VOICE=alloy # Voice ID for chosen provider
Advanced Settings
# Multi-agent setup
AGENT_CONFIG_DIR=/path/to/agents # Directory with per-agent configs
# Performance
WS_PING_INTERVAL=30000 # WebSocket ping interval (ms)
FILE_WATCH_DEBOUNCE=300 # File watcher debounce (ms)
# Logging
LOG_LEVEL=info # Options: debug, info, warn, error
Generating PASSWORD_HASH
// scripts/hash-password.ts
import bcrypt from 'bcrypt';
const password = process.argv[2];
if (!password) {
console.error('Usage: tsx scripts/hash-password.ts <password>');
process.exit(1);
}
const hash = await bcrypt.hash(password, 10);
console.log(hash);
Run:
npx tsx scripts/hash-password.ts mypassword
Deployment Modes
Local (Default)
Run Nerve and Gateway on the same machine. Best for reliability and simplicity.
HOST=127.0.0.1
PORT=3080
GATEWAY_URL=http://localhost:18789
LAN Access
Expose Nerve on your local network.
HOST=0.0.0.0
PORT=3080
PASSWORD_HASH=<bcrypt_hash>
SESSION_SECRET=<random_32_char_string>
GATEWAY_URL=http://localhost:18789
Tailscale Access
Expose via Tailscale tailnet IP or Tailscale Serve.
# Via tailnet IP
HOST=0.0.0.0
PORT=3080
PASSWORD_HASH=<bcrypt_hash>
SESSION_SECRET=<random_32_char_string>
# Via Tailscale Serve (alternative)
HOST=127.0.0.1
PORT=3080
# Run: tailscale serve https / http://127.0.0.1:3080
Hybrid
Nerve local, Gateway in cloud.
HOST=127.0.0.1
PORT=3080
GATEWAY_URL=https://your-gateway.example.com
GATEWAY_TOKEN=<gateway_token>
Full Cloud
Both Nerve and Gateway in cloud.
HOST=0.0.0.0
PORT=3080
PASSWORD_HASH=<bcrypt_hash>
SESSION_SECRET=<random_32_char_string>
GATEWAY_URL=https://your-gateway.example.com
GATEWAY_TOKEN=<gateway_token>
Agent Markers for Rich Output
Nerve recognizes special markers in agent responses to render rich UI components.
Charts
{{CHART_START}}
{
"type": "line",
"data": {
"labels": ["Jan", "Feb", "Mar", "Apr"],
"datasets": [{
"label": "Revenue",
"data": [12000, 19000, 15000, 25000]
}]
}
}
{{CHART_END}}
Supported chart types: line, bar, pie, doughnut, radar, scatter
Kanban Tasks
{{KANBAN_START}}
{
"title": "Implement user authentication",
"description": "Add JWT-based auth to API endpoints",
"priority": "high",
"tags": ["security", "backend"],
"assignee": "agent-1"
}
{{KANBAN_END}}
TTS Control
{{TTS_START}}
This text will be read aloud using the configured TTS provider.
{{TTS_END}}
Code Diffs
{{DIFF_START}}
- const old = "previous version";
+ const updated = "new version";
{{DIFF_END}}
Code Examples
Creating a Custom Backend Route
// server/routes/custom.ts
import { Hono } from 'hono';
import type { AppContext } from '../types';
const custom = new Hono<AppContext>();
// Protected route (requires auth when HOST=0.0.0.0)
custom.get('/agent-status', async (c) => {
const gatewayUrl = c.env.GATEWAY_URL;
const token = c.env.GATEWAY_TOKEN;
const response = await fetch(`${gatewayUrl}/api/agents`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
const agents = await response.json();
return c.json({
total: agents.length,
active: agents.filter(a => a.status === 'active').length,
agents,
});
});
export default custom;
Register in server/index.ts:
import custom from './routes/custom';
app.route('/api/custom', custom);
Adding a Frontend Component
// src/components/AgentStatusCard.tsx
import { useEffect, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
interface AgentStatus {
total: number;
active: number;
}
export function AgentStatusCard() {
const [status, setStatus] = useState<AgentStatus | null>(null);
useEffect(() => {
const fetchStatus = async () => {
const response = await fetch('/api/custom/agent-status');
const data = await response.json();
setStatus(data);
};
fetchStatus();
const interval = setInterval(fetchStatus, 5000);
return () => clearInterval(interval);
}, []);
if (!status) return <div>Loading...</div>;
return (
<Card>
<CardHeader>
<CardTitle>Agent Fleet Status</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{status.active}/{status.total}</div>
<p className="text-sm text-muted-foreground">Active agents</p>
</CardContent>
</Card>
);
}
WebSocket Event Handling
// src/hooks/useGatewayWebSocket.ts
import { useEffect, useRef, useState } from 'react';
interface GatewayMessage {
type: string;
payload: unknown;
}
export function useGatewayWebSocket(agentId: string) {
const [messages, setMessages] = useState<GatewayMessage[]>([]);
const wsRef = useRef<WebSocket | null>(null);
useEffect(() => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${window.location.host}/ws/gateway`);
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'subscribe', agentId }));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages((prev) => [...prev, message]);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
wsRef.current = ws;
return () => {
ws.close();
};
}, [agentId]);
return { messages, ws: wsRef.current };
}
Server-Side Event Streaming
// server/routes/events.ts
import { Hono } from 'hono';
import { streamSSE } from 'hono/streaming';
import type { AppContext } from '../types';
const events = new Hono<AppContext>();
events.get('/file-changes', async (c) => {
return streamSSE(c, async (stream) => {
const watcher = watchWorkspace(c.env.WORKSPACE_PATH);
watcher.on('change', (file) => {
stream.writeSSE({
data: JSON.stringify({ type: 'file_changed', file }),
});
});
// Keep connection alive
const ping = setInterval(() => {
stream.writeSSE({ data: 'ping' });
}, 30000);
// Cleanup on disconnect
stream.onAbort(() => {
clearInterval(ping);
watcher.close();
});
});
});
export default events;
Custom TTS Provider Integration
// server/services/tts/custom-provider.ts
import type { TTSProvider, TTSOptions } from './types';
export class CustomTTSProvider implements TTSProvider {
private apiKey: string;
private endpoint: string;
constructor(apiKey: string, endpoint: string) {
this.apiKey = apiKey;
this.endpoint = endpoint;
}
async synthesize(text: string, options: TTSOptions): Promise<Buffer> {
const response = await fetch(this.endpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
text,
voice: options.voice || 'default',
language: options.language || 'en',
}),
});
if (!response.ok) {
throw new Error(`TTS failed: ${response.statusText}`);
}
return Buffer.from(await response.arrayBuffer());
}
getSupportedVoices(): string[] {
return ['voice-1', 'voice-2', 'voice-3'];
}
}
Common Patterns
Multi-Agent Setup
Create agent-specific config files:
mkdir -p ~/.config/nerve/agents
// ~/.config/nerve/agents/researcher.json
{
"id": "researcher",
"name": "Research Agent",
"workspace": "/path/to/research-workspace",
"memory": "/path/to/research-memory.json",
"identity": "You are a research specialist focused on gathering and analyzing information.",
"skills": ["web-search", "data-analysis", "summarization"],
"model": "claude-3-5-sonnet-20241022"
}
# In .env
AGENT_CONFIG_DIR=/home/user/.config/nerve/agents
Cron Task Scheduling
// Add to agent identity or system message
"You can schedule recurring tasks using cron syntax. Example:
{{CRON_START}}
{
\"schedule\": \"0 9 * * *\",
\"task\": \"Generate daily report\",
\"agent\": \"researcher\"
}
{{CRON_END}}"
Review Flow Integration
// Agent proposes changes for review
"{{PROPOSAL_START}}
{
\"type\": \"code_change\",
\"file\": \"src/app.ts\",
\"description\": \"Refactor authentication logic\",
\"diff\": \"...\"
}
{{PROPOSAL_END}}"
Context Pressure Monitoring
// src/hooks/useContextMeter.ts
export function useContextMeter(sessionId: string) {
const [context, setContext] = useState({
used: 0,
limit: 200000,
percentage: 0,
});
useEffect(() => {
const checkContext = async () => {
const response = await fetch(`/api/sessions/${sessionId}/context`);
const data = await response.json();
setContext({
used: data.tokens,
limit: data.limit,
percentage: (data.tokens / data.limit) * 100,
});
};
checkContext();
const interval = setInterval(checkContext, 10000);
return () => clearInterval(interval);
}, [sessionId]);
return context;
}
Troubleshooting
Gateway Connection Issues
# Check gateway is running
curl http://localhost:18789/health
# Verify Nerve can reach gateway
# Check GATEWAY_URL in .env
# If using GATEWAY_TOKEN, verify it's correct
WebSocket Connection Fails
// Check browser console for:
// - CORS errors (ensure gateway allows origin)
// - Authentication failures (check PASSWORD_HASH)
// - Network tab shows 101 Switching Protocols
// Server-side debugging:
// Set LOG_LEVEL=debug in .env
// Check server/logs/websocket.log
Voice Transcription Not Working
# Verify Whisper model path
ls -la $WHISPER_MODEL_PATH
# Check microphone permissions in browser
# Chrome: Settings > Privacy and security > Site Settings > Microphone
# Test with different model sizes
# WHISPER_MODEL_PATH=/path/to/ggml-base.en.bin # Faster
# WHISPER_MODEL_PATH=/path/to/ggml-large-v3.bin # More accurate
File Watcher Not Updating
# Increase OS file watch limit (Linux)
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
# Adjust debounce in .env
FILE_WATCH_DEBOUNCE=500 # Increase if updates too frequent
Authentication Failures (HOST=0.0.0.0)
# Regenerate password hash
npx tsx scripts/hash-password.ts newpassword
# Update .env with new hash
PASSWORD_HASH=<new_hash>
# Ensure SESSION_SECRET is set and >= 32 chars
SESSION_SECRET=$(openssl rand -base64 32)
Update Fails to Roll Back
# Manual rollback
cd ~/nerve
git reflog # Find previous commit
git reset --hard HEAD@{1}
npm install
npm run build
npm run prod
Port Already in Use
# Find process using port
lsof -i :3080
# or
netstat -tuln | grep 3080
# Kill process
kill -9 <PID>
# Or use different port
PORT=3081 npm run prod
Resources
- Documentation: docs/
- Architecture: docs/ARCHITECTURE.md
- Security: docs/SECURITY.md
- Discord: discord.gg/Sh9ZGtctva
- Homepage: nerve.zone