skillZs
★ LIVE SKILL TAGS ★
>>> LIVE SKILLS INDEX <<<
* OPEN SOURCE *
NO LOGIN, NO TRACKING
※ REAL INSTALL DATA ※
← back to all skills
reason-machines/hermes-skills150 installs

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-dashboard
view source ↗

Is 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:

  1. Browser → Studio: HTTP + SSE (/api/runtime/*, /api/intents/*)
  2. 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

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>