notebooklm
Query Google NotebookLM notebooks directly from Hermes Agent via Camofox browser automation. Uses the existing Google session from Genspark for zero-additional-auth access. Pure SKILL.md + lightweight Python library helper — no Patchright, no Playwright, no .venv.
Quand l'utiliser (Trigger)
Déclenchement standard selon le contexte de l'écosystème Hermès.
Mode d'emploi (Usage)
Mode d'emploi standard via l'agent Hermès. NotebookLM Skill — Hermes Agent
Query Google NotebookLM notebooks via Camofox, reusing the existing Google session from Genspark. Each query opens the notebook, asks the question, polls for the Gemini answer (source-grounded from your docs), and returns it.
When to Use
- User shares a NotebookLM URL (
https://notebooklm.google.com/notebook/...) - User asks to query their notebooks/documentation
- User mentions NotebookLM explicitly
- You need source-grounded answers from specific uploaded documents
- “query my notebook”, “check my docs”, “ask NotebookLM”
Architecture
~/.hermes/skills/notebooklm/
├── SKILL.md # Agent instructions (this file)
├── library.py # Pure-Python JSON CRUD, no browser deps
├── scripts/
│ └── library.py # Notebook metadata management (stdlib only)
└── data/
└── library.json # Notebook metadata, created on first add
No .venv, no requirements.txt, no Patchright, no Playwright. The browser
automation uses Camofox via Hermes’ native browser_* tools. The Python
helper (library.py) uses stdlib only (argparse, json, pathlib).
Why Not the Upstream Scripts
The original skill (github.com/PleasePrompto/notebooklm-skill) uses Playwright
via patchright for browser automation. Hermes Agent already has:
- Camofox — Firefox with C++ fingerprint spoofing (stronger anti-detection)
- managed_persistence: true — session persists across restarts
- browser_ tools* — navigate, snapshot, click, type, press, console
Running Patchright alongside Camofox means two stealth browsers on one VPS, unable to share state, with separate auth flows. This skill solves everything with a single SKILL.md + a tiny Python library helper.
Auth — Session Persistence Reality
Google OAuth session cookies do NOT persist across Camofox restarts. This is a Google security feature, not a Camofox bug. Google explicitly marks OAuth tokens as non-persistable.
What works:
- Within a single Camofox process: ✅ session stays active
- After soft Camofox restart (same temp profile survives): ✅ sometimes works
- After gateway restart / VPS reboot: ❌ Google login required again
- Cookie backup/restore (see
references/session-persistence.md): ✅ reduces 2FA to first restart only
To verify session:
browser_navigate("https://notebooklm.google.com/") → if you see the notebook
list (not accounts.google.com), auth is valid.
First-Time Auth Flow (recovery after restart)
Use the procedure in references/session-persistence.md:
- Navigate to notebooklm.google.com
- Fill email/password via browser_* tools
- User approves 2FA push notification on phone (15s effort)
- Checkbox “Don’t ask again” is pre-checked ✅
- Run
scripts/backup-cookies.shto save cookies for next restart - Future restarts only need password re-entry (no 2FA)
Notebook List Snapshot (when authenticated)
The NotebookLM homepage shows a rich table with columns:
- Titre — notebook name (may include emoji prefixes like ⚖️🔬🎮)
- Sources — count of source documents
- Création — creation date
- Visibilité — public/private indicator
- Rôle — Owner, Reader, etc.
- Actions — dropdown menu (button ref like
@e14,@e15, etc.)
The snapshot is paginated. You may see a truncated line
[... X more lines truncated...] at the end. Use browser_snapshot()
again or browser_scroll() to reveal more rows.
“Vous avez atteint le nombre maximal de notebooks” — this appears when the free tier limit is hit. It does NOT block querying existing notebooks.
To find a specific notebook: Look for its name in the snapshot rows under
heading "Notebooks récents" [level=2] table. Each row has a button
"Menu d'actions du projet" (ref varies, e.g. @e14-@e26).
To open a notebook: Click on the title cell or use:
browser_navigate("https://notebooklm.google.com/notebook/<UUID>")
Pre-Workflow: Verify Session (each session)
browser_navigate("https://notebooklm.google.com/")
browser_snapshot()
If redirected to accounts.google.com → use recovery in references/session-persistence.md.
Workflow
Step 1: Check Library
terminal(command="python3 ~/.hermes/skills/notebooklm/library.py list")
If the library is empty, ask the user for notebook details or accept a URL directly.
Step 2: Resolve Notebook URL
Two ways to get the URL:
- From library:
terminal(command="python3 ~/.hermes/skills/notebooklm/library.py get-url --id <id>") - From user directly: use the —url they provided
Step 3: Navigate to Notebook
browser_navigate("https://notebooklm.google.com/notebook/<UUID>")
Wait for the page to load. If redirected to accounts.google.com, surface the auth issue and run session recovery procedure.
Step 4: Detect and Interact with Query Input
⚠️ CRITICAL: NotebookLM’s chat textarea is a React-controlled element that
DOES NOT appear in the accessibility tree. Never look for it in the
browser_snapshot() output — always use browser_console JavaScript.
There are always TWO <textarea> elements in the DOM:
textarea[0]: Source search input (placeholder: “Rechercher de nouvelles sources sur le Web”) — in the left paneltextarea[1]: Chat query input (placeholder: “Posez une question ou créez quelque chose”) — THE ONE YOU NEED
Verifying which is which:
document.querySelectorAll('textarea')[1].placeholder
// → "Posez une question ou créez quelque chose"
Filling the input — MUST use execCommand for React to detect the change:
The native value setter (Object.getOwnPropertyDescriptor(...).set) and
.textContent assignment both FAIL to trigger React’s onChange handler.
Only execCommand('insertText') works reliably:
const ta = document.querySelectorAll('textarea')[1];
ta.focus();
document.execCommand('insertText', false, "Your question here");
This is the only method that activates the submit button (disabled → false).
Submit — click the form’s submit button (Enter does NOT work):
NotebookLM’s React handler ignores keyboard events entirely. You must find and click the submit button programmatically:
const ta = document.querySelectorAll('textarea')[1];
const form = ta.closest('form');
const submitBtn = form.querySelector('button[type="submit"]');
if (!submitBtn.disabled) {
submitBtn.click();
}
Combined command (fill + submit in one shot):
(function(){
const ta = document.querySelectorAll('textarea')[1];
ta.focus();
document.execCommand('insertText', false, "Your question here");
const btn = ta.closest('form').querySelector('button[type="submit"]');
if (btn.disabled) { return "button still disabled — execCommand didn't trigger React"; }
btn.click();
return "submitted";
})()
Verify the question was submitted:
document.body.innerText.includes("Your question here")
Step 5: Wait for Response — PATIENCE IS MANDATORY
NotebookLM generates answers via Gemini, scanning ALL uploaded sources. With 30+ large legal documents, the full cycle takes 45-120 seconds.
🚨 CRITICAL BEHAVIOR RULE — do NOT poll frantically:
- Poll ONCE every 15 seconds, maximum.
- Each poll is a full page snapshot that consumes tokens.
- Between polls, truly wait — use
terminal(command="sleep 15"). - The user will tell you if they want an update. Do not send progress messages.
- If the user says “be patient like a human” — listen immediately.
- A human would glance at the screen, note “still loading”, and go back to work. Do that.
Status indicators during generation (progression order):
| Status | Phase |
|---|---|
| “Retrieving details…” | Looking up |
| “Parsing the data…” | Processing |
| “Checking your uploads…” | Scanning docs |
| “Assessing relevance…” | Matching sources |
| “Consulting your sources…” | Cross-referencing |
| “Reading through pages…” | Long docs |
| “Reading your inputs…” | Thorough review |
| “Looking at sources…” | Active research |
| “Réflexion…” | Final generation |
Polling approach:
- Execute combined fill+submit command
terminal(command="sleep 15")— actually waitbrowser_snapshot()— check for answer text or status indicator- If still generating →
terminal(command="sleep 15")again - Max timeout: 120 seconds (8 polls)
Indicators of complete response:
- Full text visible in the response area
- Source citation buttons appear (e.g. “1: 02_Charte_canadienne_droits_libertes.txt”)
- Input field becomes editable again (placeholder: “Posez une question…”)
- Status indicator text (“Réflexion…”, “Reading…”) is GONE
Extracting the answer:
document.body.innerText
The answer text is between the question heading and the “Studio” section. Include source citations — they link back to the uploaded documents.
Step 6: Return Result
Format the answer for the user. Include sources/citations if present.
If the answer is “I could not find…” or NotebookLM couldn’t answer, tell the user honestly rather than fabricating.
IMPORTANT: After returning the answer, ask the user if they need more.
library.py — Helper Script
A ~80-line Python script with stdlib-only dependencies. It manages a JSON
library of notebooks with metadata. Invoke via terminal.
# List all notebooks
python3 ~/.hermes/skills/notebooklm/library.py list
# Add a notebook
python3 ~/.hermes/skills/notebooklm/library.py add \
--url "https://notebooklm.google.com/notebook/..." \
--name "My Docs" \
--description "Project documentation" \
--topics "docs,api,guide"
# Get URL for a notebook (for navigation)
python3 ~/.hermes/skills/notebooklm/library.py get-url --id <id>
# Search notebooks
python3 ~/.hermes/skills/notebooklm/library.py search --query "keyword"
# Set active notebook
python3 ~/.hermes/skills/notebooklm/library.py activate --id <id>
# Remove notebook
python3 ~/.hermes/skills/notebooklm/library.py remove --id <id>
Always use get-url to resolve the URL before navigation. The library
stores the full URL — no need for the user to re-copy it.
Follow-up Questions
Each question opens a new browser session. To ask a follow-up:
# Step 4 combined command with context from previous answer
browser_console(expression="(function(){...fill+submit...})()")
# Wait patiently with sleep 15
# Extract with document.body.innerText
IMPORTANT: Each question must be self-contained — include enough context from prior answers so the new response doesn’t repeat work. NotebookLM’s session history is not preserved across browser navigations.
SAVE the notebook URL after the first interaction so follow-ups don’t require re-navigating from the homepage.
Pitfalls
⚠️ Session lost after gateway restart (known limitation)
managed_persistence: true does NOT persist Google OAuth cookies across
gateway restarts. See references/session-persistence.md.
Quick recovery:
browser_navigate("https://notebooklm.google.com/")- Fill email/password
- Tell user “Approve the 2FA notification on your phone”
- After login, run
bash ~/scripts/backup-cookies.sh
⚠️ Chat textarea not in accessibility tree
The React textarea NEVER appears in browser_snapshot(). Always detect
and fill it via browser_console JavaScript.
⚠️ Enter key and nativeSetter both fail
browser_press(key="Enter")— ignored by ReactObject.getOwnPropertyDescriptor(...).set— does NOT trigger onChange- Only
document.execCommand('insertText', false, text)works
⚠️ Long generation time — BE PATIENT
31 large sources = 45-120s generation. Poll every 15s max. Never poll
every 3 seconds — this is annoying, burns tokens, and looks impatient.
Use terminal(command="sleep 15") between polls.
⚠️ Submit button may remain disabled
If execCommand didn’t trigger React, the submit button stays disabled.
Verify with: ta.closest('form').querySelector('button[type="submit"]').disabled
If still disabled, try focusing the textarea first before execCommand.
⚠️ Notebook doesn’t load (empty page)
Some NotebookLM notebooks require certain Google permissions.
⚠️ Rate limits
Google accounts have daily rate limits for NotebookLM queries (~50/day on free tier).
⚠️ Cookies backup required after login
After successful login, ALWAYS run bash ~/scripts/backup-cookies.sh.
Best Practices
- Use the library — save notebook URLs for reuse
- Verify session each time — navigate to notebooklm.google.com first
- Backup cookies after login —
bash ~/scripts/backup-cookies.sh - Detect textarea via JS, not snapshot — always use browser_console
- BE PATIENT — 45-120s for large notebooks, poll every 15s max, use sleep
- Use execCommand, not nativeSetter — only execCommand triggers React
- Submit via form button, not Enter — React ignores keyboard events
- Don’t send progress messages — wait silently, deliver the answer
- Self-contained questions — include context from prior responses
- Check for “Sources” — NotebookLM cites source documents, include them
References
references/session-persistence.md— Google OAuth session persistencereferences/notebooklm-selectors.md— DOM selectors and interaction patternsscripts/backup-cookies.sh— Backup Google cookies to~/.camofox/cookies/scripts/restore-cookies.sh— Restore cookies after Camofox restart