From nanotars-webhook
Adds per-group webhook HTTP endpoints to NanoTars for external services like Home Assistant, uptime monitors, or Proxmox to POST events that trigger agent turns, avoiding cron polling.
npx claudepluginhub terrifiedbug/nanotars-skills --plugin nanotars-webhookThis skill uses the workspace's default tool permissions.
HTTP webhook endpoint for NanoTars. External services POST events to per-group endpoints, which get injected into the message pipeline — no cron polling needed.
Adds n8n workflow automation integration to NanoTars agents, enabling webhook-triggered monitoring workflows to avoid token-intensive polling. Guides n8n MCP server setup, API key config, and env variables.
Installs WhatsApp channel plugin for NanoTars using Baileys library and authenticates via QR code or pairing code. Triggers on 'add whatsapp', 'whatsapp setup', 'whatsapp channel'.
Integrates self-hosted changedetection.io with NanoTars via API keys and webhooks to enable agents for website change detection, price monitoring, and stock alerts.
Share bugs, ideas, or general feedback.
HTTP webhook endpoint for NanoTars. External services POST events to per-group endpoints, which get injected into the message pipeline — no cron polling needed.
Each group gets its own URL path (/webhook/<group-folder>) and unique secret token for isolation.
Before installing, verify NanoTars is set up:
[ -d node_modules ] && echo "DEPS: ok" || echo "DEPS: missing"
docker image inspect nanoclaw-agent:latest &>/dev/null && echo "IMAGE: ok" || echo "IMAGE: not built"
if grep -q "ANTHROPIC_API_KEY\|CLAUDE_CODE_OAUTH_TOKEN" .env 2>/dev/null || [ -f "$HOME/.claude/.credentials.json" ]; then echo "AUTH: ok"; else echo "AUTH: missing"; fi
If any check fails, tell the user to run /nanotars-setup first and stop.
/nanotars-add-group)[ -d plugins/webhook ] && echo "PLUGIN_EXISTS" || echo "NEED_PLUGIN"
[ -f data/webhook-routes.json ] && echo "ROUTES_EXIST" || echo "NEED_ROUTES"
If NEED_PLUGIN: copy plugin files first:
cp -r ${CLAUDE_PLUGIN_ROOT}/files/ plugins/webhook/
Ask the user which groups should have access to this plugin:
mainIf restricting, update plugins/webhook/plugin.json to set "groups" to the list of group folder names.
Also ask about channel types. Leave "channels": ["*"] for all, or set to specific types (e.g., ["whatsapp"]).
If data/webhook-routes.json exists, read and display current routes:
cat data/webhook-routes.json
Show the user which groups already have webhook endpoints (group name + creation date). Never display full tokens — show only the first 8 characters.
If no routes exist (first run): skip to step 4.
If routes exist (re-run), ask:
Query the database for registered groups and show the user the list. Ask which group should receive webhook events.
sqlite3 store/messages.db "
SELECT mg.platform_id AS jid, COALESCE(mg.name, ag.name) AS name, ag.folder
FROM agent_groups ag
LEFT JOIN messaging_group_agents mga ON mga.agent_group_id = ag.id
LEFT JOIN messaging_groups mg ON mg.id = mga.messaging_group_id
"
# Generate unique token
TOKEN="whk_$(openssl rand -hex 32)"
# Read existing routes or create new file
if [ -f data/webhook-routes.json ]; then
# Add new route using jq
jq --arg folder "GROUP_FOLDER" --arg secret "$TOKEN" --arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'.routes[$folder] = {secret: $secret, createdAt: $date}' data/webhook-routes.json > data/webhook-routes.tmp \
&& mv data/webhook-routes.tmp data/webhook-routes.json
else
# Create new routes file
cat > data/webhook-routes.json << EOF
{
"routes": {
"GROUP_FOLDER": {
"secret": "$TOKEN",
"createdAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
}
}
EOF
fi
Replace GROUP_FOLDER with the actual group folder name (e.g., main, family-chat).
If WEBHOOK_PORT and WEBHOOK_HOST are not already in .env:
Ask the user which network interface to bind on:
WEBHOOK_HOST=127.0.0.1. Only services on this machine can reach the webhook. Use this if external services connect via a VPN/tunnel (WireGuard, Tailscale) that terminates locally, or if you use a reverse proxy (nginx, Caddy).WEBHOOK_HOST=0.0.0.0. Services on the LAN can reach the webhook directly. Convenient for Home Assistant, n8n, etc. on the same network.If the user chooses all interfaces, warn them:
Security note: Binding to all interfaces exposes the webhook to your entire network. The endpoint is protected by Bearer token auth, but the token is sent in cleartext over HTTP. Recommendations:
- Do not expose the port to the internet — use a firewall rule to restrict access to your LAN/VPN only
- For remote access, use a VPN (WireGuard, Tailscale, Pangolin) rather than port-forwarding
- If you must expose it publicly, put it behind a reverse proxy with TLS (nginx, Caddy)
Save the choice:
echo "WEBHOOK_PORT=3457" >> .env
echo "WEBHOOK_HOST=127.0.0.1" >> .env # or 0.0.0.0
Webhook endpoint created:
URL: http://HOST:PORT/webhook/GROUP_FOLDER
Token: whk_...full token here...
Test:
curl -s -X POST http://HOST:PORT/webhook/GROUP_FOLDER \
-H "Authorization: Bearer whk_..." \
-H "Content-Type: application/json" \
-d '{"source": "test", "text": "Hello from webhook test!"}' | jq .
For the main group, also mention: POST /webhook (no path suffix) routes to main as a shorthand.
npm run build && nanotars restart # or launchctl on macOS
The routes file is also hot-reloaded — after the first restart, adding new routes takes effect without restarting.
If the user wants to regenerate a token for an existing group:
TOKEN="whk_$(openssl rand -hex 32)"
jq --arg folder "GROUP_FOLDER" --arg secret "$TOKEN" --arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'.routes[$folder].secret = $secret | .routes[$folder].createdAt = $date' data/webhook-routes.json > data/webhook-routes.tmp \
&& mv data/webhook-routes.tmp data/webhook-routes.json
If .env contains NANOCLAW_WEBHOOK_SECRET but no data/webhook-routes.json exists:
SECRET=$(grep "^NANOCLAW_WEBHOOK_SECRET=" .env | cut -d= -f2)
cat > data/webhook-routes.json << EOF
{
"routes": {
"main": {
"secret": "$SECRET",
"createdAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
}
}
EOF
NANOCLAW_WEBHOOK_SECRET from .env."Read the token for the target group:
TOKEN=$(jq -r '.routes["GROUP_FOLDER"].secret' data/webhook-routes.json)
curl -s -X POST http://localhost:3457/webhook/GROUP_FOLDER \
-H "Content-Type: application/json" \
-d '{"source": "test", "text": "hello"}' | jq .
curl -s -X POST http://localhost:3457/webhook/GROUP_FOLDER \
-H "Authorization: Bearer wrong-token" \
-H "Content-Type: application/json" \
-d '{"source": "test", "text": "hello"}' | jq .
curl -s -X POST http://localhost:3457/webhook/GROUP_FOLDER \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"source": "test", "text": "This is a test webhook message. Reply with OK if you received it."}' | jq .
sqlite3 store/messages.db "SELECT id, sender_name, content, timestamp FROM messages WHERE id LIKE 'wh-%' ORDER BY timestamp DESC LIMIT 5"
automation:
- alias: "Notify NanoTars on motion"
trigger:
- platform: state
entity_id: binary_sensor.front_door_motion
to: "on"
action:
- service: rest_command.nanotars_webhook
data:
source: home-assistant
text: "Motion detected on front door camera at {{ now().strftime('%H:%M') }}"
rest_command:
nanotars_webhook:
url: "http://NANOCLAW_IP:3457/webhook/main"
method: POST
headers:
Authorization: "Bearer YOUR_WEBHOOK_TOKEN"
Content-Type: "application/json"
payload: '{"source": "{{ source }}", "text": "{{ text }}"}'
curl -X POST http://NANOCLAW_IP:3457/webhook/admin \
-H "Authorization: Bearer YOUR_ADMIN_WEBHOOK_TOKEN" \
-H "Content-Type: application/json" \
-d '{"source": "uptime-kuma", "text": "ALERT: website.com is DOWN. Status: 503."}'
data/webhook-routes.json — each group gets a unique path and tokenPOST /webhook/<group-folder> validates the per-group Bearer token, parses JSON body { source, text }POST /webhook (no suffix) routes to the main group as a shorthandctx.insertMessage()http, crypto, fs127.0.0.1). Can be opened to LAN via WEBHOOK_HOST=0.0.0.0jq 'del(.routes["GROUP_FOLDER"])' data/webhook-routes.json > data/webhook-routes.tmp \
&& mv data/webhook-routes.tmp data/webhook-routes.json
rm -rf plugins/webhook/rm -f data/webhook-routes.json.env:
sed -i '/^NANOCLAW_WEBHOOK_SECRET=/d' .env
sed -i '/^WEBHOOK_PORT=/d' .env
sed -i '/^WEBHOOK_HOST=/d' .env
sed -i '/^NANOCLAW_WEBHOOK_URL=/d' .env