openclaw-studio-dashboard
Expert in OpenClaw Studio - web dashboard for managing OpenClaw Gateway, agents, chat, approvals, and jobs
How do I install this agent skill?
npx skills add https://github.com/reason-machines/hermes-skills --skill openclaw-studio-dashboardIs this agent skill safe to install?
- Gen Agent Trust Hubfail
The skill facilitates the setup and management of the OpenClaw Studio dashboard. It involves downloading and executing code from external, untrusted sources via npx and git clone. Additionally, it accesses sensitive configuration files containing authentication tokens and includes features that could be vulnerable to indirect prompt injection.
- Socketpass
No alerts
- Snykwarn
Risk: MEDIUM · 1 issue
What does this agent skill do?
OpenClaw Studio Dashboard
Skill by ara.so — Hermes Skills collection.
OpenClaw Studio is a clean web dashboard for OpenClaw Gateway that provides a unified interface to connect gateways, manage agents, chat, handle approvals, and configure jobs. Built with TypeScript/Next.js, it runs a server-owned control plane architecture with SSE streaming for real-time runtime events.
What OpenClaw Studio Does
- Gateway Connection: Connect to local or remote OpenClaw Gateway instances via WebSocket
- Agent Management: Create, configure, and monitor AI agents with tool policies and sandbox settings
- Chat Interface: Stream conversations with PI agents including tool calls and thinking traces
- Approval Workflows: Manage exec approvals and runtime permissions
- Job Configuration: Set up and monitor cron jobs
- Runtime Streaming: Real-time event streaming over SSE with replay/history
Installation & Startup
Quick Start (Recommended)
# Run latest version with npx
npx -y openclaw-studio@latest
# Opens on http://localhost:3000
# Default gateway URL: ws://localhost:18789
From Source
git clone https://github.com/grp06/openclaw-studio.git
cd openclaw-studio
npm install
npm run dev
Setup Helper
# Configure gateway URL/token without opening UI
npm run studio:setup
Connection Architecture
OpenClaw Studio uses a two-path architecture:
- Browser → Studio: HTTP + SSE (
/api/runtime/*,/api/intents/*) - Studio → Gateway: Server-owned WebSocket to upstream OpenClaw
Critical concept: ws://localhost:18789 means "gateway on the Studio host", not "gateway on your browser's machine".
Configuration Patterns
Deployment Scenarios
A. Both Local (Same Computer)
# Start Studio
npx -y openclaw-studio@latest
cd openclaw-studio
npm run dev
# Open http://localhost:3000
# In Studio UI:
# - Upstream URL: ws://localhost:18789
# - Upstream Token: <your-gateway-token>
Get gateway token:
openclaw config get gateway.auth.token
B. Gateway in Cloud, Studio Local
Option 1: Tailscale Serve (Recommended)
On gateway host:
tailscale serve --yes --bg --https 443 http://127.0.0.1:18789
In Studio (local laptop):
- Upstream URL:
wss://<gateway-host>.ts.net - Upstream Token:
<gateway-token>
Option 2: SSH Tunnel
From laptop:
ssh -L 18789:127.0.0.1:18789 user@<gateway-host>
In Studio:
- Upstream URL:
ws://localhost:18789 - Upstream Token:
<gateway-token>
C. Both in Cloud (Always-On)
On Studio VPS:
# Start Studio
npx -y openclaw-studio@latest
cd openclaw-studio
npm install
npm run dev
# If OpenClaw is on same VPS:
# - Upstream URL: ws://localhost:18789
# - Upstream Token: <gateway-token>
# Expose Studio over Tailscale
tailscale serve --yes --bg --https 443 http://127.0.0.1:3000
# Access from anywhere: https://<studio-host>.ts.net
Environment Variables
# Gateway connection (optional, can set in UI)
NEXT_PUBLIC_GATEWAY_URL=ws://localhost:18789
# Studio access control (required for public binds)
STUDIO_ACCESS_TOKEN=your-secure-token-here
# OpenClaw state directory
OPENCLAW_STATE_DIR=~/.openclaw
# Bind host (default: 127.0.0.1)
HOST=0.0.0.0 # Public bind - requires STUDIO_ACCESS_TOKEN
Configuration Files
Studio settings are stored in ~/.openclaw/openclaw-studio/:
~/.openclaw/
├── openclaw.json # OpenClaw Gateway config
└── openclaw-studio/
├── settings.json # Gateway URL/token
└── runtime.db # Control-plane runtime DB
API & Key Commands
Studio Setup
# Development mode with auto-restart
npm run dev
# Production build
npm run build
npm run start
# Turbo dev mode
npm run dev:turbo
# Verify/repair native dependencies
npm run verify:native-runtime:repair
npm run verify:native-runtime:check
TypeScript API Patterns
Connecting to Gateway
// In Studio UI components
import { useGatewayConnection } from '@/hooks/useGatewayConnection';
export function DashboardPage() {
const { connected, connect, disconnect, status } = useGatewayConnection();
const handleConnect = async () => {
try {
await connect({
url: 'ws://localhost:18789',
token: process.env.GATEWAY_TOKEN
});
} catch (error) {
console.error('Connection failed:', error);
}
};
return (
<div>
<button onClick={handleConnect} disabled={connected}>
{connected ? 'Connected' : 'Connect'}
</button>
<span>Status: {status}</span>
</div>
);
}
Streaming Runtime Events
// Server-side SSE streaming
import { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
// Subscribe to runtime events
const subscription = runtimeStore.subscribe((event) => {
const data = `data: ${JSON.stringify(event)}\n\n`;
controller.enqueue(encoder.encode(data));
});
// Replay history
const history = await runtimeStore.getHistory();
for (const event of history) {
const data = `data: ${JSON.stringify(event)}\n\n`;
controller.enqueue(encoder.encode(data));
}
return () => subscription.unsubscribe();
}
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
});
}
Creating Agents
// Agent creation API
interface AgentConfig {
name: string;
systemPrompt: string;
toolPolicy: 'all' | 'allowlist' | 'denylist';
allowedTools?: string[];
sandboxEnabled: boolean;
execApprovalRequired: boolean;
}
export async function createAgent(config: AgentConfig) {
const response = await fetch('/api/intents/agent-create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent_name: config.name,
system_prompt: config.systemPrompt,
tool_policy: config.toolPolicy,
allowed_tools: config.allowedTools,
sandbox_config: {
enabled: config.sandboxEnabled,
mounts: config.sandboxEnabled ? [
{ host: '/tmp', container: '/workspace', readonly: false }
] : []
},
exec_approval_required: config.execApprovalRequired
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to create agent');
}
return response.json();
}
Chat with Streaming
// Client-side chat with SSE
import { useEffect, useState } from 'react';
export function ChatInterface({ agentId }: { agentId: string }) {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [thinking, setThinking] = useState<string[]>([]);
useEffect(() => {
// Connect to runtime stream
const eventSource = new EventSource('/api/runtime/stream');
eventSource.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'tool_call':
setMessages(prev => [...prev, {
role: 'assistant',
content: `Using tool: ${data.tool_name}`,
metadata: { type: 'tool', ...data }
}]);
break;
case 'thinking':
setThinking(prev => [...prev, data.content]);
break;
case 'transcript':
setMessages(prev => [...prev, {
role: data.role,
content: data.content
}]);
setThinking([]);
break;
}
});
return () => eventSource.close();
}, [agentId]);
const sendMessage = async (content: string) => {
await fetch('/api/intents/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent_id: agentId,
message: content
})
});
};
return (
<div>
{messages.map((msg, i) => (
<div key={i} className={`message ${msg.role}`}>
{msg.content}
</div>
))}
{thinking.length > 0 && (
<div className="thinking">
{thinking.map((t, i) => <p key={i}>{t}</p>)}
</div>
)}
<input onKeyPress={(e) => {
if (e.key === 'Enter') {
sendMessage(e.currentTarget.value);
e.currentTarget.value = '';
}
}} />
</div>
);
}
Real-World Examples
Example 1: Local Development Setup
#!/bin/bash
# setup-local-dev.sh
# Start OpenClaw Gateway (in separate terminal)
# openclaw gateway start
# Get gateway token
GATEWAY_TOKEN=$(openclaw config get gateway.auth.token)
# Start Studio
npx -y openclaw-studio@latest
cd openclaw-studio
# Configure via environment
export NEXT_PUBLIC_GATEWAY_URL=ws://localhost:18789
npm run dev
# Open http://localhost:3000
# Enter gateway token in UI
Example 2: Production Cloud Setup
#!/bin/bash
# deploy-studio-cloud.sh
# On your VPS running OpenClaw Gateway
cd /opt
npx -y openclaw-studio@latest
cd openclaw-studio
# Install dependencies
npm install
# Build for production
npm run build
# Set access token for public access
export STUDIO_ACCESS_TOKEN=$(openssl rand -hex 32)
# Expose over Tailscale
tailscale serve --yes --bg --https 443 http://127.0.0.1:3000
# Run production server
npm run start
echo "Studio available at: https://$(tailscale status --json | jq -r '.Self.DNSName')/"
echo "Access token: $STUDIO_ACCESS_TOKEN"
echo "Use: https://your-host.ts.net/?access_token=$STUDIO_ACCESS_TOKEN"
Example 3: Agent with Sandbox & Approvals
// create-secure-agent.ts
import { createAgent } from './lib/agent-api';
async function createSecureCodeAgent() {
const agent = await createAgent({
name: 'secure-code-assistant',
systemPrompt: `You are a secure code assistant.
Always ask before executing commands.
Work within the sandboxed environment only.`,
toolPolicy: 'allowlist',
allowedTools: [
'bash',
'read_file',
'write_file',
'search_files'
],
sandboxEnabled: true,
execApprovalRequired: true
});
console.log('Agent created:', agent.id);
return agent;
}
// Usage
createSecureCodeAgent().catch(console.error);
Example 4: Cron Job Configuration
// configure-cron-job.ts
interface CronJobConfig {
agentId: string;
schedule: string;
task: string;
enabled: boolean;
}
async function createCronJob(config: CronJobConfig) {
const response = await fetch('/api/intents/job-create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent_id: config.agentId,
schedule: config.schedule,
task_prompt: config.task,
enabled: config.enabled
})
});
return response.json();
}
// Schedule daily backup
await createCronJob({
agentId: 'agent-123',
schedule: '0 0 * * *', // Daily at midnight
task: 'Create backup of project files and upload to storage',
enabled: true
});
Common Patterns
Pattern 1: Conditional Gateway URL
// lib/gateway-config.ts
export function getGatewayUrl(): string {
// Check environment variable first
if (process.env.NEXT_PUBLIC_GATEWAY_URL) {
return process.env.NEXT_PUBLIC_GATEWAY_URL;
}
// Check saved settings
const settings = loadStudioSettings();
if (settings?.gatewayUrl) {
return settings.gatewayUrl;
}
// Default to localhost
return 'ws://localhost:18789';
}
export function isSecureConnection(url: string): boolean {
return url.startsWith('wss://');
}
Pattern 2: Access Token Management
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const requiredToken = process.env.STUDIO_ACCESS_TOKEN;
// No token required for loopback-only binds
if (!requiredToken) {
return NextResponse.next();
}
// Check cookie first
const cookieToken = request.cookies.get('studio_access_token')?.value;
if (cookieToken === requiredToken) {
return NextResponse.next();
}
// Check query param (for initial setup)
const queryToken = request.nextUrl.searchParams.get('access_token');
if (queryToken === requiredToken) {
const response = NextResponse.next();
response.cookies.set('studio_access_token', requiredToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 30 // 30 days
});
return response;
}
return new NextResponse('Unauthorized', { status: 401 });
}
export const config = {
matcher: ['/api/:path*', '/dashboard/:path*']
};
Pattern 3: Runtime Event Handling
// lib/runtime-store.ts
import Database from 'better-sqlite3';
export class RuntimeStore {
private db: Database.Database;
private subscribers: Set<(event: RuntimeEvent) => void>;
constructor(dbPath: string) {
this.db = new Database(dbPath);
this.subscribers = new Set();
this.initDb();
}
private initDb() {
this.db.exec(`
CREATE TABLE IF NOT EXISTS runtime_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT NOT NULL,
agent_id TEXT,
payload TEXT NOT NULL,
timestamp INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_agent_timestamp
ON runtime_events(agent_id, timestamp);
`);
}
async storeEvent(event: RuntimeEvent) {
const stmt = this.db.prepare(`
INSERT INTO runtime_events (event_type, agent_id, payload, timestamp)
VALUES (?, ?, ?, ?)
`);
stmt.run(
event.type,
event.agentId,
JSON.stringify(event),
Date.now()
);
// Notify subscribers
this.subscribers.forEach(fn => fn(event));
}
async getHistory(agentId?: string, limit = 100): Promise<RuntimeEvent[]> {
const query = agentId
? `SELECT payload FROM runtime_events
WHERE agent_id = ?
ORDER BY timestamp DESC LIMIT ?`
: `SELECT payload FROM runtime_events
ORDER BY timestamp DESC LIMIT ?`;
const stmt = this.db.prepare(query);
const rows = agentId
? stmt.all(agentId, limit)
: stmt.all(limit);
return rows.map(row => JSON.parse(row.payload)).reverse();
}
subscribe(callback: (event: RuntimeEvent) => void) {
this.subscribers.add(callback);
return {
unsubscribe: () => this.subscribers.delete(callback)
};
}
}
Troubleshooting
Connection Issues
Problem: UI loads but "Connect" fails
# Check gateway is running
openclaw gateway status
# Verify gateway URL in Studio settings
cat ~/.openclaw/openclaw-studio/settings.json
# If Studio is remote, remember localhost means Studio host
# Use Tailscale Serve or SSH tunnel if needed
Problem: EPROTO / "wrong version number"
// Wrong: using wss:// to non-TLS endpoint
const url = 'wss://localhost:18789'; // ❌
// Right: use ws:// for plain HTTP
const url = 'ws://localhost:18789'; // ✅
// Right: use wss:// for Tailscale HTTPS
const url = 'wss://gateway.ts.net'; // ✅
Problem: Assets 404 under /studio
# Studio must be served at root path
# If you need /studio path:
# 1. Set basePath in next.config.js
# 2. Rebuild Studio
# next.config.js
module.exports = {
basePath: '/studio',
// ...
}
Native Module Issues
Problem: better_sqlite3.node / NODE_MODULE_VERSION mismatch
# Auto-repair (recommended)
npm run verify:native-runtime:repair
# Manual fix
npm rebuild better-sqlite3
npm install
# Check node/npm versions match
node -v && node -p "process.versions.modules"
which node && which npm
# If using nvm, ensure same node version
nvm use 20
npm install
Problem: SQLite errors on startup
# Check permissions on state directory
ls -la ~/.openclaw/openclaw-studio/
# Reset runtime DB if corrupted
rm ~/.openclaw/openclaw-studio/runtime.db
npm run dev
Access Token Issues
Problem: 401 "Studio access token required"
# Set access token for public binds
export STUDIO_ACCESS_TOKEN=your-secure-token
# Access UI with token once to set cookie
curl https://studio.ts.net/?access_token=your-secure-token
# Or set in browser
# https://studio.ts.net/?access_token=your-secure-token
Debugging Connection Path
// debug-connection.ts
export function debugConnection() {
console.log('Studio runtime environment:');
console.log('- Node version:', process.version);
console.log('- Platform:', process.platform);
console.log('- CWD:', process.cwd());
console.log('- State dir:', process.env.OPENCLAW_STATE_DIR || '~/.openclaw');
const isServer = typeof window === 'undefined';
console.log('- Running on:', isServer ? 'server' : 'browser');
if (isServer) {
const os = require('os');
console.log('- Hostname:', os.hostname());
console.log('- Network interfaces:',
Object.keys(os.networkInterfaces()));
}
console.log('\nGateway connection:');
console.log('- URL:', process.env.NEXT_PUBLIC_GATEWAY_URL || 'ws://localhost:18789');
console.log('- Token set:', !!process.env.GATEWAY_TOKEN);
console.log('- Access token required:', !!process.env.STUDIO_ACCESS_TOKEN);
}
Key Files Reference
openclaw-studio/
├── app/
│ ├── api/
│ │ ├── runtime/
│ │ │ └── stream/route.ts # SSE streaming endpoint
│ │ └── intents/
│ │ ├── agent-create/route.ts
│ │ ├── chat/route.ts
│ │ └── job-create/route.ts
│ ├── dashboard/
│ │ ├── page.tsx # Main dashboard
│ │ └── agents/page.tsx
│ └── layout.tsx
├── lib/
│ ├── runtime-store.ts # SQLite runtime DB
│ ├── gateway-client.ts # WebSocket client
│ └── settings.ts # Settings persistence
├── hooks/
│ └── useGatewayConnection.ts
├── docs/
│ ├── ui-guide.md
│ ├── pi-chat-streaming.md
│ ├── permissions-sandboxing.md
│ └── color-system.md
├── next.config.js
├── package.json
└── ARCHITECTURE.md
Additional Resources
- UI Guide:
docs/ui-guide.md- Agent creation, cron jobs, exec approvals - PI Chat Streaming:
docs/pi-chat-streaming.md- SSE event handling, replay, tool calls - Permissions:
docs/permissions-sandboxing.md- Tool policies, sandbox config, approval flows - Architecture:
ARCHITECTURE.md- Modules and data flow - Discord: https://discord.gg/EFkFHbZw
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/reason-machines/hermes-skills/openclaw-studio-dashboard">View openclaw-studio-dashboard on skillZs</a>