Run these steps on the OpenClaw host to give the bot free, unlimited web search (no API key needed).
mkdir -p /opt/searxng
SECRET=$(openssl rand -hex 32)
cat > /opt/searxng/settings.yml << EOF
use_default_settings: true
server:
secret_key: "$SECRET"
bind_address: "0.0.0.0"
port: 8080
limiter: false
search:
safe_search: 0
formats:
- html
- json
outgoing:
request_timeout: 5.0
EOFdocker run -d \
--name searxng \
--restart unless-stopped \
--network bridge \
-p 127.0.0.1:8888:8080 \
-v /opt/searxng/settings.yml:/etc/searxng/settings.yml:ro \
--memory=256m \
--cpus=0.5 \
searxng/searxng:latestVerify it works:
sleep 3 && curl -s 'http://127.0.0.1:8888/search?q=test&format=json' | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['results'][0]['title'])"cat > /usr/local/bin/websearch << 'SCRIPT'
#!/bin/bash
set -euo pipefail
if [ $# -eq 0 ]; then echo "Usage: websearch <query> [--count N]"; exit 1; fi
COUNT=5; QUERY=""
while [ $# -gt 0 ]; do
case "$1" in --count) COUNT="$2"; shift 2 ;; *) QUERY="${QUERY:+$QUERY }$1"; shift ;; esac
done
[ -z "$QUERY" ] && echo "Error: No query" && exit 1
ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$QUERY")
RESULT=$(curl -s --max-time 15 "http://127.0.0.1:8888/search?q=${ENCODED}&format=json&categories=general")
[ -z "$RESULT" ] && echo "Error: No response" && exit 1
python3 -c "
import json, sys
data = json.loads(sys.stdin.read())
count = int(sys.argv[1])
for i, r in enumerate(data.get('results', [])[:count], 1):
title = r.get('title', 'No title')
url = r.get('url', '')
content = r.get('content', 'No description').replace('\n', ' ')
if len(content) > 200: content = content[:200] + '...'
print(f'[{i}] {title}\n {url}\n {content}\n')
suggestions = data.get('suggestions', [])[:3]
if suggestions: print('Related: ' + ', '.join(suggestions))
" "$COUNT" <<< "$RESULT"
SCRIPT
chmod +x /usr/local/bin/websearchImportant: Uses Python for JSON parsing, NOT jq. Host jq is compiled against glibc 2.38 but the OpenClaw sandbox runs Debian Bookworm with older glibc — bind-mounted jq will crash with GLIBC_2.38 not found.
cat > /usr/local/bin/websearch-sandbox << 'SCRIPT'
#!/bin/bash
set -euo pipefail
if [ $# -eq 0 ]; then echo "Usage: websearch <query> [--count N]"; exit 1; fi
COUNT=5; QUERY=""
while [ $# -gt 0 ]; do
case "$1" in --count) COUNT="$2"; shift 2 ;; *) QUERY="${QUERY:+$QUERY }$1"; shift ;; esac
done
[ -z "$QUERY" ] && echo "Error: No query" && exit 1
SEARX_URL=""
for host in "172.17.0.2:8080" "172.17.0.3:8080" "172.17.0.4:8080" "172.17.0.5:8080"; do
if curl -s --max-time 2 "http://$host/" >/dev/null 2>&1; then SEARX_URL="http://$host"; break; fi
done
[ -z "$SEARX_URL" ] && echo "Error: Cannot reach SearXNG" && exit 1
ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$QUERY")
RESULT=$(curl -s --max-time 15 "${SEARX_URL}/search?q=${ENCODED}&format=json&categories=general")
[ -z "$RESULT" ] && echo "Error: No response" && exit 1
python3 -c "
import json, sys
data = json.loads(sys.stdin.read())
count = int(sys.argv[1])
for i, r in enumerate(data.get('results', [])[:count], 1):
title = r.get('title', 'No title')
url = r.get('url', '')
content = r.get('content', 'No description').replace('\n', ' ')
if len(content) > 200: content = content[:200] + '...'
print(f'[{i}] {title}\n {url}\n {content}\n')
suggestions = data.get('suggestions', [])[:3]
if suggestions: print('Related: ' + ', '.join(suggestions))
" "$COUNT" <<< "$RESULT"
SCRIPT
chmod +x /usr/local/bin/websearch-sandboxAdd this string to the agents.defaults.sandbox.docker.binds array:
"/usr/local/bin/websearch-sandbox:/usr/local/bin/websearch:ro"
Bind mounts are set at container creation time. Old sandbox containers will NOT pick up new mounts.
# Remove old sandbox containers
docker ps -q --filter 'name=openclaw-sbx' | xargs -r docker rm -f
# Restart OpenClaw
systemctl restart openclaw
# Send a test message via Telegram to trigger sandbox creation
# Then wait for the new container:
docker ps --filter 'name=openclaw-sbx'
# Verify INSIDE the new sandbox (not from host!)
docker exec <container-name> which websearch
docker exec <container-name> websearch "test query" --count 2
# Only declare success after the above returns actual search results- Do NOT use jq in sandbox scripts — host jq has glibc mismatch with sandbox
- Container IPs shift on restart — sandbox script auto-tries 172.17.0.2-5
- Bind mounts are set at container creation — old containers must be removed and recreated
- Test from inside the sandbox, not from the host — network access differs
- SearXNG aggregates Google, DuckDuckGo, Brave, Bing, Wikipedia — no API keys, no rate limits, no reCAPTCHA