Disclaimer: This project is not affiliated with, endorsed by, or associated with OpenClaw. It is an independent .NET implementation inspired by their excellent work.
Self-hosted OpenClaw.NET gateway + agent runtime in .NET (NativeAOT-friendly).
- Quickstart Guide — Fast local setup, runtime mode selection, and first usage flow.
- Tool Guide — Detailed setup for all 18+ native tools.
- Hardening Profiles — Copy/paste dev/staging/prod security setups.
- User Guide — Core concepts, providers, tools, skills, and channels.
- Startup Architecture Notes — Current bootstrap/composition/profile/pipeline layout.
- Security Guide — Mandatory reading for public deployments.
- Changelog — Tracked project changes.
- Docker Hub Overview
Published container images:
ghcr.io/clawdotnet/openclaw.net:latesttellikoroma/openclaw.net:latestpublic.ecr.aws/u6i5b9b7/openclaw.net:latest
OpenClaw.NET now separates gateway startup and runtime composition into explicit layers instead of a single large startup path.
Startup flow:
Bootstrap/
- Loads config, resolves runtime mode, applies validation and hardening, and handles early exits such as
--doctor.
Composition/andProfiles/
- Registers services and applies the effective runtime lane:
aotfor trim-safe deployments orjitfor expanded compatibility.
Pipeline/andEndpoints/
- Wires middleware, channel startup, workers, shutdown handling, and the grouped HTTP/WebSocket surfaces.
Runtime flow:
- The Gateway handles HTTP, WebSocket, webhook, auth, policy, and observability concerns.
- The Agent Runtime owns reasoning, tool execution, memory interaction, and skill loading.
- Native tools run in-process.
- Upstream-style plugins run through the Node.js bridge, with capability enforcement depending on the effective runtime lane.
- Pure
SKILL.mdpackages remain independent of the plugin bridge.
graph TD
Client[Web UI / CLI / Companion / WebSocket Client] <--> Gateway[Gateway]
Webhooks[Telegram / Twilio / WhatsApp / Generic Webhooks] --> Gateway
subgraph Startup
Bootstrap[Bootstrap]
Composition[Composition]
Profiles[Runtime Profiles]
Pipeline[Pipeline + Endpoints]
Bootstrap --> Composition --> Profiles --> Pipeline
end
subgraph Runtime
Gateway <--> Agent[Agent Runtime]
Agent <--> Tools[Native Tools]
Agent <--> Memory[(Memory + Sessions)]
Agent <-->|Provider API| LLM{LLM Provider}
Agent <-->|Bridge transport| Bridge[Node.js Plugin Bridge]
Bridge <--> JSPlugins[TS / JS Plugins]
end
Runtime lanes:
OpenClaw:Runtime:Mode="aot"keeps the trim-safe, low-memory lane.OpenClaw:Runtime:Mode="jit"enables expanded bridge surfaces and native dynamic plugins.OpenClaw:Runtime:Mode="auto"selectsjitwhen dynamic code is available andaototherwise.
For the full startup-module breakdown, see docs/architecture-startup-refactor.md.
See QUICKSTART.md for the fastest path from zero to a running gateway.
The shortest local path is:
- Set your API key:
export MODEL_PROVIDER_KEY="..."- Optional workspace:
export OPENCLAW_WORKSPACE="$PWD/workspace 8000 " - Optional runtime selection:
export OpenClaw__Runtime__Mode="jit"
- Run the gateway:
dotnet run --project src/OpenClaw.Gateway -c Release- Or validate config first:
dotnet run --project src/OpenClaw.Gateway -c Release -- --doctor - Optional config file:
dotnet run --project src/OpenClaw.Gateway -c Release -- --config ~/.openclaw/config.json
- Use one of the built-in clients:
- Web UI:
http://127.0.0.1:18789/chat - WebSocket endpoint:
ws://127.0.0.1:18789/ws - CLI:
dotnet run --project src/OpenClaw.Cli -c Release -- chat - Companion app:
dotnet run --project src/OpenClaw.Companion -c Release
Environment variables for the CLI:
OPENCLAW_BASE_URL(defaulthttp://127.0.0.1:18789)OPENCLAW_AUTH_TOKEN(only required when the gateway enforces auth)
For advanced provider setup, webhook channels, and deployment hardening, see the User Guide and Security Guide.
Common local usage paths:
- Browser UI: start the gateway and open
http://127.0.0.1:18789/chat - CLI chat:
dotnet run --project src/OpenClaw.Cli -c Release -- chat - One-shot CLI run:
dotnet run --project src/OpenClaw.Cli -c Release -- run "summarize this README" --file ./README.md - Desktop companion:
dotnet run --project src/OpenClaw.Companion -c Release - Doctor/report mode:
dotnet run --project src/OpenClaw.Gateway -c Release -- --doctor
Common runtime choices:
- Use
aotwhen you want the trim-safe mainstream lane. - Use
jitwhen you need expanded plugin compatibility or native dynamic plugins. - Leave
autoif you want the artifact to choose based on dynamic-code support.
The most practical local setup is:
- Web UI or Companion for interactive usage
- CLI for scripting and automation
--doctorbefore exposing a public bind or enabling plugins
Run the cross-platform desktop companion:
dotnet run --project src/OpenClaw.Companion -c Release
Notes:
- For non-loopback binds, set
OPENCLAW_AUTH_TOKENon the gateway and enter the same token in the companion app. - If you enable “Remember”, the token is saved to
settings.jsonunder your OS application data directory.
The gateway supports both:
- Client sends: raw UTF-8 text
- Server replies: raw UTF-8 text
If the client sends JSON shaped like this, the gateway replies with JSON:
Client → Server:
{ "type": "user_message", "text": "hello", "messageId": "optional", "replyToMessageId": "optional" }Server → Client:
{ "type": "assistant_message", "text": "hi", "inReplyToMessageId": "optional" }If OpenClaw:BindAddress is not loopback (e.g. 0.0.0.0), you must set OpenClaw:AuthToken / OPENCLAW_AUTH_TOKEN.
Preferred client auth:
Authorization: Bearer <token>
Optional legacy auth (disabled by default):
?token=<token>whenOpenClaw:Security:AllowQueryStringToken=true
Built-in WebChat auth behavior:
- The
/chatUI connects to/wsusing?token=<token>from the Auth Token field. - For Internet-facing/non-loopback binds, set
OpenClaw:Security:AllowQueryStringToken=trueif you use the built-in WebChat. - Tokens are stored in
sessionStorageby default. Enabling Remember also storesopenclaw_tokeninlocalStorage.
You can run TLS either:
- Behind a reverse proxy (recommended): nginx / Caddy / Cloudflare, forwarding to
http://127.0.0.1:18789 - Directly in Kestrel: configure HTTPS endpoints/certs via standard ASP.NET Core configuration
If you enable OpenClaw:Security:TrustForwardedHeaders=true, set OpenClaw:Security:KnownProxies to the IPs of your reverse proxies.
When binding to a non-loopback address, the gateway refuses to start unless you explicitly harden (or opt in to) the most dangerous settings:
- Wildcard tooling roots (
AllowedReadRoots=["*"],AllowedWriteRoots=["*"]) OpenClaw:Tooling:AllowShell=trueOpenClaw:Plugins:Enabled=trueorOpenClaw:Plugins:DynamicNative:Enabled=true(third-party plugin execution)- WhatsApp official webhooks without signature validation (
ValidateSignature=true+WebhookAppSecretRefrequired) - WhatsApp bridge webhooks without a bridge token (
BridgeTokenRef/BridgeTokenrequired) raw:secret refs (to reduce accidental secret commits)
To override (not recommended), set:
OpenClaw:Security:AllowUnsafeToolingOnPublicBind=trueOpenClaw:Security:AllowPluginBridgeOnPublicBind=trueOpenClaw:Security:AllowRawSecretRefsOnPublicBind=true
This project includes local tools (shell, read_file, write_file). If you expose the gateway publicly, strongly consider restricting:
OpenClaw:Tooling:AllowShell=falseOpenClaw:Tooling:AllowedReadRoots/AllowedWriteRootsto specific directories
If you are connecting from a browser-based client hosted on a different origin, configure:
OpenClaw:Security:AllowedOrigins=["https://your-ui-host"]
If AllowedOrigins is not configured and the client sends an Origin header, the gateway requires same-origin.
OpenClaw.NET now exposes two explicit runtime lanes:
OpenClaw:Runtime:Mode="aot"keeps the low-memory, trim-safe lane. Supported plugin capabilities here areregisterTool(),registerService(), plugin-packaged skills, and the supported manifest/config subset.OpenClaw:Runtime:Mode="jit"enables the expanded compatibility lane. In this mode the bridge also supportsregisterChannel(),registerCommand(),registerProvider(), andapi.on(...), and the gateway can load JIT-only in-process native dynamic plugins.
Pure ClawHub SKILL.md packages are independent of the bridge and remain the most plug-and-play compatibility path.
When you enable OpenClaw:Plugins:Enabled=true, the Gateway spawns a Node.js JSON-RPC bridge. When you enable OpenClaw:Plugins:DynamicNative:Enabled=true, the gateway also loads JIT-only in-process .NET plugins through the native dynamic host.
Across both lanes, unsupported surfaces fail fast with explicit diagnostics instead of silently degrading:
registerGatewayMethod()registerCli()- any JIT-only capability when the effective runtime mode is
aot
The /doctor report includes per-plugin load diagnostics, and the repo now includes hermetic bridge tests plus a pinned public smoke manifest for mainstream packages. For the exact matrix and TypeScript requirements such as jiti, see Plugin Compatibility Guide.
OpenClaw.NET is not a replacement for Semantic Kernel. If you're already using Microsoft.SemanticKernel, OpenClaw can act as the production gateway/runtime host (auth, rate limits, channels, OTEL, policy) around your SK code.
Supported integration patterns today:
- Wrap your SK orchestration as an OpenClaw tool: keep SK in-process, expose a single "entrypoint" tool the OpenClaw agent can call.
- Host SK-based agents behind the OpenClaw gateway: use OpenClaw for Internet-facing concerns (WebSocket,
/v1/*, Telegram/Twilio/WhatsApp), while your SK logic stays in your app/tool layer.
More details and AOT/trimming notes: see SEMANTIC_KERNEL.md.
Conceptual example (tool wrapper):
// Your tool can instantiate and call Semantic Kernel. OpenClaw policies still apply
// to *when* this tool runs, who can call it, and how often.
public sealed class SemanticKernelTool : ITool
{
public string Name => "sk_example";
public string Description => "Example SK-backed tool.";
public string ParameterSchema => "{\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}},\"required\":[\"text\"]}";
public async ValueTask<string> ExecuteAsync(string argumentsJson, CancellationToken ct)
{
// Parse argsJson, then run SK here (Kernel builder + plugin invocation).
throw new NotImplementedException();
}
}Notes:
- NativeAOT: Semantic Kernel usage may require additional trimming/reflection configuration. Keep SK interop optional so the core gateway/runtime remains NativeAOT-friendly.
Available:
src/OpenClaw.SemanticKernelAdapter— optional adapter library that exposes SK functions as OpenClaw tools.samples/OpenClaw.SemanticKernelInteropHost— runnable sample host demonstrating/v1/responseswithout requiring external LLM access.
- Create a Telegram Bot via BotFather and obtain the Bot Token.
- Set the auth token as an environment variable:
export TELEGRAM_BOT_TOKEN="..."
- Configure
OpenClaw:Channels:Telegraminsrc/OpenClaw.Gateway/appsettings.json:Enabled=trueBotTokenRef="env:TELEGRAM_BOT_TOKEN"MaxRequestBytes=65536(default; inbound webhook body cap)
Register your public webhook URL directly with Telegram's API:
POST https://api.telegram.org/bot<your-bot-token>/setWebhook?url=https://<your-public-host>/telegram/inbound
Notes:
- The inbound webhook path is configurable via
OpenClaw:Channels:Telegram:WebhookPath(default:/telegram/inbound). AllowedFromUserIdscurrently checks the numericchat.idvalue from Telegram updates (not thefrom.iduser id).
- Create a Twilio Messaging Service (recommended) or buy a Twilio phone number.
- Set the
6D40
auth token as an environment variable:
export TWILIO_AUTH_TOKEN="..."
- Configure
OpenClaw:Channels:Sms:Twilioinsrc/OpenClaw.Gateway/appsettings.json:Enabled=trueAccountSid=...AuthTokenRef="env:TWILIO_AUTH_TOKEN"MessagingServiceSid=...(preferred) orFromNumber="+1..."(fallback)AllowedFromNumbers=[ "+1YOUR_MOBILE" ]AllowedToNumbers=[ "+1YOUR_TWILIO_NUMBER" ]WebhookPublicBaseUrl="https://<your-public-host>"(required whenValidateSignature=true)MaxRequestBytes=65536(default; inbound webhook body cap)
Point Twilio’s inbound SMS webhook to:
POST https://<your-public-host>/twilio/sms/inbound
Recommended exposure options:
- Reverse proxy with TLS
- Cloudflare Tunnel
- Tailscale funnel / reverse proxy
- Keep
ValidateSignature=true - Use strict allowlists (
AllowedFromNumbers,AllowedToNumbers) - Do not set
AuthTokenReftoraw:...outside local development
Inbound webhook payloads are hard-capped before parsing. Configure these limits as needed:
OpenClaw:Channels:Sms:Twilio:MaxRequestBytes(default65536)OpenClaw:Channels:Telegram:MaxRequestBytes(default65536)OpenClaw:Channels:WhatsApp:MaxRequestBytes(default65536)OpenClaw:Webhooks:Endpoints:<name>:MaxRequestBytes(default131072)
For generic /webhooks/{name} endpoints, MaxBodyLength still controls prompt truncation after request-size validation.
If ValidateHmac=true, Secret is required; startup validation fails otherwise.
- Set required environment variables:
Bash / Zsh:
export MODEL_PROVIDER_KEY="sk-..."
export OPENCLAW_AUTH_TOKEN="$(openssl rand -hex 32)"PowerShell:
$env:MODEL_PROVIDER_KEY = "sk-..."
$env:OPENCLAW_AUTH_TOKEN = [Convert]::ToHexString((1..32 | Array { Get-Random -Min 0 -Max 256 }))
$env:EMAIL_PASSWORD = "..." # (Optional) For email toolNote: For the built-in WebChat UI (
http://<ip>:18789/chat), enter this exactOPENCLAW_AUTH_TOKENvalue in the "Auth Token" field. WebChat connects with a query token (?token=), so on non-loopback binds you must also setOpenClaw:Security:AllowQueryStringToken=true. Tokens are session-scoped by default; check Remember to persist across browser restarts. If you enable the Email Tool, setEMAIL_PASSWORDsimilarly.
docker compose up -d openclaw
export OPENCLAW_DOMAIN="openclaw.example.com" docker compose --profile with-tls up -d
### Build from source
```bash
docker build -t openclaw.net .
docker run -d -p 18789:18789 \
-e MODEL_PROVIDER_KEY="sk-..." \
-e OPENCLAW_AUTH_TOKEN="change-me" \
-v openclaw-memory:/app/memory \
openclaw.net
The same multi-arch image is published to:
ghcr.io/clawdotnet/openclaw.net:latesttellikoroma/openclaw.net:latestpublic.ecr.aws/u6i5b9b7/openclaw.net:latest
Example pull:
docker pull ghcr.io/clawdotnet/openclaw.net:latestMulti-arch push (recommended):
docker buildx build --platform linux/amd64,linux/arm64 \
-t ghcr.io/clawdotnet/openclaw.net:latest \
-t ghcr.io/clawdotnet/openclaw.net:<version> \
-t tellikoroma/openclaw.net:latest \
-t tellikoroma/openclaw.net:<version> \
-t public.ecr.aws/u6i5b9b7/openclaw.net:latest \
-t public.ecr.aws/u6i5b9b7/openclaw.net:<version> \
--push .The Dockerfile uses a multi-stage build:
- Build stage — full .NET SDK, runs tests, publishes NativeAOT binary
- Runtime stage — Ubuntu Chiseled (distroless), ~23 MB NativeAOT binary, non-root user
| Path | Purpose |
|---|---|
/app/memory |
Session history + memory notes (persist across restarts) |
/app/workspace |
Mounted workspace for file tools (optional) |
- Set
OPENCLAW_AUTH_TOKENto a strong random value - Set
MODEL_PROVIDER_KEYvia environment variable (never in config files) - Use
appsettings.Production.json(AllowShell=false, restricted roots) - Enable TLS (reverse proxy or Kestrel HTTPS)
- Set
AllowedOriginsif serving a web frontend - Set
TrustForwardedHeaders=true+KnownProxiesif behind a proxy - Set
MaxConnectionsPerIpandMessagesPerMinutePerConnectionfor rate limiting - Set
OpenClaw:SessionRateLimitPerMinuteto rate limit inbound messages (also applies to/v1/*OpenAI-compatible endpoints) - Monitor
/healthand/metricsendpoints - Pin a specific Docker image tag (not
:latest) in production
The included docker-compose.yml has a Caddy service with automatic HTTPS:
export OPENCLAW_DOMAIN="openclaw.example.com"
docker compose --profile with-tls up -dCaddy auto-provisions Let's Encrypt certificates. Edit deploy/Caddyfile to customize.
If you want the gateway to trust X-Forwarded-* headers from your proxy, set:
OpenClaw__Security__TrustForwardedHeaders=trueOpenClaw__Security__KnownProxies__0=<your-proxy-ip>
server {
listen 443 ssl http2;
server_name openclaw.example.com;
ssl_certificate /etc/letsencrypt/live/openclaw.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/openclaw.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:18789;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Configure directly in appsettings.json:
{
"Kestrel": {
"Endpoints": {
"Https": {
"Url": "https://0.0.0.0:443",
"Certificate": {
"Path": "/certs/cert.pfx",
"Password": "env:CERT_PASSWORD"
}
}
}
}
}OpenClaw now supports a background retention sweeper for persisted sessions + branches.
- Default is off:
OpenClaw:Memory:Retention:Enabled=false - Expired items are archived as raw JSON before delete
- Default TTLs: sessions
30days, branches14days - Default archive retention:
30days
Example config:
{
"OpenClaw": {
"Memory": {
"Retention": {
"Enabled": true,
"RunOnStartup": true,
"SweepIntervalMinutes": 30,
"SessionTtlDays": 30,
"BranchTtlDays": 14,
"ArchiveEnabled": true,
"ArchivePath": "./memory/archive",
"ArchiveRetentionDays": 30,
"MaxItemsPerSweep": 1000
}
}
}
}Recommended rollout:
- Run a dry-run first:
POST /memory/retention/sweep?dryRun=true - Review
GET /memory/retention/statusand/doctor/text - Confirm archive path sizing and filesystem permissions
Compaction remains disabled by default. If enabling compaction, CompactionThreshold must be greater than MaxHistoryTurns.
OpenClaw natively integrates with OpenTelemetry, providing deep insights into agent reasoning, tool execution, and session lifecycles.
| Endpoint | Auth | Description |
|---|---|---|
GET /health |
Token (if non-loopback) | Basic health check ({ status, uptime }) |
GET /metrics |
Token (if non-loopback) | Runtime counters (requests, tokens, tool calls, circuit breaker state, retention runs/outcomes) |
GET /memory/retention/status |
Token (if non-loopback) | Retention config + last run state + persisted session/branch counts |
| `POST /memory/retention/sweep?dryRun=true | false` | Token (if non-loopback) |
All agent operations emit structured logs and .NET Activity traces with correlation IDs. You can export these to OTLP collectors like Jaeger, Prometheus, or Grafana:
[abc123def456] Turn start session=ws:user1 channel=websocket
[abc123def456] Tool browser completed in 1250ms ok=True
[abc123def456] Turn complete: Turn[abc123def456] session=ws:user1 llm=2 retries=0 tokens=150in/80out tools=1
Set log levels in config:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"AgentRuntime": "Debug",
"SessionManager": "Information"
}
}
}GitHub Actions workflow (.github/workflows/ci.yml):
- On push/PR to main: build + test
- On push to main: publish NativeAOT binary artifact + Docker image to GitHub Container Registry
Looking for:
- Security review
- NativeAOT trimming improvements
- Tool sandboxing ideas
- Performance benchmarks
If this aligns with your interests, open an issue.
⭐ If this project helps your .NET AI work, consider starring it.