Curated repos, tools, and frameworks shaping the developer ecosystem.
Live data from GitHub.
by nullclaw
Fastest, smallest, and fully autonomous AI assistant infrastructure written in Zig
Want a simpler way to install and configure nullclaw with a UI? Try nullhub! (currently in beta)
nullhub provides a UI layer for the Null ecosystem: simpler nullclaw setup and configuration, orchestration from nullboiler, observability from nullwatch, and task tracking from nulltickets.
Null overhead. Null compromise. 100% Zig. 100% Agnostic.
678 KB binary. ~1 MB RAM. Boots in <2 ms. Runs on anything with a CPU.
The smallest fully autonomous AI assistant infrastructure — a static Zig binary that fits on any $5 board, boots in milliseconds, and requires nothing but libc.
Docs: English · 中文 · Contributing · Discord
678 KB binary · <2 ms startup · 5,300+ tests · 50+ providers · 19 channels · Pluggable everything
Local machine benchmark (macOS arm64, Feb 2026), normalized for 0.8 GHz edge hardware.
| OpenClaw | NanoBot | PicoClaw | ZeroClaw | 🦞 NullClaw | |
|---|---|---|---|---|---|
| Language | TypeScript | Python | Go | Rust | Zig |
| RAM | > 1 GB | > 100 MB | < 10 MB | < 5 MB | ~1 MB |
| Startup (0.8 GHz) | > 500 s | > 30 s | < 1 s | < 10 ms | < 8 ms |
| Binary Size | ~28 MB (dist) | N/A (Scripts) | ~8 MB | ~8.8 MB | 678 KB |
| Tests | — | — | — | 1,017 | 5,300+ |
| Source Files | ~400+ | — | — | ~120 | ~230 |
| Cost | Mac Mini $599 | Linux SBC ~$50 | Linux Board $10 | Any $10 hardware | Any $5 hardware |
Measured with
/usr/bin/time -lon ReleaseSmall builds. nullclaw is a static binary with zero runtime dependencies.
Reproduce locally:
zig build -Doptimize=ReleaseSmall
ls -lh zig-out/bin/nullclaw
/usr/bin/time -l zig-out/bin/nullclaw --help
/usr/bin/time -l zig-out/bin/nullclaw status
Start here if you want the shortest path to install, configure, operate, or extend nullclaw.
Localized documentation lives under docs/en/ and docs/zh/. Use the links below to jump straight to the page you need.
| Need | English | 中文 |
|---|---|---|
| Start here | docs/en/README.md | docs/zh/README.md |
| Install | docs/en/installation.md | docs/zh/installation.md |
| Install Zig | docs/en/zig-installation.md | docs/zh/zig-installation.md |
| Configure | docs/en/configuration.md | docs/zh/configuration.md |
| Commands | docs/en/commands.md | docs/zh/commands.md |
| Development | docs/en/development.md | docs/zh/development.md |
| Operations | docs/en/usage.md | docs/zh/usage.md |
| Architecture | docs/en/architecture.md | docs/zh/architecture.md |
| Security | docs/en/security.md | docs/zh/security.md |
| Gateway API | docs/en/gateway-api.md | docs/zh/gateway-api.md |
CONTRIBUTING.md, SECURITY.md, SIGNAL.md| Goal | Open this first | Then go to |
|---|---|---|
| First run in English | docs/en/README.md | docs/en/installation.md → docs/en/configuration.md → docs/en/usage.md |
| Chinese Quick Start (中文快速上手) | docs/zh/README.md | docs/zh/installation.md → docs/zh/configuration.md → docs/zh/usage.md |
| Find the right CLI command | docs/en/commands.md / docs/zh/commands.md | nullclaw help → task-specific subcommand page |
| Contribute code or docs | CONTRIBUTING.md | docs/en/development.md / docs/zh/development.md → relevant architecture page |
| Operate or secure a deployment | docs/en/usage.md / docs/zh/usage.md | docs/en/security.md / docs/zh/security.md → Gateway API |
docs/en/README.md or docs/zh/README.md and follow the guided reading order.docs/en/commands.md or docs/zh/commands.md.CONTRIBUTING.md, then read docs/en/development.md or docs/zh/development.md.The simplest path: install a ready-to-run binary with no extra runtime dependencies.
brew install nullclaw
nullclaw --help
Prerequisite: use Zig 0.16.0 (exact version). Other Zig versions are currently unsupported and may fail to build. Verify before building:
zig versionshould print0.16.0. Debian users who need Zig first can followdocs/en/zig-installation.md.
git clone https://github.com/nullclaw/nullclaw.git
cd nullclaw
zig build -Doptimize=ReleaseSmall
zig build test --summary all
Make nullclaw available on PATH:
macOS/Linux (zsh/bash):
zig build -Doptimize=ReleaseSmall -p "$HOME/.local"
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
# or ~/.bashrc
Windows (PowerShell):
zig build -Doptimize=ReleaseSmall -p "$HOME\.local"
$bin = "$HOME\.local\bin"
$user_path = [Environment]::GetEnvironmentVariable("Path", "User")
if (-not ($user_path -split ";" | Where-Object { $_ -eq $bin })) {
[Environment]::SetEnvironmentVariable("Path", "$user_path;$bin", "User")
}
$env:Path = "$env:Path;$bin"
Then:
nullclaw --help
The repository includes a Makefile wrapper around Docker Compose for local containerized runs.
make build
make config
make up
make config runs nullclaw onboard --interactive inside the agent container.
Pass only non-secret flags through CONFIG_ARGS:
make config CONFIG_ARGS="--provider openrouter"
Avoid putting API keys or bot tokens in CONFIG_ARGS; command-line arguments
can be exposed through shell history and process listings.
The compose gateway defaults to NULLCLAW_PORT=3210, binds inside the
container on 0.0.0.0, and publishes to localhost on the host:
curl http://127.0.0.1:3210/health
Override the port or host bind address when needed:
make up NULLCLAW_PORT=8080
make up NULLCLAW_BIND=0.0.0.0
Operational shortcuts:
make logs
make down
make shell
Only set NULLCLAW_BIND=0.0.0.0 on trusted networks with pairing, webhook
secrets, allowlists, and host firewall rules configured.
# Quick setup
nullclaw onboard --api-key sk-... --provider openrouter
# Or interactive wizard
nullclaw onboard --interactive
# Chat
nullclaw agent -m "Hello, nullclaw!"
# Interactive mode
nullclaw agent
# Start gateway runtime (gateway + all configured channels/accounts + heartbeat + scheduler)
nullclaw gateway # default: 127.0.0.1:3000
nullclaw gateway --port 8080 # custom port
# Check status
nullclaw status
# Run system diagnostics
nullclaw doctor
# Check channel health
nullclaw channel status
# Start specific channels
nullclaw channel start telegram
nullclaw channel start discord
nullclaw channel start signal
# Manage background service
# Linux supports systemd user services and OpenRC
nullclaw service install
nullclaw service status
# Optional secret injection hook for service mode:
# if ~/.nullclaw/service-env is executable, the installed service launcher runs it
# before starting `nullclaw gateway` (for example via dotenvx or sops)
# Migrate memory from OpenClaw
nullclaw migrate openclaw --dry-run
nullclaw migrate openclaw
If you want edge deployment (Cloudflare Worker) with Telegram + OpenAI while keeping agent policy in WASM, see:
examples/edge/cloudflare-worker/
This pattern keeps networking/secrets in the edge host and lets you swap/update logic by replacing a tiny Zig WASM module.
Every subsystem is a vtable interface — swap implementations with a config change, zero code changes.
| Subsystem | Interface | Ships with | Extend |
|---|---|---|---|
| AI Models | Provider | 50+ providers (OpenRouter, Anthropic, OpenAI, Azure OpenAI, Gemini, Vertex AI, Ollama, Venice, NEAR AI Cloud, Atlas Cloud, Evolink, Groq, Mistral, xAI, DeepSeek, Together, Fireworks, Perplexity, Cohere, Bedrock, and many OpenAI-compatible endpoints) | custom:https://your-api.com — any OpenAI-compatible API |
| Channels | Channel | CLI, Telegram, Signal, Discord, Slack, iMessage, Matrix, WhatsApp, Webhook, IRC, Lark/Feishu, OneBot, Line, DingTalk, Email, Nostr, QQ, MaixCam, Mattermost | Any messaging API |
| Memory | Memory | SQLite with hybrid search (FTS5 + vector cosine similarity), Markdown, ClickHouse, PostgreSQL, Redis, LanceDB, Lucid, LRU, API | Any persistence backend |
| Tools | Tool | shell, file_read, file_write, file_edit, file_edit_hashed, file_read_hashed, file_append, memory_store, memory_recall, memory_forget, memory_list, browser_open, screenshot, composio, http_request, web_fetch, web_search, delegate, schedule, hardware_info, hardware_memory, pushover, message, spawn, git, image, i2c, spi, and more | Any capability |
| Observability | Observer | Noop, Log, File, Multi | Prometheus, OTel |
| Runtime | RuntimeAdapter | Native, Docker (sandboxed), WASM (wasmtime) | Any runtime |
| Security | Sandbox | Landlock, Firejail, Bubblewrap, Docker, auto-detect | Any sandbox backend |
| Identity | IdentityConfig | OpenClaw (markdown), AIEOS v1.1 (JSON) | Any identity format |
| Tunnel | Tunnel | None, Cloudflare, Tailscale, ngrok, Custom | Any tunnel binary |
| Heartbeat | Engine | HEARTBEAT.md periodic tasks | — |
| Skills | Loader | TOML/JSON manifests or YAML frontmatter in SKILL.md | Community skill packs |
| Peripherals | Peripheral | Serial, Arduino, Raspberry Pi GPIO, STM32/Nucleo | Any hardware interface |
| Cron | Scheduler | Cron expressions + one-shot timers with JSON persistence | — |
All custom, zero external dependencies for the core path:
| Layer | Implementation |
|---|---|
| Vector DB | Embeddings stored as BLOB in SQLite, cosine similarity search |
| Keyword Search | FTS5 virtual tables with BM25 scoring |
| Hybrid Merge | Weighted merge (configurable vector/keyword weights) |
| Embeddings | EmbeddingProvider vtable — OpenAI, custom URL, or noop |
| Hygiene | Automatic archival + purge of stale memories |
| Snapshots | Export/import full memory state for migration |
| Engines | SQLite (default), Markdown, ClickHouse, PostgreSQL, Redis, LanceDB, Lucid, LRU, API, None |
{
"memory": {
"backend": "sqlite",
"auto_save": true,
"embedding_provider": "openai",
"vector_weight": 0.7,
"keyword_weight": 0.3,
"hygiene_enabled": true,
"snapshot_enabled": false
}
}
nullclaw enforces security at every layer.
| # | Item | Status | How |
|---|---|---|---|
| 1 | Gateway not publicly exposed | Done | Binds 127.0.0.1 by default. Refuses 0.0.0.0 without tunnel or explicit allow_public_bind. |
| 2 | Pairing required | Done | 6-digit one-time code on startup. Exchange via POST /pair for bearer token. |
| 3 | Filesystem scoped | Done | workspace_only = true by default. Null byte injection blocked. Symlink escape detection. |
| 4 | Access via tunnel only | Done | Gateway refuses public bind without active tunnel. Supports Tailscale, Cloudflare, ngrok, or custom. |
| 5 | Sandbox isolation | Done | Auto-detects best backend: Landlock, Firejail, Bubblewrap, or Docker. |
| 6 | Encrypted secrets | Done | API keys encrypted with ChaCha20-Poly1305 using local key file. |
| 7 | Resource limits | Done | Configurable memory, CPU, disk, and subprocess limits. |
| 8 | Audit logging | Done | Signed event trail with configurable retention. |
"*" = allow all (explicit opt-in)Nostr additionally: the owner_pubkey is always allowed regardless of dm_allowed_pubkeys. Private keys are encrypted at rest via SecretStore (enc2: prefix) and only decrypted into memory while the channel is running; zeroed on channel stop.
nullclaw speaks Nostr natively via NIP-17 (gift-wrapped private DMs) and NIP-04 (legacy DMs), using nak.
Prerequisites: Install nak and ensure it's in your $PATH.
Setup via onboarding wizard:
nullclaw onboard --interactive # Step 7 configures Nostr
The wizard will:
Or configure manually in the config.
How it works: On startup, nullclaw announces its DM inbox relays (kind:10050), then listens for incoming NIP-17 gift wraps and NIP-04 encrypted DMs. Outbound messages mirror the sender's protocol. Multi-relay rumor deduplication prevents duplicate responses when the same message is delivered via multiple relays.
Config: ~/.nullclaw/config.json (created by onboard)
OpenClaw compatible: nullclaw uses the same config structure as OpenClaw (snake_case). Providers live under
models.providers, the default model underagents.defaults.model.primary, and channels useaccountswrappers. Top-leveldefault_provider/default_modelkeys are not supported.Vertex AI note:
models.providers.vertex.api_keysupports either:
- a bearer token (
ya29...), or- a full Google service-account JSON object (same shape as Apps Script
GEMINI_KEYwithproject_id,client_email,private_key).
models.providers.vertex.base_urlcan be set explicitly (.../projects/<id>/locations/<loc>/publishers/google/models), or omitted when service-account JSON is used (nullclaw will derive it fromproject_id, withVERTEX_LOCATIONdefaulting toglobal). Service-account mode requiresopensslavailable in$PATHfor RS256 JWT signing.
{
"default_temperature": 0.7,
"models": {
"providers": {
"openrouter": { "api_key": "sk-or-..." },
"nearai": { "api_key": "YOUR_NEARAI_API_KEY" },
"atlas-cloud": { "api_key": "YOUR_ATLASCLOUD_API_KEY" },
"evolink": { "api_key": "YOUR_EVOLINK_API_KEY" },
"groq": { "api_key": "gsk_..." },
"vertex": {
"api_key": {
"type": "service_account",
"project_id": "your-project",
"client_email": "svc@your-project.iam.gserviceaccount.com",
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
},
"base_url": "https://aiplatform.googleapis.com/v1/projects/your-project/locations/global/publishers/google/models"
},
"anthropic": { "api_key": "sk-ant-...", "base_url": "https://api.anthropic.com" }
}
},
"agents": {
"defaults": {
"model": { "primary": "openrouter/anthropic/claude-sonnet-4" },
"heartbeat": { "every": "30m" }
},
"list": [
{ "id": "researcher", "model": { "primary": "openrouter/anthropic/claude-opus-4" }, "system_prompt": "..." }
]
},
"channels": {
"telegram": {
"accounts": {
"main": {
"bot_token": "123:ABC",
"allow_from": ["user1"],
"reply_in_private": true,
"proxy": "socks5://..."
}
}
},
"discord": {
"accounts": {
"main": {
"token": "disc-token",
"guild_id": "12345",
"allow_from": ["user1"],
"allow_bots": false
}
}
},
"irc": {
"accounts": {
"main": {
"host": "irc.libera.chat",
"port": 6697,
"nick": "nullclaw",
"channel": "#nullclaw",
"tls": true,
"allow_from": ["user1"]
},
"meshrelay": {
"host": "irc.meshrelay.xyz",
"port": 6697,
"nick": "nullclaw",
"channels": ["#agents"],
"tls": true,
"nickserv_password": "YOUR_NICKSERV_PASSWORD",
"allow_from": ["*"]
}
}
},
"slack": {
"accounts": {
"main": {
"bot_token": "xoxb-...",
"app_token": "xapp-...",
"allow_from": ["user1"]
}
}
},
"nostr": {
"private_key": "enc2:...",
"owner_pubkey": "hex-pubkey-of-owner",
"relays": ["wss://relay.damus.io", "wss://nos.lol", "wss://relay.nostr.band"],
"dm_allowed_pubkeys": ["*"],
"display_name": "NullClaw",
"about": "AI assistant on Nostr",
"nip05": "nullclaw@yourdomain.com",
"lnurl": "lnurl1..."
}
},
"tools": {
"media": {
"audio": {
"enabled": true,
"language": "ru",
"models": [{ "provider": "groq", "model": "whisper-large-v3" }]
}
}
},
"mcp_servers": {
"filesystem": {
"transport": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem"]
},
"remote": {
"transport": "http",
"url": "https://mcp.example.com/rpc",
"timeout_ms": 10000,
"headers": {
"Authorization": "Bearer example-token"
}
}
},
"memory": {
"backend": "sqlite",
"auto_save": true,
"embedding_provider": "openai",
"vector_weight": 0.7,
"keyword_weight": 0.3
},
"gateway": {
"port": 3000,
"require_pairing": true,
"allow_public_bind": false
},
"autonomy": {
"level": "supervised",
"workspace_only": true,
"max_actions_per_hour": 20
},
"runtime": {
"kind": "native",
"docker": {
"image": "alpine:3.20",
"network": "none",
"memory_limit_mb": 512,
"read_only_rootfs": true
}
},
"tunnel": { "provider": "none" },
"secrets": { "encrypt": true },
"identity": { "format": "openclaw" },
"security": {
"sandbox": { "backend": "auto" },
"resources": { "max_memory_mb": 512, "max_cpu_percent": 80 },
"audit": { "enabled": true, "retention_days": 90 }
}
}
Config values are literal. NullClaw does not expand ${VAR} inside config.json
strings, including custom header values. If you need environment-based secrets,
render config.json ahead of time with your own deployment tooling.
Telegram forum topics:
topic_id field under channels.telegram.agents.list/bind <agent>bindings[].match.peer.id with the canonical thread form "<chat_id>:thread:<topic_id>"."<chat_id>"./bind status shows the current effective route and the available agent ids./bind clear removes only the exact binding for the current account/chat/topic and falls back to the broader route./bind persists an exact bindings[] entry for the current Telegram account and peer./bind status distinguishes an exact local override from an inherited broader fallback.bindings[] does not matter./bind is controlled by channels.telegram.accounts.<id>.binding_commands_enabled.Example:
{
"bindings": [
{
"agent_id": "coder",
"match": {
"channel": "telegram",
"account_id": "main",
"peer": { "kind": "group", "id": "-1001234567890:thread:42" }
}
},
{
"agent_id": "orchestrator",
"match": {
"channel": "telegram",
"account_id": "main",
"peer": { "kind": "group", "id": "-1001234567890" }
}
}
]
}
In that setup, topic 42 routes to coder, while the rest of the forum falls back to orchestrator.
Named agent profiles are configured separately from bindings. Bindings only choose which named agent handles a given chat/topic.
If a named agent should run from its own workspace, set agents.list[].workspace_path.
Relative paths are resolved from the directory that contains config.json, the workspace is scaffolded on first use, and the agent gets a durable memory namespace agent:<agent-id>.
Setting workspace_path does not disable system_prompt: when both are configured, the named profile prompt is still applied and the workspace bootstrap files are loaded from that dedicated workspace.
This applies to nullclaw agent --agent <id>, /subagents spawn --agent <id>, and routed sessions resolved through bindings.
Minimal end-to-end example:
{
"agents": {
"list": [
{
"id": "orchestrator",
"provider": "openrouter",
"model": "anthropic/claude-sonnet-4"
},
{
"id": "coder",
"provider": "ollama",
"model": "qwen2.5-coder:14b",
"system_prompt": "You are the coding agent for this topic."
}
]
},
"channels": {
"telegram": {
"accounts": {
"main": {
"bot_token": "123456:ABCDEF",
"allow_from": ["YOUR_TELEGRAM_USER_ID"],
"binding_commands_enabled": true,
"topic_commands_enabled": true,
"topic_map_command_enabled": true,
"commands_menu_mode": "scoped"
}
}
}
},
"bindings": [
{
"agent_id": "orchestrator",
"match": {
"channel": "telegram",
"account_id": "main",
"peer": { "kind": "group", "id": "-1001234567890" }
}
}
]
}
Operator flow:
/bind coder inside the target forum topic.nullclaw writes a new exact bindings[] entry to ~/.nullclaw/config.json for that topic and Telegram account.nullclaw must have write access to ~/.nullclaw/config.json for /bind to persist changes.About account_id:
account_id identifies the configured Telegram account entry, not a topic and not an agent.channels.telegram.accounts form, the object key becomes the account id. For example, accounts.main means account_id = "main", and accounts.backup means account_id = "backup".bindings, match.account_id limits that binding to one specific Telegram account.match.account_id is omitted, the binding can match any Telegram account for that channel.Effect on delivery:
account_id, so match.account_id = "main" matches only messages received by channels.telegram.accounts.main.account_id = "main" and another to account_id = "sub" does not split one chat across two agents automatically; it scopes each binding to a different configured Telegram account.Use this when you want full web-search provider control plus unrestricted shell command allowlist behavior:
{
"http_request": {
"enabled": true,
"search_base_url": "https://searx.example.com",
"search_provider": "auto",
"search_fallback_providers": ["jina", "duckduckgo"]
},
"autonomy": {
"level": "full",
"allowed_commands": ["*"],
"allowed_paths": ["*"],
"require_approval_for_medium_risk": false,
"block_medium_risk_commands": false,
"block_high_risk_commands": false
}
}
http_request.search_base_url accepts either instance root (https://host) or explicit endpoint (https://host/search); local/private SearXNG instances may also use plain HTTP such as http://localhost:8888 or http://192.168.1.10:8888/search.http_request.search_base_url now fails config validation at startup (no automatic fallback for malformed URL).http_request.search_provider supports: auto, searxng, duckduckgo (ddg), brave, firecrawl, tavily, perplexity, exa, jina.http_request.search_fallback_providers is optional and is tried in order when the primary provider fails.BRAVE_API_KEY, FIRECRAWL_API_KEY, TAVILY_API_KEY, PERPLEXITY_API_KEY, EXA_API_KEY, JINA_API_KEY (or shared WEB_SEARCH_API_KEY where supported). DuckDuckGo and SearXNG do not require API keys.allowed_commands entries support "cmd", "cmd *", and "*" formats.
"cmd" and "cmd *" both allow that command family at the allowlist stage."*" allows any command at the allowlist stage.block_medium_risk_commands: false when you intentionally allow network/transfer commands such as curl/wget or other medium-risk mutations.allowed_paths: ["*"] allows access outside workspace, except system-protected paths.Use channels.web for browser UI events (WebChannel v1):
{
"channels": {
"web": {
"accounts": {
"default": {
"transport": "local",
"listen": "127.0.0.1",
"port": 32123,
"path": "/ws",
"auth_token": "replace-with-long-random-token",
"message_auth_mode": "pairing",
"allowed_origins": ["http://localhost:5173", "chrome-extension://your-extension-id"]
}
}
}
}
}
"listen": "127.0.0.1".message_auth_mode controls inbound user_message auth:
"pairing" (default): send pairing_request, receive pairing_result, include UI access_token in every user_message."token" (local transport only): include auth_token in each user_message payload (access_token is also accepted for compatibility).auth_token hardens the WebSocket upgrade and becomes required when binding non-loopback addresses.127.0.0.1, but a public/LAN bind must authenticate the /ws upgrade on the first hop with ?token=<auth_token> or Authorization: Bearer <auth_token>.pairing_request may omit payload.pairing_code, and legacy loopback clients that still send 123456 remain compatible./ws is the WebSocket endpoint. /pair belongs to the HTTP gateway API and is not part of the web channel handshake."listen": "0.0.0.0", prefer a stable configured token plus message_auth_mode: "token" behind TLS/reverse proxy, or keep loopback bind and expose it through SSH tunnel/proxy.NULLCLAW_WEB_TOKEN, NULLCLAW_GATEWAY_TOKEN, OPENCLAW_GATEWAY_TOKEN).{
"channels": {
"web": {
"accounts": {
"default": {
"transport": "relay",
"relay_url": "wss://relay.nullclaw.io/ws/agent",
"relay_agent_id": "default",
"relay_token": "replace-with-relay-token",
"relay_token_ttl_secs": 2592000,
"relay_pairing_code_ttl_secs": 300,
"relay_ui_token_ttl_secs": 86400,
"relay_e2e_required": false
}
}
}
}
}
relay_token (config) -> NULLCLAW_RELAY_TOKEN (env) -> persisted web-relay-<account_id> credential -> generated token.pairing_request with one-time pairing_code, receive pairing_result with UI access_token JWT (and optional set_cookie string for relay HTTP layer).user_message must include valid UI JWT in access_token (top-level or payload.access_token).relay_e2e_required=true), UI and agent exchange X25519 keys during pairing and send encrypted payloads in payload.e2e.spec/webchannel_v1.json.| Endpoint | Method | Auth | Description |
|---|---|---|---|
/health | GET | None | Health check (always public) |
/pair | POST | X-Pairing-Code header | Exchange one-time code for bearer token |
/webhook | POST | Authorization: Bearer <token> | Send message: {"message": "your prompt"} |
/media/transcribe | POST | Authorization: Bearer <token> | Transcribe audio_base64 payloads through configured STT |
/.well-known/agent-card.json | GET | None | A2A Agent Card discovery (public) |
/a2a | POST | Authorization: Bearer <token> | A2A JSON-RPC endpoint (canonical methods plus legacy slash aliases) |
/whatsapp | GET | Query params | Meta webhook verification |
/whatsapp | POST | None (Meta signature) | WhatsApp incoming message webhook |
NullClaw implements Google's A2A protocol v0.3.0, allowing any A2A-compatible agent or client to discover, authenticate, and interact with your instance over JSON-RPC 2.0.
Enable in ~/.nullclaw/config.json:
{
"a2a": {
"enabled": true,
"name": "nullclaw",
"description": "General-purpose AI assistant",
"url": "https://example.com",
"version": "1.0.0"
}
}
Endpoints:
| Endpoint | Auth | Description |
|---|---|---|
GET /.well-known/agent-card.json | None | Agent Card discovery (public) |
POST /a2a | Bearer token | JSON-RPC 2.0 dispatch |
Supported methods: message/send, message/stream, tasks/get, tasks/cancel, tasks/list, tasks/resubscribe.
Quick test:
# 1. Get a bearer token
# Set PAIRING_CODE to the one-time code for this gateway session.
TOKEN=$(curl -s -X POST -H "X-Pairing-Code: $PAIRING_CODE" http://localhost:3000/pair | jq -r .token)
# 2. Discover the agent
curl http://localhost:3000/.well-known/agent-card.json
# 3. Send a message
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"message/send","params":{"message":{"messageId":"msg-1","role":"user","parts":[{"kind":"text","text":"Hello"}]}}}' \
http://localhost:3000/a2a
See Gateway API docs for full A2A reference including streaming, task lifecycle, and configuration details.
POST /media/transcribe accepts JSON with audio_base64, mime_type, source, and optional language, then returns {"text":"..."} plus metadata. Configure STT under tools.media.audio; remote custom STT endpoints must use HTTPS. Raise gateway.max_body_size_bytes, gateway.request_timeout_secs, and gateway.webhook_rate_limit_per_minute for live desktop audio chunking.
| Command | Description |
|---|---|
onboard --api-key sk-... --provider openrouter | Quick setup with API key and provider |
onboard --interactive | Full interactive wizard |
onboard --channels-only | Reconfigure channels/allowlists only |
agent -m "..." | Single message mode |
acp | Start the Agent Client Protocol stdio adapter for ACP-compatible editors |
agent | Interactive chat mode |
gateway | Start long-running runtime (default: 127.0.0.1:3000) |
service install|start|stop|restart|status|uninstall | Manage background service |
doctor | Diagnose system health |
status [--json] | Show full system status or emit the machine-readable runtime snapshot |
channel list|info|start|status|add|remove | Manage channels, including JSON account inventory for automation |
cron list|status|add|add-agent|once|once-agent|remove|pause|resume|run|update|runs | Manage scheduled tasks |
skills list|install|remove|info | Manage skill packs, including install --name <query> registry search |
history list|show | View session conversation history |
memory stats|count|reindex|search|get|list|export-jsonl|hygiene-report|drain-outbox|forget | Inspect, export, and maintain memory |
hardware scan|flash|monitor | Hardware device management |
config show|get, models list|summary|info|benchmark|refresh | Read config/model admin state and manage the model catalog |
workspace edit|reset-md | Maintain workspace markdown/bootstrap files |
capabilities [--json] | Show runtime capabilities manifest |
auth login|status|logout | Manage OAuth authentication |
migrate openclaw [--dry-run] [--source PATH] | Import memory + migrate config from OpenClaw |
update [--check] [--yes] | Check for and install updates |
Build and tests are pinned to Zig 0.16.0.
zig build # Dev build
zig build -Doptimize=ReleaseSmall # Release build (678 KB)
zig build test --summary all # 5,300+ tests
Channel CJM coverage (ingress parsing/filtering, session key routing, account propagation, bus handoff) is validated by tests in:
src/channel_manager.zig (runtime channel registration/start semantics + listener mode wiring)src/config.zig (OpenClaw-compatible channels.*.accounts parsing, multi-account selection/ordering, aliases)src/gateway.zig (Telegram/WhatsApp/LINE/Lark routed session keys from webhook payloads)src/daemon.zig (gateway-loop inbound route resolution for Discord/QQ/OneBot/Mattermost/MaixCam)src/channels/discord.zig, src/channels/mattermost.zig, src/channels/qq.zig, src/channels/onebot.zig, src/channels/signal.zig, src/channels/line.zig, src/channels/whatsapp.zig (per-channel inbound/outbound contracts)Language: Zig 0.16.0
Source files: ~250
Lines of code: ~249,000
Tests: 5,300+
Binary: 678 KB (ReleaseSmall)
Peak RSS: ~1 MB
Startup: <2 ms (Apple Silicon)
Dependencies: 0 (besides libc + optional SQLite)
src/
main.zig CLI entry point + argument parsing
root.zig Module hierarchy (public API)
config.zig JSON config loader + 30 sub-config structs
agent.zig Agent loop, auto-compaction, tool dispatch
daemon.zig Daemon supervisor with exponential backoff
gateway.zig HTTP gateway (rate limiting, idempotency, pairing)
channels/ 19 channel implementations (telegram, signal, discord, slack, nostr, matrix, whatsapp, line, lark, onebot, mattermost, qq, ...)
providers/ 50+ AI provider integrations
memory/ SQLite backend, embeddings, vector search, hygiene, snapshots
tools/ 35+ tool implementations
security/ Secrets (ChaCha20), sandbox backends (landlock, firejail, ...)
cron.zig Cron scheduler with JSON persistence
health.zig Component health registry
tunnel.zig Tunnel vtable (cloudflare, ngrok, tailscale, custom)
peripherals.zig Hardware peripheral vtable (serial, Arduino, RPi, Nucleo)
runtime.zig Runtime vtable (native, docker, WASM)
skillforge.zig Skill discovery (GitHub), evaluation, integration
...
nullclaw uses CalVer (YYYY.M.D) for releases — e.g. v2026.2.20.
vYYYY.M.D (one release per day max; patch suffix vYYYY.M.D.N if needed)v... -> nullclaw --version)dev unless you override with zig build -Dversion=...nullclaw --version prints the current versionSee CONTRIBUTING.md for development environment setup, workflow, validation commands, and the PR checklist.
Implement a vtable interface, submit a PR:
Provider -> src/providers/Channel -> src/channels/Tool -> src/tools/Memory backend -> src/memory/Tunnel -> src/tunnel.zigSandbox backend -> src/security/Peripheral -> src/peripherals.zigSkill -> ~/.nullclaw/workspace/skills/<name>/ or ~/.nullclaw/workspace/skills/<category>/<name>/nullclaw is a pure open-source software project. It has no token, no cryptocurrency, no blockchain component, and no financial instrument of any kind. This project is not affiliated with any token or financial product.
MIT — see LICENSE
nullclaw — Null overhead. Null compromise. Deploy anywhere. Swap anything.
An agent-managed museum exhibit, built in Rust with Gajae-Code / LazyCodex — developed and maintained with no human intervention.