/install open-notebook
Open Notebook
Self-hosted NotebookLM alternative for OpenClaw agents. The agent reaches open-notebook through a local FastAPI bridge that adds per-agent auth, a per-notebook allowlist, and an audit log.
🔒 Security: Calls go over loopback to a local bridge. The bridge's
X-API-Keyheader is the only auth.
Compatibility
- open-notebook: v1.9.0 or later
- Bridge endpoint contract:
DELETE /v1/sources/{id},GET /v1/sources/{id},GET /v1/notebooks/{id}- all required - Tested embedding model:
perplexity/pplx-embed-v1-4b(OpenRouter)
When to use
- "Save this to my research" / "Add this to my [topic] notebook"
- "What did I save about X?" / "Find my notes on Y"
- "Create a new notebook for Z"
- "Chat with my [topic] research" / "Ask my notes about…"
Do NOT use for: secrets, credentials, ephemeral scratch work. Notebooks are stored unencrypted at rest.
Install / Setup
This skill is a bridge client - it does nothing on its own. You need a running open-notebook deployment and the bridge service.
1. Deploy open-notebook
Follow the lfnovo/open-notebook deployment guide. Run it locally or on a server.
2. Set up the bridge
The bridge is a FastAPI service that wraps the open-notebook API with per-agent auth. See bridge setup below.
3. Install this skill
clawhub install crabsticksalad/open-notebook
4. Configure environment
Add to ~/.openclaw/.env:
OPEN_NOTEBOOK_BRIDGE_URL=http://127.0.0.1:5077
OPEN_NOTEBOOK_API_KEY=\x3Cyour-agent-key>
5. Restart gateway
systemctl --user restart openclaw-gateway
Bridge setup
The bridge is a small FastAPI app (main.py) that:
- Authenticates agents via
X-API-Keyheader - Audits every call to a log file
- Enforces per-notebook allowlists (using
check_notebookfunction)
Minimal bridge main.py
#!/usr/bin/env python3
"""Open Notebook Bridge - auth + audit + per-agent allowlist wrapper."""
import json, os, logging
from pathlib import Path
from fastapi import FastAPI, Header, HTTPException, Depends, Request
from fastapi.responses import Response
import httpx, uvicorn
UPSTREAM = "http://127.0.0.1:5055"
BRIDGE_PORT = int(os.environ.get("BRIDGE_PORT", "5077"))
AGENTS_FILE = Path(os.environ.get("AGENTS_FILE", "agents.json")).expanduser()
AUDIT_LOG = Path(os.environ.get("AUDIT_LOG", "audit.log")).expanduser()
ON_PASSWORD = os.environ.get("OPEN_NOTEBOOK_PASSWORD", "")
AGENTS = json.loads(AGENTS_FILE.read_text()) if AGENTS_FILE.exists() else {}
logging.basicConfig(filename=str(AUDIT_LOG), level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s")
audit = logging.getLogger("audit")
app = FastAPI()
def check_notebook(agent, notebook_id):
"""Enforce allowed_notebooks allowlist. Set allowed_notebooks to ['*'] for full access."""
allowed = agent.get("allowed_notebooks", [])
if allowed == "*" or allowed == ["*"]:
return
if notebook_id not in allowed:
audit.warning(f"DENIED {agent['name']} access to {notebook_id}")
raise HTTPException(403, f"not allowed to access {notebook_id}")
async def auth(x_api_key: str | None = Header(None)):
if not x_api_key or x_api_key not in AGENTS:
audit.warning(f"REJECTED unknown api key ...{x_api_key[-8:]}")
raise HTTPException(401, "invalid api key")
return AGENTS[x_api_key]
@app.get("/v1/health")
async def health():
return {"status": "ok", "agents": len(AGENTS)}
@app.get("/v1/notebooks")
async def list_notebooks(agent=Depends(auth)):
audit.info(f"{agent['name']} GET /v1/notebooks")
async with httpx.AsyncClient(timeout=120) as c:
r = await c.get(f"{UPSTREAM}/api/notebooks", headers={"Authorization": f"Bearer {ON_PASSWORD}"})
return Response(content=r.content, status_code=r.status_code)
@app.post("/v1/notebooks")
async def create_notebook(request: Request, agent=Depends(auth)):
body = await request.json()
audit.info(f"{agent['name']} POST /v1/notebooks")
async with httpx.AsyncClient(timeout=120) as c:
r = await c.post(f"{UPSTREAM}/api/notebooks", json=body, headers={"Authorization": f"Bearer {ON_PASSWORD}"})
return Response(content=r.content, status_code=r.status_code)
@app.get("/v1/notebooks/{notebook_id}")
async def get_notebook(notebook_id: str, agent=Depends(auth)):
check_notebook(agent, notebook_id)
audit.info(f"{agent['name']} GET /v1/notebooks/{notebook_id}")
async with httpx.AsyncClient(timeout=120) as c:
r = await c.get(f"{UPSTREAM}/api/notebooks/{notebook_id}", headers={"Authorization": f"Bearer {ON_PASSWORD}"})
return Response(content=r.content, status_code=r.status_code)
@app.delete("/v1/notebooks/{notebook_id}")
async def delete_notebook(notebook_id: str, agent=Depends(auth)):
check_notebook(agent, notebook_id)
audit.info(f"{agent['name']} DELETE /v1/notebooks/{notebook_id}")
async with httpx.AsyncClient(timeout=120) as c:
r = await c.delete(f"{UPSTREAM}/api/notebooks/{notebook_id}", headers={"Authorization": f"Bearer {ON_PASSWORD}"})
return Response(content=r.content, status_code=r.status_code)
@app.post("/v1/notebooks/{notebook_id}/sources")
async def add_source(notebook_id: str, request: Request, agent=Depends(auth)):
check_notebook(agent, notebook_id)
body = await request.json()
audit.info(f"{agent['name']} POST /v1/notebooks/{notebook_id}/sources")
async with httpx.AsyncClient(timeout=120) as c:
r = await c.post(f"{UPSTREAM}/api/sources/json", json={**body, "notebook_id": notebook_id}, headers={"Authorization": f"Bearer {ON_PASSWORD}"})
return Response(content=r.content, status_code=r.status_code)
@app.get("/v1/sources/{source_id}")
async def get_source(source_id: str, agent=Depends(auth)):
audit.info(f"{agent['name']} GET /v1/sources/{source_id}")
async with httpx.AsyncClient(timeout=120) as c:
r = await c.get(f"{UPSTREAM}/api/sources/{source_id}", headers={"Authorization": f"Bearer {ON_PASSWORD}"})
return Response(content=r.content, status_code=r.status_code)
@app.delete("/v1/sources/{source_id}")
async def delete_source(source_id: str, agent=Depends(auth)):
audit.info(f"{agent['name']} DELETE /v1/sources/{source_id}")
async with httpx.AsyncClient(timeout=120) as c:
r = await c.delete(f"{UPSTREAM}/api/sources/{source_id}", headers={"Authorization": f"Bearer {ON_PASSWORD}"})
return Response(content=r.content, status_code=r.status_code)
@app.post("/v1/search")
async def search(request: Request, agent=Depends(auth)):
body = await request.json()
audit.info(f"{agent['name']} POST /v1/search")
async with httpx.AsyncClient(timeout=120) as c:
r = await c.post(f"{UPSTREAM}/api/search", json=body, headers={"Authorization": f"Bearer {ON_PASSWORD}"})
return Response(content=r.content, status_code=r.status_code)
@app.post("/v1/notebooks/{notebook_id}/chat")
async def ask(notebook_id: str, request: Request, agent=Depends(auth)):
check_notebook(agent, notebook_id)
body = await request.json()
audit.info(f"{agent['name']} POST /v1/notebooks/{notebook_id}/chat")
async with httpx.AsyncClient(timeout=120) as c:
r = await c.post(f"{UPSTREAM}/api/ask", json={**body, "notebook_id": notebook_id}, headers={"Authorization": f"Bearer {ON_PASSWORD}"})
return Response(content=r.content, status_code=r.status_code)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=BRIDGE_PORT)
agents.json
{
"\x3Cyour-agent-key>": {
"name": "agent-name",
"allowed_notebooks": "*",
"readwrite": true
}
}
Bridge env vars
| Variable | Description |
|---|---|
OPEN_NOTEBOOK_PASSWORD |
Password for the open-notebook API (from open-notebook .env) |
AGENTS_FILE |
Path to agents.json (default: agents.json) |
BRIDGE_PORT |
Port to listen on (default: 5077) |
How to use
Use the exec tool to run {baseDir}/scripts/on.sh. All commands return JSON.
| Command | Purpose |
|---|---|
on.sh health |
Bridge + upstream liveness |
on.sh list-notebooks |
List allowed notebooks |
on.sh get-notebook \x3Cid> |
Notebook details |
on.sh create-notebook "Title" "Description" |
Create a new notebook |
on.sh add-source \x3Cnb-id> --text "..." |
Add a source (also --url / --file, optional --title) |
on.sh get-source \x3Cid> |
Check source processing status |
on.sh search "natural language query" |
Cross-notebook vector search |
on.sh ask \x3Cnb-id> "Question?" |
RAG answer with citations, one notebook |
on.sh delete-source \x3Cid> |
Remove a source |
on.sh delete-notebook \x3Cid> |
Remove a notebook (irreversible) |
Conventions
- Notebooks are referenced by their
id(e.g.notebook:v9px33nuskufk4snelyt). When the user says a name, calllist-notebooksfirst to resolve. searchis fast and cross-notebook (uses the embedding model). Use it for "find anything I saved about X".askis slower, scoped to one notebook, and returns a synthesized answer with citations. Use it for "summarize what I know about X".- Adding a source is async on the upstream side. The
add-sourcecall returns the new source ID, but embedding/processing happens in the background. Poll withget-source \x3Cid>untilstatusisprocessedorcompletedbefore relying on the source foraskorsearch.
Errors and recovery
| Symptom | Likely cause | Fix |
|---|---|---|
bridge unreachable (connection refused) |
Bridge service down | Restart the bridge service |
HTTP 401 {"detail":"missing X-API-Key header"} |
OPEN_NOTEBOOK_API_KEY not set |
Verify the var is in ~/.openclaw/.env and restart the gateway |
HTTP 401 {"detail":"invalid api key"} |
Wrong key | Compare to your agents.json |
HTTP 403 agent ... not allowed to access ... |
This agent's allowlist excludes the notebook | list-notebooks to see what this agent can see |
| HTTP 404 | Bad notebook ID | Re-list notebooks to get the correct ID |
| HTTP 500 (get-source) | Bad source ID | Upstream returns 500 for non-existent source IDs - verify the source exists before polling |
Timeout on ask (>120s) |
Upstream LLM call slow | Narrow the question |
Privacy
Notebook data (sources, notes, chat, embeddings, files) is not encrypted at rest in the upstream open-notebook stack - only LLM API keys are encrypted. Do not put secrets, credentials, or sensitive PII into a notebook.
Files
{baseDir}/scripts/on.sh- bridge client (bash, curl + jq with python3 fallback)- Bridge service: runs locally on port 5077; audit log path is deployment-specific
- Per-agent key registry: deployment-specific path (set during bridge setup)
- 确保已安装 OpenClaw(本地或 Docker 部署)
- 在对话框中输入安装命令:
/install open-notebook - 安装完成后,直接呼叫该 Skill 的名称或使用
/open-notebook触发 - 根据 Skill 的参数说明提供必要输入,即可获得结构化输出
Open Notebook Skill 是什么?
Access and manage a self-hosted Open Notebook research system (NotebookLM alternative). Create notebooks, add sources (text/URL/file), cross-notebook search,... 它是一个面向 Claude Code / OpenClaw 的 AI Agent Skill 插件,目前累计下载 41 次。
如何安装 Open Notebook Skill?
在 OpenClaw 或 Claude Code 对话框中运行命令「/install open-notebook」即可一键安装,无需额外配置。
Open Notebook Skill 是免费的吗?
是的,Open Notebook Skill 完全免费,采用 MIT-0 许可证,可自由下载、安装和使用。
Open Notebook Skill 支持哪些平台?
Open Notebook Skill 跨平台运行,可在任意部署了 OpenClaw / Claude Code 的环境中使用(cross-platform)。
谁开发了 Open Notebook Skill?
由 Crabstick(@crabsticksalad)开发并维护,当前版本 v1.2.2。