π¦ DotnetClaw is a personal AI assistant developed in .NET framework β Semantic Kernel agents, Telegram Bot channel, Agent Skills, MCP client, Playwright browser, Cursor agent CLI integration
DotnetClaw is a self-hosted AI coding assistant that runs on your machine. It combines Microsoft Semantic Kernel, MCP (Model Context Protocol), a Telegram bot, and a Playwright browser into a single .NET 10 CLI agent that can read/write files, run shell commands, browse the web, and control your coding agent like Cursor or Claude Code β all from natural language.
Supports: OpenAI Β· Azure OpenAI Β· Anthropic Claude
DotnetClaw is inspired by π¦ OpenClaw (formerly Clawdbot / Moltbot) by Peter Steinberger β a personal AI assistant concept that proved how powerful a well-wired agent loop can be. This is a .NET reimagining of that idea.
- Slash commands (/status /ask /plan /agent /cursor /claude)
- Chat and control your DotnetClaw agent from terminal
- Show chat history
- Install agent skills from DotnetClawHub (hub)
DotnetClaw CLI running in Windows Powershell Terminal:
DotnetClaw CLI running in Ubuntu bash terminal (via WSL):
- Onboarding
- Dashboard
- Chat
- Skills
- Terminal UI
- Telegram channel configuration
Web UI for DotnetClaw developed in Blazor:
- Create & Publish own custom (agent) skill
- Browse agent skills
- Install agent skill via API
- SKILL.md support
Agent Skills Hub for DotnetClaw inspired by ClawHub developed in Blazor:
Browse & search for skills:
Skill details & installation:
- .NET 10 SDK
- An OpenAI API key (or Azure OpenAI / Anthropic β see providers)
# Set your API key
set OPENAI_API_KEY=sk-...
# Restore & run
cd src/DotnetClaw
dotnet run| Environment Variable | Description | Default |
|---|---|---|
DOTNETCLAW_PROVIDER |
openai | azure | anthropic |
openai |
OPENAI_API_KEY |
OpenAI API key | β |
AZURE_OPENAI_ENDPOINT |
Azure OpenAI endpoint URL | β |
AZURE_OPENAI_API_KEY |
Azure OpenAI key | β |
AZURE_OPENAI_DEPLOYMENT |
Azure deployment name | Model ID |
Or edit appsettings.json directly.
| Command | Action |
|---|---|
help |
Show available commands |
reset |
Clear conversation history |
history |
Print full conversation history |
exit |
Quit |
cd tests/DotnetClaw.Tests
dotnet testUser Input
β
βΌ
ClawAgentLoop.RunTurnAsync()
β
ββββΊ ChatCompletionAgent (SK)
β β
β βββ FunctionChoiceBehavior.Auto() β auto tool-call selection
β β
β βββ [Tool Call] Shell.run_command
β βββ [Tool Call] FileSystem.read_file
β βββ [Tool Call] Dotnet.find_csharp_projects
β β β²
β β βββ results fed back into context
β β
β βββ Final text response βββ streamed to terminal
β
ββββΊ Max iterations guard (default: 20)
On every session start (and on reset), DotnetClaw loads *.md files from ./workspace/
and injects them into the system prompt before any user message is sent.
./workspace/
βββ SOUL.md β Who the agent is (personality, values, style)
βββ AGENTS.md β How it uses tools and handles multi-agent flows
βββ USER.md β Who you are (role, prefs, tech stack)
βββ CONTEXT.md β What you're working on right now
βββ MEMORY.md β Persistent facts you want remembered
βββ *.md β Any additional documents, loaded alphabetically
Loading order is controlled by WorkspaceDocumentPriority in appsettings.json
(default: SOUL β AGENTS β USER β CONTEXT β MEMORY β TOOLS β RULES).
Remaining *.md files follow alphabetically.
The workspace folder is optional β if it doesn't exist, DotnetClaw starts with the base system prompt only.
| Command | Action |
|---|---|
workspace |
Show loaded documents table |
ws reload |
Force reload from disk without resetting chat |
prompt |
Print the full effective system prompt |
The agent can query workspace docs mid-conversation via the Workspace plugin:
list_workspace_docsβ table of loaded docsget_workspace_doc SOULβ fetch a specific doc's contentreload_workspaceβ hot-reload from diskget_workspace_contextβ full injected context block
| Function | Description |
|---|---|
run_command |
Execute any shell command |
list_directory |
Tree-style directory listing |
get_working_directory |
Return current working directory |
| Function | Description |
|---|---|
read_file |
Read a text file |
write_file |
Create or overwrite a file |
append_file |
Append to an existing file |
delete_file |
Delete a file |
file_exists |
Check if a path exists |
find_files |
Glob search for files |
| Function | Description |
|---|---|
find_csharp_projects |
Locate .csproj files |
summarise_csharp_file |
Structural summary of a .cs file |
get_nuget_packages |
List NuGet packages in a .csproj |
| Function | Description |
|---|---|
list_workspace_docs |
Table of all loaded identity documents |
get_workspace_doc |
Fetch full content of a specific document |
reload_workspace |
Force reload all docs from disk |
get_workspace_context |
Full combined context block (as injected) |
| Function | Mode | File changes? | Description |
|---|---|---|---|
cursor_agent |
agent |
β Yes | Autonomous coding β reads, plans, edits files |
cursor_plan |
plan |
β No | Returns a step-by-step plan, no edits |
cursor_ask |
ask |
β No | Q&A about the codebase, read-only |
cursor_run |
any | depends | Low-level runner with full flag control |
CLI structure built by the plugin:
agent --mode=<agent|plan|ask> --prompt "..." [--model <model>] [--yes] [extraFlags] <workspace>
Configuration (appsettings.json β DotnetClaw:Cursor):
| Key | Default | Description |
|---|---|---|
ExecutablePath |
"agent" |
Path to agent.exe / agent, or bare name if it's on PATH |
DefaultTimeoutSeconds |
300 |
Per-invocation timeout (max 1800) |
RequireConfirmationForAgentMode |
true |
Prompt user before running in agent mode (destructive) |
AutoApproveInAgentMode |
false |
Pass --yes to suppress Cursor's own confirmations |
Model |
"" |
Override model, e.g. "claude-3-5-sonnet" or "gpt-4o" |
ExtraFlags |
"" |
Raw flags appended to every invocation |
Finding agent.exe on your system:
# Windows
%LOCALAPPDATA%\Programs\cursor\resources\app\bin\agent.exe
# macOS
/Applications/Cursor.app/Contents/Resources/app/bin/agent
# Linux
~/.local/share/cursor/resources/app/bin/agent
Set ExecutablePath in appsettings.json or add the binary's folder to your PATH.
Add a new skill in 3 steps:
- Create
Plugins/MyPlugin.cswith[KernelFunction]methods - Register in
KernelFactory.cs:builder.Plugins.AddFromObject(services.GetRequiredService<MyPlugin>(), "MySkill");
- Add DI registration in
Program.cs:services.AddSingleton<MyPlugin>();
DotnetClaw connects to any Model Context Protocol server and exposes its tools directly to the Semantic Kernel agent β no hand-written glue code per tool.
Packages used:
ModelContextProtocol.Client 0.1.0-preview.10β official .NET MCP SDK (stdio + SSE transports)Microsoft.SemanticKernel.Plugins.MCP 1.30.0β auto-converts MCP tool schemas β SKKernelFunctions viaIMcpClient.AsKernelPluginAsync()
appsettings.json: Mcp:Servers[]
β
βΌ
McpConnectionManager (IHostedService)
β’ Launches stdio servers as child processes (npx/uvx/python/custom binary)
β’ OR connects to SSE servers over HTTP
β’ Holds live IMcpClient instances, one per server
β’ Connects in parallel at startup; failures are logged, not fatal
β
βΌ
McpKernelLoader (called from ClawAgentLoop.InitialiseAsync)
β’ Calls IMcpClient.AsKernelPluginAsync("Mcp_{name}") for each connected client
β’ Each MCP tool β SK KernelFunction with auto-generated description + parameters
β’ Agent sees them identically to built-in skills
β
βΌ
Agent turn: "Read the file /project/README.md"
β Mcp_filesystem.read_file(path: "/project/README.md")
β MCP server returns content
β Agent gets the result
Enable servers in appsettings.json under DotnetClaw:Mcp:Servers:
"Mcp": {
"ConnectionTimeoutSeconds": 30,
"LogToolCallDetails": false,
"Servers": [
{
"Name": "filesystem",
"Description": "Read/write local files",
"Transport": "Stdio",
"Command": "npx",
"Arguments": [ "-y", "@modelcontextprotocol/server-filesystem", "/my/projects" ],
"Enabled": true
},
{
"Name": "github",
"Description": "Search repos, issues, files on GitHub",
"Transport": "Stdio",
"Command": "npx",
"Arguments": [ "-y", "@modelcontextprotocol/server-github" ],
"Environment": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..." },
"Enabled": true
},
{
"Name": "my-remote-server",
"Description": "Custom SSE-based MCP server",
"Transport": "Sse",
"Url": "http://localhost:3000",
"Enabled": true
}
]
}| Transport | How it connects | Use for |
|---|---|---|
Stdio |
Spawns a child process, communicates over stdin/stdout | Local tools: npx, uvx, Python scripts, custom binaries |
Sse |
HTTP long-lived connection (Server-Sent Events) | Remote servers, Docker containers, shared team servers |
| Server | Command | What it does |
|---|---|---|
@modelcontextprotocol/server-filesystem |
npx -y @modelcontextprotocol/server-filesystem <path> |
Read/write/search files |
@modelcontextprotocol/server-github |
npx -y @modelcontextprotocol/server-github |
Repos, issues, PRs |
@modelcontextprotocol/server-postgres |
npx -y @modelcontextprotocol/server-postgres <conn> |
Read-only SQL |
mcp-server-fetch |
uvx mcp-server-fetch |
Fetch + Markdown convert URLs |
@modelcontextprotocol/server-brave-search |
npx -y @modelcontextprotocol/server-brave-search |
Web search |
Each server becomes a plugin named Mcp_{serverName}. Special characters in server names are replaced with _:
Server "filesystem" β plugin "Mcp_filesystem"
Server "my-github" β plugin "Mcp_my_github"
The agent calls these exactly like built-in skills. With a filesystem server connected:
You: "List all .cs files in /src and find the one with the most lines"
β Mcp_filesystem.list_directory(path: "/src")
β [agent loops through results]
β Mcp_filesystem.read_file(path: "/src/biggest.cs")
β "The file with the most lines is ClawAgentLoop.cs (181 lines)"
| Function | Description |
|---|---|
mcp_list_servers |
List all servers, connection status, tool counts |
mcp_list_tools |
List tools + parameters for a specific server |
mcp_call_tool |
Call a tool with raw JSON args (debug/fallback) |
mcp_reconnect |
Reconnect a server and reload its tools into kernel |
mcp_list_resources |
List MCP resources exposed by a server |
mcp_read_resource |
Read a resource by URI |
DotnetClaw has a full headless browser via Microsoft Playwright. The agent can navigate the web, take screenshots, fill forms, click buttons, and push screenshots directly to Telegram β all from natural language instructions.
Install browser binaries after first dotnet build:
# Install the Playwright CLI tool
dotnet tool install --global Microsoft.Playwright.CLI
# Install Chromium (default). Add firefox or webkit if needed.
playwright install chromiumOr without the global tool:
pwsh bin/Debug/net10.0/playwright.ps1 install chromiumYou: "Go to https://github.com and take a screenshot"
β browser_navigate(url: "https://github.com")
β browser_screenshot_and_send() β sends photo to Telegram
You: "Log into the admin panel at https://app.example.com"
β browser_navigate(url: "https://app.example.com/login")
β browser_fill(cssSelector: "#username", value: "admin")
β browser_fill(cssSelector: "#password", value: "secret")
β browser_click(cssSelector: "button[type='submit']")
β browser_screenshot_and_send(caption: "Login result")
You: "Fill the contact form and submit it"
β browser_submit_form(
fields: "#name=Alice\n#email=alice@example.com\n#message=Hello",
submitSelector: "#send-btn",
successSelector: ".thank-you-message")
| Command | Description |
|---|---|
/goto <url> |
Navigate browser to URL, auto-send screenshot |
/screenshot |
Screenshot current page β Telegram photo |
/screenshot <selector> |
Screenshot a specific CSS element |
| Function | Description |
|---|---|
browser_navigate |
Navigate to a URL, returns title + status + load time |
browser_screenshot |
Save screenshot to disk, returns file path |
browser_screenshot_and_send |
Screenshot + send as Telegram photo in one step |
browser_get_text |
Extract visible text from page or element |
browser_fill |
Type value into a form field by CSS selector |
browser_click |
Click any element by CSS selector |
browser_submit_form |
Fill multiple fields + click submit atomically |
browser_evaluate |
Run JavaScript on the page, return the result |
"Browser": {
"BrowserType": "chromium", // chromium | firefox | webkit
"Headless": true, // false to watch the browser window
"DefaultTimeoutMs": 30000,
"ScreenshotDirectory": "screenshots",
"ScreenshotFormat": "png", // png | jpeg
"JpegQuality": 90,
"PersistBrowserSession": true, // reuse page between calls (cookies persist)
"ViewportWidth": 1280,
"ViewportHeight": 800,
"SlowMoMs": 0 // slow down ops (ms) β useful for debugging
}BrowserSessionManager β IHostedService, owns IPlaywright + IBrowser
β lazy-inits on first use
β persistent mode: reuses IPage (login state survives between turns)
β isolated mode: fresh IPage per call (clean state)
β
βΌ
PlaywrightBrowserSession β IBrowserSession wrapping a real Playwright IPage
β navigate, screenshot, fill, click, evaluate, waitForSelector
β
βΌ
BrowserPlugin β 8 KernelFunctions
β
βββ browser_navigate
βββ browser_screenshot β saves to ./screenshots/
βββ browser_screenshot_and_send β ITelegramBotClient.SendPhotoAsync (multipart upload)
βββ browser_get_text
βββ browser_fill
βββ browser_click
βββ browser_submit_form β fills fields sequentially, then clicks submit
βββ browser_evaluate β runs JS on page
TelegramCommandRouter
βββ /goto <url> β browser_navigate + browser_screenshot_and_send
βββ /screenshot [sel] β browser_screenshot_and_send
Control DotnetClaw remotely via Telegram β no port forwarding or webhook required.
Uses long-polling (getUpdates) and raw HttpClient β zero Telegram SDK dependency.
- Message @BotFather β
/newbotβ copy the token - Message @userinfobot to get your chat ID
- Set config in
appsettings.json:
"Telegram": {
"Enabled": true,
"BotToken": "123456789:ABCdef...",
"AllowedChatIds": [ 123456789 ]
}Or via environment variable (recommended for production):
export TELEGRAM_BOT_TOKEN=123456789:ABCdef...| Command | Description | Touches files? |
|---|---|---|
/ask <question> |
Ask DotnetClaw anything | β |
<free text> |
Same as /ask | β |
/plan <prompt> |
Cursor plan mode | β |
/cursor_ask <q> |
Cursor Q&A about codebase | β |
/agent <prompt> |
Cursor agent (edits files!) | β |
/reset |
Clear conversation + reload workspace | β |
/status |
Show bot status | β |
/help |
Show command list | β |
| Key | Default | Description |
|---|---|---|
Enabled |
false |
Must be true to activate |
BotToken |
"" |
Token from @BotFather (or TELEGRAM_BOT_TOKEN env var) |
AllowedChatIds |
[] |
Whitelist of authorised chat IDs |
LongPollTimeoutSeconds |
30 |
Seconds per getUpdates call |
MaxMessageLength |
4000 |
Auto-split threshold (Telegram limit is 4096) |
ParseMode |
MarkdownV2 |
Message formatting mode |
SendTypingIndicator |
true |
Show "typingβ¦" while processing |
The agent can push Telegram messages mid-task using the Telegram kernel plugin:
You: "Run the tests and notify me on Telegram when done"
β agent calls Shell.run_command("dotnet test")
β agent calls Telegram.send_telegram_notification("Tests Complete", "12 passed, 0 failed")
β π± You receive a Telegram message immediately
Telegram long-poll (getUpdates, 30s)
β
βΌ
TelegramPollingService β IHostedService, runs alongside the REPL
β
βββ AllowedChatIds whitelist check
βββ Per-chat SemaphoreSlim (serialises concurrent messages)
βββ SendChatAction "typingβ¦" (instant feedback)
β
βΌ
TelegramCommandRouter β Parses /commands and free text
β
βββ /ask + freetext β ClawAgentLoop.RunTurnAsync(outputSink: ResponseCollector)
βββ /plan β CursorPlugin.CursorPlanAsync
βββ /agent β CursorPlugin.CursorAgentAsync
βββ /cursor_ask β CursorPlugin.CursorAskAsync
βββ /reset /status /help β inline string responses
β
βΌ
ITelegramBotClient β Raw HttpClient, no SDK
sendMessage (MarkdownV2, auto-splits >4000 chars, retries as plain text on parse error)
DotnetClaw/
βββ workspace/ β Identity documents (loaded every session)
β βββ SOUL.md β Agent personality & values
β βββ AGENTS.md β Tool-use rules & orchestration behaviour
β βββ USER.md β User profile & preferences
β βββ CONTEXT.md β Current project / session context
β βββ MEMORY.md β Persistent facts across resets
β βββ <custom>.md β Any additional documents
βββ src/DotnetClaw/
β βββ Program.cs β Entry point + REPL loop
β βββ appsettings.json β Configuration
β βββ Config/
β β βββ DotnetClawOptions.cs β Typed config model
β βββ Workspace/
β β βββ WorkspaceDocument.cs β Typed record for a loaded identity doc
β β βββ WorkspaceLoader.cs β Scans ./workspace, loads in priority order
β βββ Agents/
β β βββ ClawAgentLoop.cs β Core agentic loop (SK ChatCompletionAgent)
β β βββ KernelFactory.cs β Kernel + plugin wiring, provider selection
β βββ Telegram/
β β βββ TelegramModels.cs β TelegramUpdate, Message, Chat, User, ApiResponse<T>
β β βββ TelegramBotClient.cs β ITelegramBotClient + raw HttpClient impl
β β βββ TelegramCommandRouter.cs β Command parser + dispatch to agent/Cursor
β β βββ TelegramPollingService.cs β IHostedService long-poll loop
β β βββ ShellPlugin.cs β run_command, list_directory
β β βββ FileSystemPlugin.cs β read/write/find files
β β βββ DotnetPlugin.cs β .csproj / C# project analysis
β β βββ WorkspacePlugin.cs β Runtime workspace query skill
β β βββ CursorPlugin.cs β cursor_agent / cursor_plan / cursor_ask / cursor_run
β β βββ CursorTypes.cs β CursorMode enum, CursorResult
β β βββ CursorProcessRunner.cs β ICursorProcessRunner + real OS process impl
β β βββ TelegramPlugin.cs β send_telegram_message, send_telegram_notification
β βββ UI/
β βββ SpectreConsoleRenderer.cs β Rich terminal UI via Spectre.Console
βββ tests/DotnetClaw.Tests/
βββ ShellPluginTests.cs
βββ FileSystemPluginTests.cs
βββ WorkspaceLoaderTests.cs
βββ CursorPluginTests.cs β FakeCursorProcessRunner, all modes + edge cases
βββ TelegramBotClientTests.cs β MockHttpMessageHandler, send/receive/split
βββ TelegramCommandRouterTests.cs β Command parsing, routing, Markdown escaping
MIT

