openclaw-control-center

Local-first, security-first control center for OpenClaw agents — visibility dashboard with readonly defaults, token attribution, collaboration tracing, and…

INSTALLATION
npx skills add https://github.com/aradotso/trending-skills --skill openclaw-control-center
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

git clone https://github.com/TianyiDataScience/openclaw-control-center.git

cd openclaw-control-center

npm install

cp .env.example .env

npm run build

npm test

npm run smoke:ui

npm run dev:ui

Open:

  • http://127.0.0.1:4310/?section=overview&lang=zh
  • http://127.0.0.1:4310/?section=overview&lang=en

Use npm run dev:ui over UI_MODE=true npm run dev — more stable, especially on Windows shells.

Project Structure

openclaw-control-center/

├── control-center/          # All modifications must stay within this directory

│   ├── src/

│   │   ├── runtime/         # Core runtime, connectors, monitors

│   │   └── ui/              # Frontend UI components

│   ├── .env.example

│   └── package.json

├── docs/

│   └── assets/              # Screenshots and documentation images

├── README.md

└── README.en.md

Critical constraint: Only modify files inside control-center/. Never modify ~/.openclaw/openclaw.json.

Environment Configuration

Copy .env.example to .env and configure:

# Security defaults — do NOT change without understanding implications

READONLY_MODE=true

LOCAL_TOKEN_AUTH_REQUIRED=true

IMPORT_MUTATION_ENABLED=false

IMPORT_MUTATION_DRY_RUN=false

APPROVAL_ACTIONS_ENABLED=false

APPROVAL_ACTIONS_DRY_RUN=true

# Connection

OPENCLAW_GATEWAY_URL=http://127.0.0.1:PORT

OPENCLAW_HOME=~/.openclaw

# UI

PORT=4310

DEFAULT_LANG=zh

Security Flag Meanings

Flag

Default

Effect

READONLY_MODE

true

All state-changing endpoints disabled

LOCAL_TOKEN_AUTH_REQUIRED

true

Import/export and write APIs require local token

IMPORT_MUTATION_ENABLED

false

Import mutations blocked entirely

IMPORT_MUTATION_DRY_RUN

false

Dry-run mode for imports when enabled

APPROVAL_ACTIONS_ENABLED

false

Approval actions hard-disabled

APPROVAL_ACTIONS_DRY_RUN

true

Approval actions run as dry-run when enabled

Key Commands

# Development

npm run dev:ui          # Start UI server (recommended)

npm run dev             # One-shot monitor run, no HTTP UI

# Build & Test

npm run build           # TypeScript compile

npm test                # Run test suite

npm run smoke:ui        # Smoke test the UI endpoints

# Lint

npm run lint            # ESLint check

npm run lint:fix        # Auto-fix lint issues

TypeScript Code Examples

Connecting to the Runtime Monitor

import { createMonitor } from './src/runtime/monitor';

const monitor = createMonitor({

  gatewayUrl: process.env.OPENCLAW_GATEWAY_URL ?? 'http://127.0.0.1:4310',

  readonlyMode: process.env.READONLY_MODE !== 'false',

  localTokenAuthRequired: process.env.LOCAL_TOKEN_AUTH_REQUIRED !== 'false',

});

// Fetch current system overview

const overview = await monitor.getOverview();

console.log(overview.systemStatus);      // 'healthy' | 'degraded' | 'critical'

console.log(overview.pendingItems);      // number

console.log(overview.activeAgents);      // Agent[]

Reading Agent Staff Status

import { StaffConnector } from './src/runtime/connectors/staff';

const staff = new StaffConnector({ gatewayUrl: process.env.OPENCLAW_GATEWAY_URL });

// Get agents actively executing (not just queued)

const activeAgents = await staff.getActiveAgents();

activeAgents.forEach(agent => {

  console.log(`${agent.name}: ${agent.status}`);

  // 'executing' | 'queued' | 'idle' | 'blocked'

  console.log(`Current task: ${agent.currentTask?.title ?? 'none'}`);

  console.log(`Last output: ${agent.lastOutput}`);

});

// Get the full staff roster including queue depth

const roster = await staff.getRoster();

Tracing Cross-Session Collaboration

import { CollaborationTracer } from './src/runtime/connectors/collaboration';

const tracer = new CollaborationTracer({ gatewayUrl: process.env.OPENCLAW_GATEWAY_URL });

// Get parent-child session handoffs

const handoffs = await tracer.getSessionHandoffs();

handoffs.forEach(handoff => {

  console.log(`${handoff.parentSession} → ${handoff.childSession}`);

  console.log(`Delegated task: ${handoff.taskTitle}`);

  console.log(`Status: ${handoff.status}`);

});

// Get verified cross-session messages (e.g. Main ⇄ Pandas)

const crossSessionMessages = await tracer.getCrossSessionMessages();

crossSessionMessages.forEach(msg => {

  console.log(`${msg.fromAgent} ⇄ ${msg.toAgent}: ${msg.messageType}`);

  // messageType: 'sessions_send' | 'inter-session message'

});

Fetching Token Usage and Spend

import { UsageConnector } from './src/runtime/connectors/usage';

const usage = new UsageConnector({ gatewayUrl: process.env.OPENCLAW_GATEWAY_URL });

// Today's usage

const today = await usage.getUsageSummary('today');

console.log(`Tokens used: ${today.tokensUsed}`);

console.log(`Cost: $${today.costUsd.toFixed(4)}`);

console.log(`Context pressure: ${today.contextPressure}`);

// contextPressure: 'low' | 'medium' | 'high' | 'critical'

// Usage trend over 7 days

const trend = await usage.getUsageTrend(7);

trend.forEach(day => {

  console.log(`${day.date}: ${day.tokensUsed} tokens, $${day.costUsd.toFixed(4)}`);

});

// Token attribution by task (who ate the scheduled task tokens)

const attribution = await usage.getTokenAttribution();

attribution.tasks.forEach(task => {

  console.log(`${task.title}: ${task.tokensUsed} (${task.percentOfTotal}%)`);

});

Reading Memory State

import { MemoryConnector } from './src/runtime/connectors/memory';

const memory = new MemoryConnector({

  openclawHome: process.env.OPENCLAW_HOME ?? '~/.openclaw',

});

// Get memory health per active agent (scoped to openclaw.json)

const memoryState = await memory.getMemoryState();

memoryState.agents.forEach(agent => {

  console.log(`${agent.name}:`);

  console.log(`  Available: ${agent.memoryAvailable}`);

  console.log(`  Searchable: ${agent.memorySearchable}`);

  console.log(`  Needs review: ${agent.needsReview}`);

});

// Read daily memory for an agent

const dailyMemory = await memory.readDailyMemory('main-agent');

console.log(dailyMemory.content);

// Edit memory (requires READONLY_MODE=false and valid local token)

await memory.writeDailyMemory('main-agent', updatedContent, { token: localToken });

Checking Wiring Status

import { WiringChecker } from './src/runtime/connectors/wiring';

const wiring = new WiringChecker({ gatewayUrl: process.env.OPENCLAW_GATEWAY_URL });

const status = await wiring.getWiringStatus();

status.connectors.forEach(connector => {

  console.log(`${connector.name}: ${connector.status}`);

  // status: 'connected' | 'partial' | 'disconnected'

  if (connector.status !== 'connected') {

    console.log(`  Fix: ${connector.nextStep}`);

  }

});

Approving Tasks (Gated Endpoint)

import { TaskConnector } from './src/runtime/connectors/tasks';

const tasks = new TaskConnector({

  gatewayUrl: process.env.OPENCLAW_GATEWAY_URL,

  approvalActionsEnabled: process.env.APPROVAL_ACTIONS_ENABLED === 'true',

  approvalActionsDryRun: process.env.APPROVAL_ACTIONS_DRY_RUN !== 'false',

});

// This throws if APPROVAL_ACTIONS_ENABLED=false (default)

try {

  const result = await tasks.approveTask('task-id-123', { token: localToken });

  if (result.dryRun) {

    console.log('Dry run — no actual state change');

  }

} catch (err) {

  if (err.code === 'APPROVAL_ACTIONS_DISABLED') {

    console.log('Set APPROVAL_ACTIONS_ENABLED=true to enable approvals');

  }

}

UI Section Navigation

Navigate via query params:

http://127.0.0.1:4310/?section=overview&lang=zh

http://127.0.0.1:4310/?section=usage&lang=en

http://127.0.0.1:4310/?section=staff&lang=zh

http://127.0.0.1:4310/?section=collaboration&lang=en

http://127.0.0.1:4310/?section=tasks&lang=zh

http://127.0.0.1:4310/?section=memory&lang=en

http://127.0.0.1:4310/?section=documents&lang=zh

http://127.0.0.1:4310/?section=settings&lang=en

Sections: overview | usage | staff | collaboration | tasks | memory | documents | settings

Languages: zh (Chinese, default) | en (English)

Integration Patterns

Embedding in an Existing OpenClaw Workflow

If your OpenClaw agent needs to hand off instructions to the control center for setup, use the documented install block:

// In your OpenClaw agent task

const installInstructions = `

cd openclaw-control-center

npm install

cp .env.example .env

# Edit .env: set OPENCLAW_GATEWAY_URL and OPENCLAW_HOME

npm run build && npm test && npm run dev:ui

`;

Adding a Custom Connector

All connectors live in control-center/src/runtime/connectors/. Follow this pattern:

// control-center/src/runtime/connectors/my-connector.ts

import { BaseConnector, ConnectorOptions } from './base';

export interface MyData {

  id: string;

  value: string;

}

export class MyConnector extends BaseConnector {

  constructor(options: ConnectorOptions) {

    super(options);

  }

  async getData(): Promise<MyData[]> {

    // Always check readonly mode for any write operation

    this.assertNotReadonly('getData is readonly-safe');

    const response = await this.fetch('/api/my-endpoint');

    return response.json() as Promise<MyData[]>;

  }

}

Custom UI Section

// control-center/src/ui/sections/MySection.tsx

import React from 'react';

import { useConnector } from '../hooks/useConnector';

import { MyConnector } from '../../runtime/connectors/my-connector';

export const MySection: React.FC = () => {

  const { data, loading, error } = useConnector(MyConnector, 'getData');

  if (loading) return <div>Loading...</div>;

  if (error) return <div>Error: {error.message}</div>;

  return (

    <ul>

      {data?.map(item => (

        <li key={item.id}>{item.value}</li>

      ))}

    </ul>

  );

};

Troubleshooting

"Missing src/runtime" or "Missing core source"

This almost always means the working directory is wrong:

# Ensure you're in the repo root

pwd  # should end in /openclaw-control-center

ls control-center/src/runtime  # should exist

If cloned correctly but still missing: the clone was incomplete. Re-clone:

git clone https://github.com/TianyiDataScience/openclaw-control-center.git

UI Doesn't Start / Port Conflicts

# Check if port 4310 is in use

lsof -i :4310

# or on Windows

netstat -ano | findstr :4310

# Change port in .env

PORT=4311

Data Not Showing (Partial or Empty Sections)

  • Open Settings接线状态 (Wiring Status) — it lists exactly which connectors are connected, partial, or missing.
  • Common causes:
  • OPENCLAW_GATEWAY_URL not set or wrong port
  • OPENCLAW_HOME doesn't point to actual ~/.openclaw
  • OpenClaw subscription snapshot not at default path

Token Auth Failures

# Generate a local token (see openclaw docs for token location)

cat ~/.openclaw/local-token

# Pass via header in API calls

curl -H "X-Local-Token: <token>" http://127.0.0.1:4310/api/tasks/approve

Approval Actions Silently Do Nothing

Check your .env:

APPROVAL_ACTIONS_ENABLED=true   # Must be true

APPROVAL_ACTIONS_DRY_RUN=false  # Must be false for real execution

Both must be explicitly set. Default is disabled + dry-run.

Memory Section Shows Inactive Agents

The memory section is scoped to agents listed in openclaw.json. If deleted agents still appear:

# Check active agents

cat ~/.openclaw/openclaw.json | grep -A5 '"agents"'

Remove stale entries from openclaw.json — the memory section will update on next load.

Windows Shell Issues

Prefer npm run dev:ui over UI_MODE=true npm run dev. Cross-env variable setting behaves differently in PowerShell/CMD. The dev:ui script handles this internally.

Prerequisites

  • Node.js + npm
  • A running OpenClaw installation with accessible Gateway
  • Read access to ~/.openclaw on the local machine
  • (Optional) ~/.codex and OpenClaw subscription snapshot for full usage data
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card