From claude-skills
Captures learning moments from Claude sessions and persists them to a GitHub knowledge repo via the GitHub MCP Worker. Use when the user says "save this", "push this to my notes", "capture this", "extract my learnings", "checkpoint", or runs /learned. Also triggers at the end of sessions with significant learning content, or when Claude explains a root cause, makes an architectural decision, or breaks a debugging spiral. Works in Claude.ai Chat, Cowork (via Desktop Commander bash), and Claude Code (via /learned slash command).
How this skill is triggered — by the user, by Claude, or both
Slash command
/claude-skills:knowledge-captureThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Capture learning moments from Claude sessions and persist them to the GitHub knowledge repo (`tieubao/til`).
Capture learning moments from Claude sessions and persist them to the GitHub knowledge repo (tieubao/til).
ASSETS_WORKER_URL (the base URL of your worker, e.g. https://your-worker.example.com) and ASSETS_WORKER_TOKEN (bearer token for authorization). These are referenced as ${ASSETS_WORKER_URL} and $ASSETS_WORKER_TOKEN in the curl commands below.cairosvg Python package (Claude.ai container) or rsvg-convert (macOS). Only needed if PNG is explicitly required..learned/ locally if GitHub MCP is unavailableWhen auto-detecting, say: "That explanation about [topic] seems worth capturing. Want me to push it to GitHub?"
| Signal | Type | Notes |
|---|---|---|
| User asked a question, Claude answered | Q&A | Most common pattern |
| "What is X" or concept lookup | Definition | Reference card for a term or concept |
| Short single concept, < 500 words, no question | TIL | Quick insight or fact |
| Multi-section, long explanation | Article | Longer reference |
| Comparison of tools, frameworks, approaches | Comparison | Table-driven, verdict-oriented |
| Design decisions with rationale and tradeoffs | Decision Record | ADR-style: context, decision, alternatives, consequences |
| Evaluation or scoring of a tool/approach | Evaluation | Rubric-scored, verdict at the end |
| Step-by-step workflow or process | Playbook | Sequential steps with decision points |
| Architecture, system design, or structural explanation | Architecture | Component descriptions, relationships, data flow |
Default to Q&A if the conversation had a question-answer flow. Use Definition when the answer is essentially "here's what this thing is" with no deeper investigation. Default to TIL if ambiguous and no clear question.
Match the note structure to the content, not the other way around. The 4 original types (Q&A, Definition, TIL, Article) are starting points. If the content is a comparison matrix, don't force it into Q&A format -- use a comparison structure with a table. If it's a decision with tradeoffs, use an ADR structure. The repo should feel like a library of diverse reference materials, not a monotonous stack of Q&A cards.
Strip ALL conversational artifacts:
The note should read as a standalone reference, not a chat transcript.
Q&A-specific cleaning: Rewrite the user's question to be clear and context-free. The original question might be sloppy, shorthand, or assume shared context ("why doesn't this work?" becomes "Why does Python's asyncio.run() raise RuntimeError inside Jupyter notebooks?"). The answer should be direct and self-contained, not a reply to someone.
For Q&A (most common):
## Question
[The question, rewritten to be clear, specific, and standalone]
## Answer
[The answer, clean and direct. Include code examples if the original had them.]
## Key Takeaway
[1-2 sentence summary of the core insight. Optional -- skip if the answer is already short enough.]
For Definition (reference card):
## Definition
[Concise, precise definition of the term or concept. 1-3 sentences.]
## Context
[When you'd encounter this, why it matters, how it relates to adjacent concepts. Keep brief.]
## Example
[A concrete example, code snippet, or analogy. Optional -- skip if the definition is self-explanatory.]
For TIL:
[Start directly with content. No heading needed, title is set separately.]
[Content should be standalone and self-contained.]
For Article:
## [Section heading]
[Content organized by logical sections]
For Comparison:
[Opening paragraph: what is being compared and why]
| Dimension | Option A | Option B | Option C |
|-----------|----------|----------|----------|
| [criterion 1] | [assessment] | [assessment] | [assessment] |
| [criterion 2] | [assessment] | [assessment] | [assessment] |
## Verdict
[Which option wins, for whom, under what conditions. Be opinionated.]
For Decision Record (ADR-style):
## Context
[What prompted this decision. The problem or tradeoff being resolved.]
## Decision
[What was decided. Be specific.]
## Alternatives considered
[What was rejected and why. Brief per alternative.]
## Consequences
[What this means going forward. Both positive and negative.]
For Evaluation:
[Brief description of what is being evaluated]
| Criterion | Score | Rationale |
|-----------|-------|-----------|
| [criterion] | [X/N] | [why this score] |
## Verdict: [ADOPT / BOOKMARK / SKIP]
[One paragraph summary with recommendation and conditions.]
For Playbook:
## When to use
[The situation or trigger that calls for this playbook]
## Steps
### 1. [Step name]
[What to do, what to check, what output to expect]
### 2. [Step name]
[Continue. Include decision points: "If X, do Y. If Z, do W."]
## Common pitfalls
[What goes wrong and how to avoid it]
For Architecture:
## Overview
[What this system/component does, one paragraph]
## Components
[Description of each component, its responsibility, and how it connects to others. Use a table or bullet list depending on count.]
## Data flow
[How data moves through the system. Include a diagram if available.]
## Key decisions
[Why it's built this way, not another way]
Choosing the right format: The templates above are reference patterns, not rigid molds. After detecting the content type, design the actual layout to fit the specific learning content and its context. Two notes of the same type can (and should) have different structures if the content calls for it.
The layout design process:
Examples of adaptive layout:
A "Comparison" note about 3 CLI tools might use the standard table format. But a "Comparison" note about architectural approaches might work better as side-by-side prose sections with a "when to use which" summary, because the nuances don't compress into table cells.
A "Q&A" about a simple API gotcha uses the standard Question/Answer/Takeaway format. But a "Q&A" about a complex debugging journey might use Question/Investigation/Root cause/Fix/Why this happens, because the debugging process IS the learning.
A "Decision Record" for a tech stack choice follows the ADR template closely. But a "Decision Record" about a workflow design might add a "How we'll know this was wrong" section because the decision is harder to reverse.
The bar: Would someone reading this note in 6 months find the structure helpful for quickly locating the information they need? If a section heading doesn't help them navigate, it shouldn't exist. If a piece of information is buried because the template didn't have a place for it, add a section.
The repo should feel like a curated library, not a database dump. Each note should feel like it was written for this specific topic, not stamped from a template factory.
python-asyncio-event-loop-internals.md)The topic param determines which folder the note lands in. The repo is organized as an Obsidian vault with topic-based folders.
Repo structure (as of 2026-04-21): All content notes live under notes/<domain>/. Framework files (CLAUDE.md, README.md, index.md, log.md, _inbox/, _templates/, _docs/) stay at repo root. Images go to notes/assets/<domain>/. Always pass topic as notes/<domain>, never as bare <domain>. The MCP worker treats topic as a literal path segment and does not auto-prepend notes/.
First, check existing topics by calling Github MCP Worker:list_notes to see the current folder structure. Prefer placing notes in existing folders when the content genuinely fits.
Topic selection rules:
notes/mcp/.notes/. Keep it short, lowercase, hyphenated. E.g. notes/nodejs, notes/finance, notes/devops.notes/youtube/ (the domain), not notes/nodejs/ (the technique). The reasoning: you'll search the repo by domain ("what do I know about YouTube?"), not by implementation detail ("what have I done with undici?").Topic options:
1. `notes/youtube` -- domain knowledge about YouTube transcript extraction (recommended)
2. `notes/nodejs` -- the core fix involves Node.js fetch/proxy behavior
3. `notes/devops` -- it's infrastructure/container knowledge
I'd go with `notes/youtube` since the knowledge is about YouTube's system. Your call.
Always preview the proposed topic, title, and tags before pushing. Never auto-push without user confirmation.
Avoid date-based paths. Always provide a topic. If you truly can't categorize something, use notes/misc/ rather than falling back to YYYY/MM/.
Tags serve as topic categorization. Pick 1-3 tags from the content domain.
Common tag patterns:
mcp, cloudflare, go, python, typescript, reactfinance, crypto, devops, security, aiarchitecture, debugging, config, workflowdwarves, opsTags are passed as an array to push_note and written into YAML frontmatter by the tool.
Default behavior: preserve visuals from the conversation. If the conversation produced diagrams, charts, SVGs, or visualizer output that are relevant to the note being captured, they should be included in the note. Don't drop visuals just because the capture pipeline is text-focused.
Three sources of visuals to check:
When to generate a new diagram vs reuse an existing one:
Detect visuals: Check if the conversation turns being captured contained:
Four pipelines depending on source format:
Default: Upload SVG directly. SVGs render natively on GitHub and in Obsidian. Skip PNG conversion unless there's a specific reason to rasterize.
# 1. Save SVG to temp file
cat > /tmp/capture-diagram.svg << 'SVGEOF'
[SVG CONTENT]
SVGEOF
# 2. Upload SVG directly to R2
RESPONSE=$(curl -s -X POST ${ASSETS_WORKER_URL}/upload \
-H "Authorization: Bearer $ASSETS_WORKER_TOKEN" \
-H "Content-Type: image/svg+xml" \
-H "X-Filename: [SLUG]" \
--data-binary @/tmp/capture-diagram.svg)
# 3. Extract URL
IMAGE_URL=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['url'])")
# 4. Clean up
rm -f /tmp/capture-diagram.svg
When to use PNG instead of SVG:
var(--color-text-primary)) that only resolve inside claude.ai. In this case, replace the CSS variables with hardcoded light-theme colors before uploading the SVG, OR convert to PNG.PNG fallback (only when needed):
cairosvg at default settings produces blurry text. The fix is higher DPI, not wider images. Use a smaller logical width with 2x pixel density to keep files small but text crisp.
Image size tiers (pick based on content complexity):
| Tier | Logical width | Actual pixels (2x DPI) | Best for | Approx compressed size |
|---|---|---|---|---|
| Small | 800px | 1600px | Simple flowcharts, 3-5 boxes | 15-40KB |
| Medium | 1000px | 2000px | Most diagrams, architecture overviews | 30-80KB |
| Large | 1200px | 2400px | Dense diagrams, wide comparison tables | 60-150KB |
Default to Medium (1000px). Only go Large if the diagram has dense text or many columns that would get cramped.
Option 1: Playwright (best quality, uses real browser rendering)
# Install playwright if not available
pip install playwright --break-system-packages
python3 -m playwright install chromium
# Convert SVG to high-DPI PNG via headless Chromium
# Adjust viewport width per size tier: 800 / 1000 / 1200
python3 << 'PYEOF'
import asyncio
from playwright.async_api import async_playwright
async def svg_to_png(svg_path, png_path, width=1000, scale=2):
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page(viewport={"width": width, "height": 800}, device_scale_factor=scale)
await page.goto(f"file://{svg_path}")
await page.wait_for_timeout(500)
svg_el = await page.query_selector("svg")
if svg_el:
await svg_el.screenshot(path=png_path)
else:
await page.screenshot(path=png_path, full_page=True)
await browser.close()
asyncio.run(svg_to_png("/tmp/capture-diagram.svg", "/tmp/capture-diagram.png"))
PYEOF
Option 2: cairosvg at 2x DPI (faster, decent quality)
pip install cairosvg --break-system-packages
# Medium tier: 1000px logical, 2000px actual at 192 DPI
python3 -c "from cairosvg import svg2png; svg2png(url='/tmp/capture-diagram.svg', write_to='/tmp/capture-diagram.png', output_width=2000, dpi=192)"
# For Small tier: output_width=1600
# For Large tier: output_width=2400
Then compress and upload:
# Compress PNG -- pngquant gives 60-80% size reduction with negligible quality loss
# Install if needed: apt-get install -y pngquant (Claude.ai container) or brew install pngquant (macOS)
pngquant --quality=65-90 --speed 1 --force --output /tmp/capture-diagram-compressed.png /tmp/capture-diagram.png
# Fall back to uncompressed if pngquant fails (e.g. already optimized or not installed)
if [ ! -f /tmp/capture-diagram-compressed.png ]; then
cp /tmp/capture-diagram.png /tmp/capture-diagram-compressed.png
fi
# Upload compressed PNG to R2
RESPONSE=$(curl -s -X POST ${ASSETS_WORKER_URL}/upload \
-H "Authorization: Bearer $ASSETS_WORKER_TOKEN" \
-H "Content-Type: image/png" \
-H "X-Filename: [SLUG]" \
--data-binary @/tmp/capture-diagram-compressed.png)
IMAGE_URL=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['url'])")
rm -f /tmp/capture-diagram.svg /tmp/capture-diagram.png /tmp/capture-diagram-compressed.png
Decision: Playwright vs cairosvg?
GitHub and Obsidian cannot render JSX. Instead of trying to screenshot JSX with Puppeteer, re-express the visual as SVG before capture.
Most JSX artifacts from Claude are charts, diagrams, tables, or simple layouts. These can be faithfully recreated as SVG, which renders natively on GitHub and in Obsidian.
Process:
When re-expressing JSX to SVG:
<rect>, <line>, <circle>, <path><rect> cells and <text> elements, or just use a markdown table insteadIf the JSX is too complex to re-express as SVG (heavy interactivity, animations, deeply nested state), fall back to:
The diagram shows [description of the visual content].
<details>
<summary>JSX source (render in browser to view)</summary>
\`\`\`jsx
[FULL JSX SOURCE]
\`\`\`
</details>
This covers the iOS case where there's no bash or browser available.
# 1. Save mermaid source
cat > /tmp/capture-diagram.mmd << 'MMDEOF'
[MERMAID SOURCE]
MMDEOF
# 2. Render with mermaid-cli -- prefer SVG output
npx -y @mermaid-js/mermaid-cli mmdc -i /tmp/capture-diagram.mmd -o /tmp/capture-diagram.svg -w 1200
# 3. Upload SVG to R2 (same as Pipeline A)
If SVG output is not usable, render to PNG with higher quality: mmdc -i /tmp/capture-diagram.mmd -o /tmp/capture-diagram.png -w 2400 -s 2
If mermaid-cli is not available, fall back to including the mermaid source in a code block (same approach as JSX fallback).
The Visualizer tool produces HTML widgets that render inline in claude.ai. These often contain the most valuable visual summaries (comparison matrices, phase maps, scoring tables, workflow diagrams). They need special handling because:
Capture strategy (prioritized):
Convert to markdown table (preferred for data-heavy widgets). If the HTML widget is essentially a styled table, comparison matrix, or scored list, convert it to a clean markdown table. This is the most portable format and renders everywhere (GitHub, Obsidian, any markdown viewer). Most Visualizer widgets from this conversation (phase mapping, hook scoring, repo evaluations) are best captured this way.
Convert to SVG (for diagrams and flowcharts). If the HTML widget contains an inline SVG diagram, extract the SVG and run through Pipeline A.
Preserve as HTML file (for complex interactive content). If the widget is genuinely interactive and the interactivity is the point (a calculator, a configurator, an interactive explainer), save the HTML source alongside the note:
push_note to a parallel path: {topic}/{slug}-widget.htmlSee [interactive version](./{slug}-widget.html)Screenshot fallback (if none of the above work). Take a screenshot of the widget rendering and upload to R2 as PNG. This is the last resort because screenshots are not searchable or editable.
Decision framework for HTML widgets:
Embed in markdown body using standard image syntax:

Since the GitHub repo is an Obsidian vault, standard markdown image links render correctly.
Use the Github MCP Worker:push_note tool:
Tool: Github MCP Worker:push_note
Parameters:
title: "Python asyncio event loop internals"
content: "[cleaned markdown body]"
tags: ["python", "async", "architecture"]
topic: "notes/python"
source: "Claude.ai chat"
The tool handles:
{topic}/{slug}.md. The topic value must start with notes/ for content notes (e.g. notes/python). Framework files like CLAUDE.md, README.md, index.md, log.md, _inbox/, _templates/, _docs/ stay at repo root and should not be pushed via this skill.tieubao/til repoAlways pass the topic param. See Step 4.5 for how to pick it.
Source field convention:
"Claude.ai chat""Claude iOS""Claude Code session""Cowork session""Claude Code - capacities-mcp"After pushing, confirm with the user:
Captured: "Python asyncio event loop internals"
Topic: notes/python/
Tags: python, async, architecture
Path: notes/python/python-asyncio-event-loop-internals.md
Link: https://github.com/tieubao/til/blob/master/notes/python/python-asyncio-event-loop-internals.md
Include the GitHub link if returned by the tool.
If the GitHub MCP tool is unavailable (connector not connected, worker down):
Save each capture as a separate file in the project-local .learned/ directory (or ~/.learned/ if no project context):
# Use project-local dir if in a git repo, otherwise home-level
if git rev-parse --show-toplevel &>/dev/null; then
DIR="$(git rev-parse --show-toplevel)/.learned"
else
DIR=~/.learned
fi
mkdir -p "$DIR"
# Generate slug from title
SLUG=$(echo "[TITLE]" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
DATE=$(date +%Y-%m-%d)
FILEPATH="${DIR}/${DATE}-${SLUG}.md"
cat > "$FILEPATH" << 'EOF'
---
title: [TITLE]
date: [ISO date]
tags: [list]
source: [session context]
pushed: false
---
[MARKDOWN BODY]
EOF
Tell the user: "GitHub MCP wasn't available. Saved to [filepath]. Push manually or retry later."
When the user says "extract my learnings", "checkpoint", or "capture knowledge":
Found [N] learning moments to capture:
1. "[title]" (type: [Comparison/Q&A/Decision Record/etc.])
Topic: [folder] | Tags: [tag1, tag2]
Layout:
## Overview -- what's being compared, 2 sentences
## Comparison table -- 4 criteria x 3 options
## Verdict -- opinionated recommendation
2. "[title]" (type: [type])
Topic: [folder] | Tags: [tag1, tag2]
Layout:
## Context -- the problem, 1 paragraph
## The insight -- core learning, with code example
## When this matters -- practical trigger
3. "[title]" (type: [type])
Topic: [folder] | Tags: [tag1, tag2]
Layout:
[TIL -- single block, ~200 words, no sections needed]
Push all / Pick specific numbers / Adjust layout / Skip?
The layout preview shows the section headings and a brief note of what each section will contain. This lets the user judge whether the structure fits the content BEFORE the note is written and pushed. The user can request layout changes ("make #2 a comparison instead of Q&A", "add a 'common mistakes' section to #1") before confirmation.
push_noteThis preview step is NOT optional, even for single notes. The user must see the content layout and confirm before any push_note call. The only exception: if the user says "push this exact thing" and points to a specific message, treat that as pre-confirmed for that single note.
Use this when 3+ related atomic notes have accumulated in ~/learned-today.md (or .learned/) that share a theme worth synthesizing into one cohesive long-form article (~700-1500 words) instead of staying as standalone short captures.
When to consolidate:
When NOT to consolidate:
Steps:
~/learned-today.md. Group by theme. Identify which 3+ should merge into one article and which should stay as standalone atoms.status: refined. Keep the source field referencing the originating session for traceability.## Related section linking to (a) standalone atomic notes from the same session that didn't get consolidated, (b) sibling cluster articles, (c) existing til posts where the topic genuinely overlaps.push_note as a single article to the chosen topic folder.mv ~/learned-today.md ~/learned-today.md.archive-YYYY-MM-DD so the buffer is empty for the next session.Concrete example (worked through in a 2026-05-04 dfoundation session):
8 atomic notes captured during a multi-hour brainstorm on agentkernel + Hermes multi-tenant on Mac mini → 5 long articles + 2 short briefs in tieubao/til/. Two atoms about agentkernel mechanics (broken-flag pen-test + wrapper-vs-framework debug rule) merged into one cluster article on the agentkernel-Apple-Containers integration story; one atom (Apple Containers Definition) became its own overview article in notes/macos/; one atom (Firecracker on macOS) stayed as a short standalone brief. Triage was the load-bearing step; drafting and pushing followed mechanically.
Future automation hook (not built yet): a consolidate_notes MCP tool could automate steps 2-3 (cluster theme + outline) by reading .learned/, computing topic affinity, and proposing clusters. Building it is deferred until the manual workflow proves painful at higher cadence. Track as TODO.
For Claude Code, this skill's logic is split into:
/learned command: cherry-pick the last response and push immediately via GitHub MCP/push-learned command: batch push all files from .learned/ directory.learned/ during coding (one file per capture, named YYYY-MM-DD-slug.md)These slash commands should be installed at ~/.claude/commands/learned.md and ~/.claude/commands/push-learned.md.
push_note.notes/youtube/ might also be tagged nodejs and proxy).npx claudepluginhub dwarvesf/claude-skillsCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.