- AI
- Notebooklm
- Claude skills
- Skill md
- Templates
- Knowledge
NotebookLM Skills Factory
Turn a pile of your own sources (PDFs, docs, playbooks, course notes, YouTube talks) into a Claude skill that follows your method instead of the internet's average. You load the sources into NotebookLM; this skill hands you the exact prompt, checks the SKILL.md NotebookLM writes (name and description rules, sections, sources behind every rule), installs it, and runs three tests before you trust it. Use on "make a skill from my notes", "turn this course into a skill", "NotebookLM skill", "skills factory", "FACTORY template", or when someone wants Claude to do a recurring job their way.
SKILL.md
NotebookLM Skills Factory
A skill is a folder with one SKILL.md that tells Claude how to do one job your way. The hard
part is writing it. NotebookLM answers from the sources you load into a notebook and cites them,
so the method it writes out comes from your material, not from whatever sounds right. This skill
runs the loop around it:
- one notebook per job
- NotebookLM writes the SKILL.md from those sources (you paste the prompt below)
- you check the citations and paste the answer back here
- this skill checks the file, installs it, and tests it
You cannot open NotebookLM from here: it has no API. Steps 1 to 3 happen in the person's browser. Everything else is yours.
1. Pick one job
Ask in one message:
- Which job should the skill do? One job only ("cold emails to agency owners", "landing page hero sections", "support replies about refunds"), never "marketing".
- What sources do they have for it: their best examples, a playbook, a course, docs, videos?
- Where should the skill live: Claude Code, the Claude app, or another agent?
If they name several jobs, do the one they repeat most first, and list the others for later.
2. Hand over the notebook steps
Tell them, short:
- Open https://notebooklm.google.com and create a notebook named after the job.
- Add only the sources for that job: PDFs, Google Docs, pasted text, web pages, YouTube links. Their own best work counts as a source. Mixing jobs gives a vague skill.
- Paste the prompt below into the notebook chat.
- Click a few citation numbers on the rules that matter. Delete any rule they cannot trace.
- Paste the whole answer back here, including the "Sources" list at the end.
Give them templates/notebooklm-prompt.md with {{JOB}} replaced by the job and {{NAME}} by a
lowercase-hyphen name you suggest (max 64 characters, no "claude" or "anthropic", e.g.
cold-email-agencies). Send the prompt as one block they can copy.
If they want to see the shape first, show templates/SKILL.starter.md.
3. Check what comes back
Save their paste to a working file, then:
python3 scripts/check_skill.py <work>/SKILL.md --fix
--fix makes only mechanical repairs: strips code fences and chat text around the file, fixes
the name's format, and moves the Sources list into SOURCES.md next to the skill (kept for
checking, not loaded every time). Then read the file yourself:
- Errors stop the skill loading. Fix them before anything else.
- No "use when" in the description: rewrite it to name the requests that should trigger it. Claude chooses skills by the description alone.
- A rule with no source behind it: point it out and ask whether to keep it. Never add rules of your own; if the method has a gap, the fix is a better source in the notebook.
- Over ~500 lines: move long reference material (price lists, full examples) into a separate file in the skill folder and link to it from SKILL.md.
Show them the final file before installing.
4. Install it
bash scripts/install_skill.sh <work>/SKILL.md --claude-code # ~/.claude/skills/<name>/
bash scripts/install_skill.sh <work>/SKILL.md --zip # <name>.zip for the Claude app
bash scripts/install_skill.sh <work>/SKILL.md --dir <folder> # any other agent's skills folder
It runs the checker again and never overwrites an existing skill. For the Claude app: Settings, Capabilities, Skills, upload the zip (needs a paid plan with code execution on). Skills do not sync between Claude Code, the app and the API; install it in each place they use.
5. Test it before they trust it
Fill in templates/tests.md with three requests for their job: a normal one, an edge case the
sources barely cover, and a near miss that should not use the skill. Have them run each in a
fresh conversation (or run them yourself if the skill is installed where you are), score against
the skill's own Checklist, and record pass or fail.
- A fail on the normal request: tighten the Method or Rules.
- New rules appearing on the edge case: the model filled a gap. Add a source that covers it, or add a rule saying what to do, then regenerate that section in NotebookLM.
- The near miss triggers it: narrow the description.
Fix SKILL.md, rerun only the failed tests, and stop when all three pass.
6. Next job
One notebook and one skill per job. When they add a new source, regenerate the affected section in NotebookLM and run the checker and tests again. Offer the next job on their list.
Rules
- The method comes from their sources, never from you. Missing is missing; say so.
- NotebookLM grounds a skill in the sources; it does not make the sources right. Say this plainly if they expect "no mistakes".
- Never overwrite or delete an existing skill. Never install anything they have not seen.
- Respect copyright: a skill built from a paid course is for their own use, not for publishing.
Files
5 files ship with this skill. They are installed beside the SKILL.md.
#!/usr/bin/env python3
"""Check a SKILL.md (or a skill folder) before installing it. Standard library only.
Usage:
python3 scripts/check_skill.py <SKILL.md or skill folder>
python3 scripts/check_skill.py <file> --fix # also writes the cleaned file back
Errors (exit 1) are things that stop the skill loading: missing or bad frontmatter,
a name that breaks the rules, an empty or over-long description.
Warnings are quality problems worth fixing: no "use when" in the description,
missing sections, no sources list, a file so long it costs context on every use.
--fix only does safe, mechanical repairs: strips code fences NotebookLM sometimes wraps
the answer in, drops chat text before the frontmatter, lowercases/hyphenates the name,
and moves a trailing "## Sources" section into SOURCES.md next to the skill.
"""
import os, re, sys
NAME_RE = re.compile(r'^[a-z0-9]+(-[a-z0-9]+)*$')
SECTIONS = ['Goal', 'Method', 'Rules', 'Examples', 'Mistakes to avoid', 'Checklist']
def parse(text):
m = re.match(r'^---\s*\n(.*?)\n---\s*\n?(.*)$', text, re.S)
if not m:
return None, text
fm = {}
key = None
for line in m.group(1).splitlines():
km = re.match(r'^([A-Za-z_][\w-]*):\s*(.*)$', line)
if km:
key = km.group(1)
fm[key] = km.group(2).strip()
elif key and line.startswith((' ', '\t')):
fm[key] = (fm[key] + ' ' + line.strip()).strip()
for k, v in fm.items():
if v in ('>', '|', '>-', '|-'):
fm[k] = ''
fm[k] = fm[k].strip().strip('"').strip("'")
return fm, m.group(2)
def clean(text):
text = text.strip()
text = re.sub(r'^```[a-zA-Z]*\s*\n', '', text)
text = re.sub(r'\n```\s*$', '', text)
i = text.find('---\n')
if i > 0:
text = text[i:]
return text.strip() + '\n'
def main():
if len(sys.argv) < 2:
sys.exit(__doc__)
path = sys.argv[1]
fix = '--fix' in sys.argv
if os.path.isdir(path):
path = os.path.join(path, 'SKILL.md')
if not os.path.exists(path):
sys.exit(f'not found: {path}')
raw = open(path, encoding='utf-8').read()
text = clean(raw)
errors, warnings, notes = [], [], []
fm, body = parse(text)
if fm is None:
errors.append('No frontmatter. The file must start with --- , name: , description: , --- .')
fm, body = {}, text
name = fm.get('name', '')
if fix and name and not NAME_RE.match(name):
new = re.sub(r'[^a-z0-9]+', '-', name.lower()).strip('-')[:64]
text = text.replace(f'name: {name}', f'name: {new}', 1)
notes.append(f'name "{name}" -> "{new}"')
name = new
if not name:
errors.append('name is missing.')
else:
if len(name) > 64:
errors.append(f'name is {len(name)} characters; the limit is 64.')
if not NAME_RE.match(name):
errors.append(f'name "{name}" must be lowercase letters, numbers and hyphens only.')
if 'claude' in name or 'anthropic' in name:
errors.append(f'name "{name}" cannot contain the reserved words "claude" or "anthropic".')
desc = fm.get('description', '')
if not desc:
errors.append('description is missing or empty.')
else:
if len(desc) > 1024:
errors.append(f'description is {len(desc)} characters; the limit is 1,024.')
if re.search(r'<[^>]+>', desc):
errors.append('description cannot contain XML or HTML tags.')
if not re.search(r'\buse (it )?(when|for|on)\b', desc, re.I):
warnings.append('description does not say when to use the skill ("Use when ..."). Claude picks skills by the description, so name the requests that should trigger it.')
if len(desc) < 60:
warnings.append('description is very short; it may not trigger reliably.')
heads = [h.strip() for h in re.findall(r'^##\s+(.+)$', body, re.M)]
missing = [s for s in SECTIONS if not any(h.lower().startswith(s.lower()) for h in heads)]
if missing:
warnings.append('missing sections: ' + ', '.join(missing))
src = re.search(r'^##\s+Sources\b.*$', body, re.M | re.I)
has_sources_file = os.path.exists(os.path.join(os.path.dirname(os.path.abspath(path)), 'SOURCES.md'))
if not src and not has_sources_file:
warnings.append('no "## Sources" list. Ask NotebookLM which source supports each rule, and delete any rule it cannot trace.')
elif fix:
sources = body[src.start():].strip()
body_wo = body[:src.start()].rstrip() + '\n'
text = text.replace(body, body_wo, 1) if body in text else text
out = os.path.join(os.path.dirname(os.path.abspath(path)), 'SOURCES.md')
open(out, 'w', encoding='utf-8').write(sources + '\n')
notes.append(f'moved the Sources list to {out} (kept for checking, not loaded on every use)')
lines = text.count('\n')
if lines > 500:
warnings.append(f'{lines} lines. Keep SKILL.md under ~500 lines; move long reference material into a separate file in the skill folder.')
for bad in ('as an ai', 'i cannot', "i can't access", 'based on the sources provided'):
if bad in text.lower():
warnings.append(f'contains chat filler ("{bad}"); delete it so it does not become an instruction.')
if fix and (text != raw or notes):
open(path, 'w', encoding='utf-8').write(text)
notes.append(f'wrote {path}')
print(f'skill: {name or "?"} ({lines} lines)')
for n in notes:
print(' fixed ' + n)
for e in errors:
print(' ERROR ' + e)
for w in warnings:
print(' warn ' + w)
if not errors and not warnings:
print(' ok ready to install')
sys.exit(1 if errors else 0)
if __name__ == '__main__':
main()