Try an interactive version of this dialog: Sign up at solve.it.com, click Upload, and pass this URL.
Agent Skills
In this dialog we will implement the Agent Skills specification on top of claudette.
Let's begin by implementing skill discovery. Can you paste in a new Note cell the relevant quote from the spec about what skills look like and how they are discovered?
{
"id": "toolu_01QkBRcMCiUxWZ365QaXyEDP",
"call": {
"function": "read_url",
"arguments": {
"url": "https://agentskills.io/home"
}
},
"result": "'Skip to main content\\n\\n[Agent Skills home pageAgent Skills](/)\\n\\nSearch...\\n\\n\u2318KAsk AI\\n\\n * [agentskills/agentskills](https://github.com/agentskills/agentskills)\\n * [agentskills/agentskills](https://github.com/agentskills/agentskills)\\n\\n\\n\\nSearch...\\n\\nNavigation\\n\\nOverview\\n\\n * [Overview](/home)\\n\\n\\n * [What are skills?](/what-are-skills)\\n\\n\\n * [Specification](/specification)\\n\\n\\n * [Integrate skills](/integrate-skills)\\n\\n\\n\\nOn this page\\n\\n * Why Agent Skills?\\n * What can Agent Skills enable?\\n * Adoption\\n * Open development\\n * Get started\\n\\n\\n\\n# Overview\\n\\nCo<TRUNCATED>"
}
Let me also get the specification page which has the details on skill structure and discovery:
{
"id": "toolu_01EgU7W4FKLA8tsSbARTsHZD",
"call": {
"function": "read_url",
"arguments": {
"url": "https://agentskills.io/specification"
}
},
"result": "'Skip to main content\\n\\n[Agent Skills home pageAgent Skills](/)\\n\\nSearch...\\n\\n\u2318KAsk AI\\n\\n * [agentskills/agentskills](https://github.com/agentskills/agentskills)\\n * [agentskills/agentskills](https://github.com/agentskills/agentskills)\\n\\n\\n\\nSearch...\\n\\nNavigation\\n\\nSpecification\\n\\n * [Overview](/home)\\n\\n\\n * [What are skills?](/what-are-skills)\\n\\n\\n * [Specification](/specification)\\n\\n\\n * [Integrate skills](/integrate-skills)\\n\\n\\n\\nOn this page\\n\\n * Directory structure\\n * SKILL.md format\\n * Frontmatter (required)\\n * name field\\n * description field\\n * license fie<TRUNCATED>"
}
Now let me get the "Integrate skills" page which should have the discovery details:
{
"id": "toolu_01N8vNsmx9qh3CEW2U3W3SFJ",
"call": {
"function": "read_url",
"arguments": {
"url": "https://agentskills.io/integrate-skills"
}
},
"result": "'Skip to main content\\n\\n[Agent Skills home pageAgent Skills](/)\\n\\nSearch...\\n\\n\u2318KAsk AI\\n\\n * [agentskills/agentskills](https://github.com/agentskills/agentskills)\\n * [agentskills/agentskills](https://github.com/agentskills/agentskills)\\n\\n\\n\\nSearch...\\n\\nNavigation\\n\\nIntegrate skills into your agent\\n\\n * [Overview](/home)\\n\\n\\n * [What are skills?](/what-are-skills)\\n\\n\\n * [Specification](/specification)\\n\\n\\n * [Integrate skills](/integrate-skills)\\n\\n\\n\\nOn this page\\n\\n * Integration approaches\\n * Overview\\n * Skill discovery\\n * Loading metadata\\n * Parsing frontmatter\\<TRUNCATED>"
}
Now I have the relevant information. Let me create a note with the key quotes about skill structure and discovery:
{
"id": "toolu_015mWHuu73DknWmuGgjhbdDv",
"call": {
"function": "add_msg",
"arguments": {
"content": "## Agent Skills: Structure and Discovery\n\n### What is a Skill?\n\nA skill is a directory containing at minimum a `SKILL.md` file:\n\n```\nskill-name/\n\u2514\u2500\u2500 SKILL.md # Required\n```\n\nThe `SKILL.md` file must contain YAML frontmatter followed by Markdown content:\n\n```yaml\n---\nname: skill-name\ndescription: A description of what this skill does and when to use it.\n---\n```\n\n### Skill Discovery\n\nFrom the [Integrate Skills](https://agentskills.io/integrate-skills) page:\n\n> Skills are folders containing a `SKILL.md` file. Your agent should scan configured directories for valid skills.\n\n### Loading Me<TRUNCATED>"
}
},
"result": "'_9cfeea5f'"
}
Done! I've created a note summarising the key parts of the spec regarding:
- Skill structure - A folder with a
SKILL.mdfile containing YAML frontmatter (name + description) followed by markdown instructions - Discovery - Scan configured directories for folders containing
SKILL.mdfiles - Metadata loading - Parse only frontmatter at startup to keep context usage low
- Context injection - Use XML format with
<available_skills>containing skill name, description, and location
What would you like to tackle first for the implementation?
Agent Skills: Structure and Discovery
What is a Skill?
A skill is a directory containing at minimum a SKILL.md file:
skill-name/
└── SKILL.md # Required
The SKILL.md file must contain YAML frontmatter followed by Markdown content:
---
name: skill-name
description: A description of what this skill does and when to use it.
---
Skill Discovery
From the Integrate Skills page:
Skills are folders containing a
SKILL.mdfile. Your agent should scan configured directories for valid skills.
Loading Metadata
At startup, parse only the frontmatter of each
SKILL.mdfile. This keeps initial context usage low.
Injecting into Context
For Claude models, the recommended format uses XML:
<available_skills>
<skill>
<name>pdf-processing</name>
<description>Extracts text and tables from PDF files, fills forms, merges documents.</description>
<location>/path/to/skills/pdf-processing/SKILL.md</location>
</skill>
</available_skills>
For filesystem-based agents, include the
locationfield with the absolute path to the SKILL.md file. For tool-based agents, the location can be omitted. Keep metadata concise. Each skill should add roughly 50-100 tokens to the context.
with open('/app/data/skills/skills/pdf/SKILL.md', 'r') as f:
post = frontmatter.load(f)
display(post['name'])
display(post['description'])
# A colon-separated list of directories containing skills
os.environ['SKILLS_PATH'] = '/app/data/skills/skills'
@dataclass
class Skill:
name: str
description: str
license: str = None
@classmethod
def from_directory(cls, directory):
post = frontmatter.load(os.path.join(directory, 'SKILL.md'))
return cls(**post)
def load_skills() -> List[Skill]:
return [
Skill.from_directory(full_path)
for skills_root in os.environ.get('SKILLS_PATH', []).split(":")
for directory in os.listdir(skills_root)
if os.path.isdir(full_path := os.path.join(skills_root, directory))
]
load_skills()
[Skill(name='xlsx', description='Comprehensive spreadsheet creation, editing, and analysis with support for formulas, formatting, data analysis, and visualization. When Claude needs to work with spreadsheets (.xlsx, .xlsm, .csv, .tsv, etc) for: (1) Creating new spreadsheets with formulas and formatting, (2) Reading or analyzing data, (3) Modify existing spreadsheets while preserving formulas, (4) Data analysis and visualization in spreadsheets, or (5) Recalculating formulas', license='Proprietary. LICENSE.txt has complete terms'),
Skill(name='webapp-testing', description='Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.', license='Complete terms in LICENSE.txt'),
Skill(name='theme-factory', description='Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.', license='Complete terms in LICENSE.txt'),
Skill(name='docx', description='Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. When Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks', license='Proprietary. LICENSE.txt has complete terms'),
Skill(name='web-artifacts-builder', description='Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts.', license='Complete terms in LICENSE.txt'),
Skill(name='skill-creator', description="Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.", license='Complete terms in LICENSE.txt'),
Skill(name='frontend-design', description='Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.', license='Complete terms in LICENSE.txt'),
Skill(name='pptx', description='Presentation creation, editing, and analysis. When Claude needs to work with presentations (.pptx files) for: (1) Creating new presentations, (2) Modifying or editing content, (3) Working with layouts, (4) Adding comments or speaker notes, or any other presentation tasks', license='Proprietary. LICENSE.txt has complete terms'),
Skill(name='algorithmic-art', description="Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.", license='Complete terms in LICENSE.txt'),
Skill(name='internal-comms', description='A set of resources to help me write all kinds of internal communications, using the formats that my company likes to use. Claude should use this skill whenever asked to write some sort of internal communications (status reports, leadership updates, 3P updates, company newsletters, FAQs, incident reports, project updates, etc.).', license='Complete terms in LICENSE.txt'),
Skill(name='doc-coauthoring', description='Guide users through a structured workflow for co-authoring documentation. Use when user wants to write documentation, proposals, technical specs, decision docs, or similar structured content. This workflow helps users efficiently transfer context, refine content through iteration, and verify the doc works for readers. Trigger when user mentions writing docs, creating proposals, drafting specs, or similar documentation tasks.', license=None),
Skill(name='pdf', description='Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale.', license='Proprietary. LICENSE.txt has complete terms'),
Skill(name='brand-guidelines', description="Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.", license='Complete terms in LICENSE.txt'),
Skill(name='slack-gif-creator', description='Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like "make me a GIF of X doing Y for Slack."', license='Complete terms in LICENSE.txt'),
Skill(name='canvas-design', description="Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.", license='Complete terms in LICENSE.txt'),
Skill(name='mcp-builder', description='Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).', license='Complete terms in LICENSE.txt')]We are still missing some other possible keys, but let's move on for now. Now please add a cell with the relevant quote of the spec that describes how to insert the skill list into the system prompt.
{
"id": "toolu_01JKLL6qEHQ7gEZd8i9kVKAd",
"call": {
"function": "read_url",
"arguments": {
"url": "https://agentskills.io/integrate-skills"
}
},
"result": "'Skip to main content\\n\\n[Agent Skills home pageAgent Skills](/)\\n\\nSearch...\\n\\n\u2318KAsk AI\\n\\n * [agentskills/agentskills](https://github.com/agentskills/agentskills)\\n * [agentskills/agentskills](https://github.com/agentskills/agentskills)\\n\\n\\n\\nSearch...\\n\\nNavigation\\n\\nIntegrate skills into your agent\\n\\n * [Overview](/home)\\n\\n\\n * [What are skills?](/what-are-skills)\\n\\n\\n * [Specification](/specification)\\n\\n\\n * [Integrate skills](/integrate-skills)\\n\\n\\n\\nOn this page\\n\\n * Integration approaches\\n * Overview\\n * Skill discovery\\n * Loading metadata\\n * Parsing frontmatter\\<TRUNCATED>"
}
{
"id": "toolu_0157v464CZhBw4dqhBjDMfZq",
"call": {
"function": "add_msg",
"arguments": {
"content": "## Injecting Skills into System Prompt\n\nFrom the [Integrate Skills](https://agentskills.io/integrate-skills) spec:\n\n> Include skill metadata in the system prompt so the model knows what skills are available. Follow your platform's guidance for system prompt updates. For example, for Claude models, the recommended format uses XML:\n\n```xml\n<available_skills>\n <skill>\n <name>pdf-processing</name>\n <description>Extracts text and tables from PDF files, fills forms, merges documents.</description>\n <location>/path/to/skills/pdf-processing/SKILL.md</location>\n </skill>\n <skill>\n <name><TRUNCATED>"
}
},
"result": "'_b4d031bd'"
}
Done! I've added a note with the spec's guidance on injecting skills into the system prompt. The key points are:
- Use XML format with
<available_skills>wrapper - Each skill gets
<name>,<description>, and optionally<location> - Include
locationfor filesystem-based agents (like ours) - Keep it concise: ~50-100 tokens per skill
Ready to implement the to_prompt() method on your Skill class?
Injecting Skills into System Prompt
From the Integrate Skills spec:
Include skill metadata in the system prompt so the model knows what skills are available. Follow your platform's guidance for system prompt updates. For example, for Claude models, the recommended format uses XML:
<available_skills>
<skill>
<name>pdf-processing</name>
<description>Extracts text and tables from PDF files, fills forms, merges documents.</description>
<location>/path/to/skills/pdf-processing/SKILL.md</location>
</skill>
<skill>
<name>data-analysis</name>
<description>Analyzes datasets, generates charts, and creates summary reports.</description>
<location>/path/to/skills/data-analysis/SKILL.md</location>
</skill>
</available_skills>
For filesystem-based agents, include the
locationfield with the absolute path to the SKILL.md file. For tool-based agents, the location can be omitted. Keep metadata concise. Each skill should add roughly 50-100 tokens to the context.
Yes! Since Python 3.9, ElementTree.tostring() and ElementTree.indent() support pretty-printing:
from xml.etree import ElementTree as ET
# Create your element tree, then indent it before converting to string:
ET.indent(root) # modifies tree in-place
xml_str = ET.tostring(root, encoding='unicode')
You can also specify the indentation level with ET.indent(root, space=" ") for 2 spaces, or space=" " for 4.
Great! I think that does it for discovery. Next I want to implement some tools so claudette can actually use the skills:
skill(name)will "activate" a skill (return the contents of the SKILL.md)read(path)will return the contents of a file on disk.script(path)will run the specified skill-bundled python script and return the output.
Here is a reference for how Claude Code consumes skills: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview
Yes, that's a solid starting set! Those three tools cover the core workflow:
skill(name)— Load the full instructions when a skill is relevantread(path)— Access supporting files (examples, templates, configs) referenced in the skillscript(path)— Execute bundled utilities (e.g. PDF processing, spreadsheet manipulation)
One thing you might want to add later is a write(path, content) tool — many skills (like docx, pdf, xlsx) will need to create output files. But you can add that when you hit that need.
Want to start implementing skill(name) first?
Please add a message with the relevant snippet from https://mikhail.io/2025/10/claude-code-skills/ that explains what Anthropic's skill tool description looks like.
{
"id": "toolu_01VLuRRRr4hToDJkG5Fei8L1",
"call": {
"function": "read_url",
"arguments": {
"url": "https://mikhail.io/2025/10/claude-code-skills/"
}
},
"result": "'[  Mikhail Shilkov ](/)\\n\\n * [TOPICS](/tags/)\\n * [ARCHIVES](/archives/)\\n * [TALKS](/talks/)\\n * [ABOUT](/about/)\\n * [__](https://mikhail.io/feed/)\\n * [__](https://x.com/MikhailShilkov)\\n * [__](https://dev.to/mikhailshilkov)\\n * [__](https://medium.com/@MikhailShilkov)\\n * [__](https://github.com/MikhailShilkov)\\n * [__](https://www.linkedin.com/in/MikhailShilkov)\\n\\n\\n\\n * __\\n\\n * [ __](h<TRUNCATED>"
}
{
"id": "toolu_011UUKrhdRX4Cu8qoZQQWK3i",
"call": {
"function": "add_msg",
"arguments": {
"content": "### Claude Code's Skill Tool Definition\n\nFrom [Mikhail Shilkov's reverse-engineering of Claude Code](https://mikhail.io/2025/10/claude-code-skills/):\n\n```\n## Skill\n\n**Input Schema:**\n\nobject:\n - command (required):\n string\n # The skill name (no arguments). E.g., \"pdf\" or \"xlsx\"\n\n**Description:**\n\nExecute a skill within the main conversation\n\n<skills_instructions>\nWhen users ask you to perform tasks, check if any of the available skills\nbelow can help complete the task more effectively. Skills provide specialized\ncapabilities and domain knowledge.\n\nHow to use skills:\n- Invoke skills us<TRUNCATED>"
}
},
"result": "'_f52feb63'"
}
Done! I've added a note with the key snippet showing Claude Code's Skill tool definition.
Interesting design choice: they embed the <available_skills> XML inside the tool's description field rather than in the system prompt. This keeps the system prompt lean and bundles everything skill-related in one place.
Claude Code's Skill Tool Definition
From Mikhail Shilkov's reverse-engineering of Claude Code:
## Skill
**Input Schema:**
object:
- command (required):
string
# The skill name (no arguments). E.g., "pdf" or "xlsx"
**Description:**
Execute a skill within the main conversation
<skills_instructions>
When users ask you to perform tasks, check if any of the available skills
below can help complete the task more effectively. Skills provide specialized
capabilities and domain knowledge.
How to use skills:
- Invoke skills using this tool with the skill name only (no arguments)
- When you invoke a skill, you will see <command-message>The "{name}" skill is loading</command-message>
- The skill's prompt will expand and provide detailed instructions on how to complete the task
- Examples:
- `command: "pdf"` - invoke the pdf skill
- `command: "xlsx"` - invoke the xlsx skill
- `command: "ms-office-suite:pdf"` - invoke using fully qualified name
Important:
- Only use skills listed in <available_skills> below
- Do not invoke a skill that is already running
- Do not use this tool for built-in CLI commands (like /help, /clear, etc.)
</skills_instructions>
<available_skills>
[Skills are listed here - see next section]
</available_skills>
Note: Claude Code embeds the <available_skills> XML directly in the tool description, rather than in the system prompt.
from IPython.display import Markdown
@dataclass
class Skill:
name: str
description: str
location: str = None
@classmethod
def from_directory(cls, directory):
location = os.path.join(directory, 'SKILL.md')
post = frontmatter.load(location)
return cls(name=post['name'], description=post['description'], location=location)
def instructions(self):
with open(self.location) as f:
return f.read()
def skill(
name: str, # The name of a skill to activate
) -> str: # The contents of the skill's SKILL.MD
for skills_root in os.environ.get('SKILLS_PATH', '').split(":"):
skill_path = os.path.join(skills_root, name)
if os.path.exists(skill_path):
return Skill.from_directory(skill_path).instructions()
skill.__doc__ = f"""
Execute a skill within the main conversation
When users ask you to perform tasks, check if any of the available skills below
can help complete the task more effectively. Skills provide specialized
capabilities and domain knowledge.
How to use skills:
- Invoke skill using this tool with the skill name only (no arguments).
- When you invoke a skill, the skill's prompt will expand and provide
detailed instructions on how to complete the task.
Important
- Only use skills listed in <available_skills> below.
- Do not invoke a skill that is already running.
{available_skills()}
"""
Markdown(skill('pdf'))
name: pdf description: Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale. license: Proprietary. LICENSE.txt has complete terms
PDF Processing Guide
Overview
This guide covers essential PDF processing operations using Python libraries and command-line tools. For advanced features, JavaScript libraries, and detailed examples, see reference.md. If you need to fill out a PDF form, read forms.md and follow its instructions.
Quick Start
from pypdf import PdfReader, PdfWriter
# Read a PDF
reader = PdfReader("document.pdf")
print(f"Pages: {len(reader.pages)}")
# Extract text
text = ""
for page in reader.pages:
text += page.extract_text()
Python Libraries
pypdf - Basic Operations
Merge PDFs
from pypdf import PdfWriter, PdfReader
writer = PdfWriter()
for pdf_file in ["doc1.pdf", "doc2.pdf", "doc3.pdf"]:
reader = PdfReader(pdf_file)
for page in reader.pages:
writer.add_page(page)
with open("merged.pdf", "wb") as output:
writer.write(output)
Split PDF
reader = PdfReader("input.pdf")
for i, page in enumerate(reader.pages):
writer = PdfWriter()
writer.add_page(page)
with open(f"page_{i+1}.pdf", "wb") as output:
writer.write(output)
Extract Metadata
reader = PdfReader("document.pdf")
meta = reader.metadata
print(f"Title: {meta.title}")
print(f"Author: {meta.author}")
print(f"Subject: {meta.subject}")
print(f"Creator: {meta.creator}")
Rotate Pages
reader = PdfReader("input.pdf")
writer = PdfWriter()
page = reader.pages[0]
page.rotate(90) # Rotate 90 degrees clockwise
writer.add_page(page)
with open("rotated.pdf", "wb") as output:
writer.write(output)
pdfplumber - Text and Table Extraction
Extract Text with Layout
import pdfplumber
with pdfplumber.open("document.pdf") as pdf:
for page in pdf.pages:
text = page.extract_text()
print(text)
Extract Tables
with pdfplumber.open("document.pdf") as pdf:
for i, page in enumerate(pdf.pages):
tables = page.extract_tables()
for j, table in enumerate(tables):
print(f"Table {j+1} on page {i+1}:")
for row in table:
print(row)
Advanced Table Extraction
import pandas as pd
with pdfplumber.open("document.pdf") as pdf:
all_tables = []
for page in pdf.pages:
tables = page.extract_tables()
for table in tables:
if table: # Check if table is not empty
df = pd.DataFrame(table[1:], columns=table[0])
all_tables.append(df)
# Combine all tables
if all_tables:
combined_df = pd.concat(all_tables, ignore_index=True)
combined_df.to_excel("extracted_tables.xlsx", index=False)
reportlab - Create PDFs
Basic PDF Creation
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
c = canvas.Canvas("hello.pdf", pagesize=letter)
width, height = letter
# Add text
c.drawString(100, height - 100, "Hello World!")
c.drawString(100, height - 120, "This is a PDF created with reportlab")
# Add a line
c.line(100, height - 140, 400, height - 140)
# Save
c.save()
Create PDF with Multiple Pages
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
from reportlab.lib.styles import getSampleStyleSheet
doc = SimpleDocTemplate("report.pdf", pagesize=letter)
styles = getSampleStyleSheet()
story = []
# Add content
title = Paragraph("Report Title", styles['Title'])
story.append(title)
story.append(Spacer(1, 12))
body = Paragraph("This is the body of the report. " * 20, styles['Normal'])
story.append(body)
story.append(PageBreak())
# Page 2
story.append(Paragraph("Page 2", styles['Heading1']))
story.append(Paragraph("Content for page 2", styles['Normal']))
# Build PDF
doc.build(story)
Command-Line Tools
pdftotext (poppler-utils)
# Extract text
pdftotext input.pdf output.txt
# Extract text preserving layout
pdftotext -layout input.pdf output.txt
# Extract specific pages
pdftotext -f 1 -l 5 input.pdf output.txt # Pages 1-5
qpdf
# Merge PDFs
qpdf --empty --pages file1.pdf file2.pdf -- merged.pdf
# Split pages
qpdf input.pdf --pages . 1-5 -- pages1-5.pdf
qpdf input.pdf --pages . 6-10 -- pages6-10.pdf
# Rotate pages
qpdf input.pdf output.pdf --rotate=+90:1 # Rotate page 1 by 90 degrees
# Remove password
qpdf --password=mypassword --decrypt encrypted.pdf decrypted.pdf
pdftk (if available)
# Merge
pdftk file1.pdf file2.pdf cat output merged.pdf
# Split
pdftk input.pdf burst
# Rotate
pdftk input.pdf rotate 1east output rotated.pdf
Common Tasks
Extract Text from Scanned PDFs
# Requires: pip install pytesseract pdf2image
import pytesseract
from pdf2image import convert_from_path
# Convert PDF to images
images = convert_from_path('scanned.pdf')
# OCR each page
text = ""
for i, image in enumerate(images):
text += f"Page {i+1}:\n"
text += pytesseract.image_to_string(image)
text += "\n\n"
print(text)
Add Watermark
from pypdf import PdfReader, PdfWriter
# Create watermark (or load existing)
watermark = PdfReader("watermark.pdf").pages[0]
# Apply to all pages
reader = PdfReader("document.pdf")
writer = PdfWriter()
for page in reader.pages:
page.merge_page(watermark)
writer.add_page(page)
with open("watermarked.pdf", "wb") as output:
writer.write(output)
Extract Images
# Using pdfimages (poppler-utils)
pdfimages -j input.pdf output_prefix
# This extracts all images as output_prefix-000.jpg, output_prefix-001.jpg, etc.
Password Protection
from pypdf import PdfReader, PdfWriter
reader = PdfReader("input.pdf")
writer = PdfWriter()
for page in reader.pages:
writer.add_page(page)
# Add password
writer.encrypt("userpassword", "ownerpassword")
with open("encrypted.pdf", "wb") as output:
writer.write(output)
Quick Reference
| Task | Best Tool | Command/Code |
|---|---|---|
| Merge PDFs | pypdf | writer.add_page(page) |
| Split PDFs | pypdf | One page per file |
| Extract text | pdfplumber | page.extract_text() |
| Extract tables | pdfplumber | page.extract_tables() |
| Create PDFs | reportlab | Canvas or Platypus |
| Command line merge | qpdf | qpdf --empty --pages ... |
| OCR scanned PDFs | pytesseract | Convert to image first |
| Fill PDF forms | pdf-lib or pypdf (see forms.md) | See forms.md |
Next Steps
- For advanced pypdfium2 usage, see reference.md
- For JavaScript libraries (pdf-lib), see reference.md
- If you need to fill out a PDF form, follow the instructions in forms.md
- For troubleshooting guides, see reference.md
from pathlib import Path
def is_valid_absolute_path(
path: str
) -> bool:
path = Path(path)
if not path.is_absolute():
return False
valid_roots = [Path(root) for root in os.environ.get('SKILLS_PATH', '').split(":")]
if not any(path.is_relative_to(root) for root in valid_roots):
return False
return True
p = '/app/data/skills/skills/pdf/forms.md'
display(p, is_valid_absolute_path(p))
p = '~/skills/skills/pdf/forms.md'
display(p, is_valid_absolute_path(p))
p = '/app/data/skills/README.md'
display(p, is_valid_absolute_path(p))
def read(
absolute_path: str # Absolute path of the file to read
) -> str: # Text contents of the read file
"""
Returns the text contents of the file at the given absolute path
"""
if not is_valid_absolute_path(absolute_path):
raise ValueError("The path is not absolute, or the agent is not authorized to read it")
with open(absolute_path, 'r') as f:
return f.read()
read('/app/data/skills/skills/pdf/forms.md')
'**CRITICAL: You MUST complete these steps in order. Do not skip ahead to writing code.**\n\nIf you need to fill out a PDF form, first check to see if the PDF has fillable form fields. Run this script from this file\'s directory:\n `python scripts/check_fillable_fields <file.pdf>`, and depending on the result go to either the "Fillable fields" or "Non-fillable fields" and follow those instructions.\n\n# Fillable fields\nIf the PDF has fillable form fields:\n- Run this script from this file\'s directory: `python scripts/extract_form_field_info.py <input.pdf> <field_info.json>`. It will create a JSON file with a list of fields in this format:\n```\n[\n {\n "field_id": (unique ID for the field),\n "page": (page number, 1-based),\n "rect": ([left, bottom, right, top] bounding box in PDF coordinates, y=0 is the bottom of the page),\n "type": ("text", "checkbox", "radio_group", or "choice"),\n },\n // Checkboxes have "checked_value" and "unchecked_value" properties:\n {\n "field_id": (unique ID for the field),\n "page": (page number, 1-based),\n "type": "checkbox",\n "checked_value": (Set the field to this value to check the checkbox),\n "unchecked_value": (Set the field to this value to uncheck the checkbox),\n },\n // Radio groups have a "radio_options" list with the possible choices.\n {\n "field_id": (unique ID for the field),\n "page": (page number, 1-based),\n "type": "radio_group",\n "radio_options": [\n {\n "value": (set the field to this value to select this radio option),\n "rect": (bounding box for the radio button for this option)\n },\n // Other radio options\n ]\n },\n // Multiple choice fields have a "choice_options" list with the possible choices:\n {\n "field_id": (unique ID for the field),\n "page": (page number, 1-based),\n "type": "choice",\n "choice_options": [\n {\n "value": (set the field to this value to select this option),\n "text": (display text of the option)\n },\n // Other choice options\n ],\n }\n]\n```\n- Convert the PDF to PNGs (one image for each page) with this script (run from this file\'s directory):\n`python scripts/convert_pdf_to_images.py <file.pdf> <output_directory>`\nThen analyze the images to determine the purpose of each form field (make sure to convert the bounding box PDF coordinates to image coordinates).\n- Create a `field_values.json` file in this format with the values to be entered for each field:\n```\n[\n {\n "field_id": "last_name", // Must match the field_id from `extract_form_field_info.py`\n "description": "The user\'s last name",\n "page": 1, // Must match the "page" value in field_info.json\n "value": "Simpson"\n },\n {\n "field_id": "Checkbox12",\n "description": "Checkbox to be checked if the user is 18 or over",\n "page": 1,\n "value": "/On" // If this is a checkbox, use its "checked_value" value to check it. If it\'s a radio button group, use one of the "value" values in "radio_options".\n },\n // more fields\n]\n```\n- Run the `fill_fillable_fields.py` script from this file\'s directory to create a filled-in PDF:\n`python scripts/fill_fillable_fields.py <input pdf> <field_values.json> <output pdf>`\nThis script will verify that the field IDs and values you provide are valid; if it prints error messages, correct the appropriate fields and try again.\n\n# Non-fillable fields\nIf the PDF doesn\'t have fillable form fields, you\'ll need to visually determine where the data should be added and create text annotations. Follow the below steps *exactly*. You MUST perform all of these steps to ensure that the the form is accurately completed. Details for each step are below.\n- Convert the PDF to PNG images and determine field bounding boxes.\n- Create a JSON file with field information and validation images showing the bounding boxes.\n- Validate the the bounding boxes.\n- Use the bounding boxes to fill in the form.\n\n## Step 1: Visual Analysis (REQUIRED)\n- Convert the PDF to PNG images. Run this script from this file\'s directory:\n`python scripts/convert_pdf_to_images.py <file.pdf> <output_directory>`\nThe script will create a PNG image for each page in the PDF.\n- Carefully examine each PNG image and identify all form fields and areas where the user should enter data. For each form field where the user should enter text, determine bounding boxes for both the form field label, and the area where the user should enter text. The label and entry bounding boxes MUST NOT INTERSECT; the text entry box should only include the area where data should be entered. Usually this area will be immediately to the side, above, or below its label. Entry bounding boxes must be tall and wide enough to contain their text.\n\nThese are some examples of form structures that you might see:\n\n*Label inside box*\n```\n┌────────────────────────┐\n│ Name: │\n└────────────────────────┘\n```\nThe input area should be to the right of the "Name" label and extend to the edge of the box.\n\n*Label before line*\n```\nEmail: _______________________\n```\nThe input area should be above the line and include its entire width.\n\n*Label under line*\n```\n_________________________\nName\n```\nThe input area should be above the line and include the entire width of the line. This is common for signature and date fields.\n\n*Label above line*\n```\nPlease enter any special requests:\n________________________________________________\n```\nThe input area should extend from the bottom of the label to the line, and should include the entire width of the line.\n\n*Checkboxes*\n```\nAre you a US citizen? Yes □ No □\n```\nFor checkboxes:\n- Look for small square boxes (□) - these are the actual checkboxes to target. They may be to the left or right of their labels.\n- Distinguish between label text ("Yes", "No") and the clickable checkbox squares.\n- The entry bounding box should cover ONLY the small square, not the text label.\n\n### Step 2: Create fields.json and validation images (REQUIRED)\n- Create a file named `fields.json` with information for the form fields and bounding boxes in this format:\n```\n{\n "pages": [\n {\n "page_number": 1,\n "image_width": (first page image width in pixels),\n "image_height": (first page image height in pixels),\n },\n {\n "page_number": 2,\n "image_width": (second page image width in pixels),\n "image_height": (second page image height in pixels),\n }\n // additional pages\n ],\n "form_fields": [\n // Example for a text field.\n {\n "page_number": 1,\n "description": "The user\'s last name should be entered here",\n // Bounding boxes are [left, top, right, bottom]. The bounding boxes for the label and text entry should not overlap.\n "field_label": "Last name",\n "label_bounding_box": [30, 125, 95, 142],\n "entry_bounding_box": [100, 125, 280, 142],\n "entry_text": {\n "text": "Johnson", // This text will be added as an annotation at the entry_bounding_box location\n "font_size": 14, // optional, defaults to 14\n "font_color": "000000", // optional, RRGGBB format, defaults to 000000 (black)\n }\n },\n // Example for a checkbox. TARGET THE SQUARE for the entry bounding box, NOT THE TEXT\n {\n "page_number": 2,\n "description": "Checkbox that should be checked if the user is over 18",\n "entry_bounding_box": [140, 525, 155, 540], // Small box over checkbox square\n "field_label": "Yes",\n "label_bounding_box": [100, 525, 132, 540], // Box containing "Yes" text\n // Use "X" to check a checkbox.\n "entry_text": {\n "text": "X",\n }\n }\n // additional form field entries\n ]\n}\n```\n\nCreate validation images by running this script from this file\'s directory for each page:\n`python scripts/create_validation_image.py <page_number> <path_to_fields.json> <input_image_path> <output_image_path>\n\nThe validation images will have red rectangles where text should be entered, and blue rectangles covering label text.\n\n### Step 3: Validate Bounding Boxes (REQUIRED)\n#### Automated intersection check\n- Verify that none of bounding boxes intersect and that the entry bounding boxes are tall enough by checking the fields.json file with the `check_bounding_boxes.py` script (run from this file\'s directory):\n`python scripts/check_bounding_boxes.py <JSON file>`\n\nIf there are errors, reanalyze the relevant fields, adjust the bounding boxes, and iterate until there are no remaining errors. Remember: label (blue) bounding boxes should contain text labels, entry (red) boxes should not.\n\n#### Manual image inspection\n**CRITICAL: Do not proceed without visually inspecting validation images**\n- Red rectangles must ONLY cover input areas\n- Red rectangles MUST NOT contain any text\n- Blue rectangles should contain label text\n- For checkboxes:\n - Red rectangle MUST be centered on the checkbox square\n - Blue rectangle should cover the text label for the checkbox\n\n- If any rectangles look wrong, fix fields.json, regenerate the validation images, and verify again. Repeat this process until the bounding boxes are fully accurate.\n\n\n### Step 4: Add annotations to the PDF\nRun this script from this file\'s directory to create a filled-out PDF using the information in fields.json:\n`python scripts/fill_pdf_form_with_annotations.py <input_pdf_path> <path_to_fields.json> <output_pdf_path>\n'How can I run a Python script given a path? What are my options, and what is most idiomatic?
The main options:
-
subprocess.run()— The modern, idiomatic choice (Python 3.5+). Captures output, handles errors, supports timeouts and working directory. -
os.system()— Simple but limited. Only returns exit code, no output capture. -
runpy.run_path()— Runs script in current interpreter and returns its globals. Useful for dynamic module loading, but less isolation.
For your script() tool, subprocess.run() is the way to go since you want to:
- Capture stdout/stderr to return to the agent
- Run scripts in isolation
- Potentially set the working directory to the skill's folder
Basic pattern:
import subprocess
result = subprocess.run(['python', script_path], capture_output=True, text=True)
# result.stdout, result.stderr, result.returncode
Want to implement script() yourself, or would you like me to sketch it out?
By default, subprocess.run() returns stdout and stderr as bytes. Setting text=True decodes them to strings (using the system's default encoding, usually UTF-8).
# text=False (default): result.stdout is b'Hello\n'
# text=True: result.stdout is 'Hello\n'
It's equivalent to the older universal_newlines=True parameter.
What does the output of claude code's Read and Bash tools look like? Is there a specific format? Search the web to make sure.
I'm particularly curious about how the Bash tool handles stdout vs stderr, and the return code.
Found it. The code execution tool (the API version) has a very clear format:
* The bash code execution tool result includes separate stdout, stderr, and return_code fields:
{
"type": "bash_code_execution_tool_result",
"tool_use_id": "srvtoolu_01B3C4D5E6F7G8H9I0J1K2L3",
"content": {
"type": "bash_code_execution_result",
"stdout": "total 24\ndrwxr-xr-x 2 user user 4096 Jan 1 12:00 .\n...",
"stderr": "",
"return_code": 0
}
}
Where return_code is 0 for success, non-zero for failure.
For Claude Code's Bash tool (the CLI tool), it's a bit different. * The implementation concatenates stdout and stderr together: return result.stdout + result.stderr. * * There's actually a known issue that "Claude Code's bash tool does not preserve interleaving of stdout and stderr" — they get separated with stdout first, then stderr.
So the API code execution tool keeps them cleanly separated with explicit fields, while Claude Code's Bash tool merges them into a single string.
import subprocess
import sys
@dataclass
class RunResult:
stdout: str
stderr: str
return_code: int
def run(
absolute_path: str, # The path to a python script
arguments: List[str] = [], # A list of arguments for the script
) -> RunResult:
"Runs the python script at the given absolute path"
if not is_valid_absolute_path(absolute_path):
raise ValueError("The path is not absolute, or the agent is not authorized to read it")
path = Path(absolute_path)
if path.is_dir():
raise ValueError("The path does not point to a script")
# I'm comfortable with using this same python, but in production we should probably
# use some sort of isolation. A virtualenv at the very least.
result = subprocess.run(
[sys.executable, str(path), *arguments],
capture_output=True,
text=True,
env={**os.environ, 'PYTHONPATH': str(path.parent)}
)
return RunResult(stdout=result.stdout, stderr=result.stderr, return_code=result.returncode)
run("/app/data/skills/skills/pdf/scripts/check_bounding_boxes_test.py")
from claudette import *
model = models[2]
display(model)
chat = Chat(model, tools=[skill, read, run])
prompt = "How should I approach announcing in the company newsletter that we are launching a product?"
for o in chat.toolloop(prompt):
if o.role == 'user':
pass
elif hasattr(o, 'stop_reason') and o.stop_reason == 'tool_use':
for b in o.content:
if b.type == 'tool_use':
display(f"Used {b.name} with args {b.input}")
else:
display(o)
'claude-haiku-4-5'
"Used skill with args {'name': 'internal-comms'}"
"Used read with args {'absolute_path': '/app/data/skills/skills/internal-comms/examples/company-newsletter.md'}"
Perfect! Here's how you should approach announcing a product launch in your company newsletter:
Key Approach
1. Placement & Prominence
- Feature it in the :megaphone: Company Announcements section at the top
- This signals it's a major company-wide initiative
- Keep the announcement concise (1-2 sentences) but impactful
2. What to Include
- The announcement itself: What the product is, what problem it solves, and why it matters
- Links to key resources:
- Product documentation or launch page
- Slack announcement channel where people can ask questions
- Any leadership messages about the launch
- Customer-facing materials or press releases
- Call to action: What you want employees to do (learn more, share with customers, provide feedback, etc.)
3. Tone & Style
- Use "we" language ("We're excited to announce..." or "We've launched...")
- Keep it short and scannable (remember people read newsletters quickly)
- Focus on company-wide impact, not technical details
- Highlight why this matters to the whole company, not just one team
4. Supporting Context
- If relevant, you might add a second bullet in the announcements section with context like:
- Timeline/availability information
- Which teams led the effort
- Customer impact or market significance
- Next steps for rollout
5. Additional Sections
- Consider adding a :dart: Progress on Priorities section if the product launch was a major strategic goal
- Include relevant team achievements that contributed to the launch
Example Structure
:megaphone: Company Announcements
- We're thrilled to announce the launch of [Product Name]!
[1-2 sentence description of what it does and why it matters].
Check out the [launch page] and join [#product-launch-channel] for questions.
- [Product Name] is now available to [customers/beta users/etc.]
with [key features]. See the [documentation] for implementation details.
Would you like help drafting the actual announcement text, or do you have specific details about your product launch you'd like to work with?
- id:
msg_01YVnt7HMg8VYkkSMNNz4zFT - content:
[{'citations': None, 'text': 'Perfect! Here\'s how you should approach announcing a product launch in your company newsletter:\n\n## Key Approach\n\n**1. Placement & Prominence**\n- Feature it in the **:megaphone: Company Announcements** section at the top\n- This signals it\'s a major company-wide initiative\n- Keep the announcement concise (1-2 sentences) but impactful\n\n**2. What to Include**\n- **The announcement itself**: What the product is, what problem it solves, and why it matters\n- **Links to key resources**: \n - Product documentation or launch page\n - Slack announcement channel where people can ask questions\n - Any leadership messages about the launch\n - Customer-facing materials or press releases\n- **Call to action**: What you want employees to do (learn more, share with customers, provide feedback, etc.)\n\n**3. Tone & Style**\n- Use "we" language ("We\'re excited to announce..." or "We\'ve launched...")\n- Keep it short and scannable (remember people read newsletters quickly)\n- Focus on company-wide impact, not technical details\n- Highlight why this matters to the whole company, not just one team\n\n**4. Supporting Context**\n- If relevant, you might add a second bullet in the announcements section with context like:\n - Timeline/availability information\n - Which teams led the effort\n - Customer impact or market significance\n - Next steps for rollout\n\n**5. Additional Sections**\n- Consider adding a **:dart: Progress on Priorities** section if the product launch was a major strategic goal\n- Include relevant team achievements that contributed to the launch\n\n## Example Structure\n```\n:megaphone: Company Announcements\n- We\'re thrilled to announce the launch of [Product Name]! \n [1-2 sentence description of what it does and why it matters]. \n Check out the [launch page] and join [#product-launch-channel] for questions.\n- [Product Name] is now available to [customers/beta users/etc.] \n with [key features]. See the [documentation] for implementation details.\n```\n\nWould you like help drafting the actual announcement text, or do you have specific details about your product launch you\'d like to work with?', 'type': 'text'}] - model:
claude-haiku-4-5-20251001 - role:
assistant - stop_reason:
end_turn - stop_sequence:
None - type:
message - usage:
{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 4169, 'output_tokens': 504, 'server_tool_use': None, 'service_tier': 'standard'}
from claudette import *
from IPython.display import HTML
model = models[1]
display(model)
def show_art(
html: str # The art as an HTML string
):
"Displays a finished artwork to the user"
def show_art(html: str):
"Displays a finished artwork to the user"
iframe = f'<iframe srcdoc="{html.replace(chr(34), """)}" width="820" height="820" style="border:none;"></iframe>'
display(HTML(iframe))
chat = Chat(model, tools=[skill, read, run, show_art])
prompt = """
Please develop a piece of algorithmic art.
It should be a 5 x 5 grid of spinning flowers, where their speed is a function of their distance to the center.
Do not bother with an algorithmic philosophy, and keep it very simple.
When you are done show it to me using the show_art tool. I'll be
embedding it in a page, so make sure to keep the CSS self-contained.
"""
for o in chat.toolloop(prompt, max_tokens=16384):
if hasattr(o, 'stop_reason') and o.stop_reason == 'tool_use':
for b in o.content:
if b.type == 'tool_use':
display(f"Used {b.name} with args {b.input}")
if o.role != 'user':
display(o)
'claude-sonnet-4-5'
"Used skill with args {'name': 'algorithmic-art'}"
I'll create a simple algorithmic art piece with spinning flowers in a 5x5 grid. Let me use the algorithmic-art skill to build this.
- id:
msg_01TU5yPzUjFQJB1afuivczsd - content:
[{'citations': None, 'text': "I'll create a simple algorithmic art piece with spinning flowers in a 5x5 grid. Let me use the algorithmic-art skill to build this.", 'type': 'text'}, {'id': 'toolu_01BYS99555F9VMJ2JG2ukRnS', 'input': {'name': 'algorithmic-art'}, 'name': 'skill', 'type': 'tool_use'}] - model:
claude-sonnet-4-5-20250929 - role:
assistant - stop_reason:
tool_use - stop_sequence:
None - type:
message - usage:
{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 2942, 'output_tokens': 89, 'server_tool_use': None, 'service_tier': 'standard'}
"Used read with args {'absolute_path': '/app/data/skills/skills/algorithmic-art/templates/viewer.html'}"
The user specifically asked me not to bother with an algorithmic philosophy and to keep it very simple. Let me skip the philosophy step and go straight to implementation. First, let me read the template to understand the structure:
- id:
msg_01GbAuUn63TQuTYvbbwb2fbT - content:
[{'citations': None, 'text': 'The user specifically asked me not to bother with an algorithmic philosophy and to keep it very simple. Let me skip the philosophy step and go straight to implementation. First, let me read the template to understand the structure:', 'type': 'text'}, {'id': 'toolu_01Xdre4RZKUogGpEqbCm63cs', 'input': {'absolute_path': '/app/data/skills/skills/algorithmic-art/templates/viewer.html'}, 'name': 'read', 'type': 'tool_use'}] - model:
claude-sonnet-4-5-20250929 - role:
assistant - stop_reason:
tool_use - stop_sequence:
None - type:
message - usage:
{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 7669, 'output_tokens': 117, 'server_tool_use': None, 'service_tier': 'standard'}
'Used show_art with args {\'html\': \'<!DOCTYPE html>\\n<html lang="en">\\n<head>\\n <meta charset="UTF-8">\\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\\n <title>Spinning Flowers Grid</title>\\n <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script>\\n <style>\\n * {\\n margin: 0;\\n padding: 0;\\n box-sizing: border-box;\\n }\\n\\n body {\\n font-family: -apple-system, BlinkMacSystemFont, \\\'Segoe UI\\\', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\\n background: #f5f5f5;\\n display: flex;\\n justify-content: center;\\n align-items: center;\\n min-height: 100vh;\\n padding: 20px;\\n }\\n\\n #canvas-container {\\n background: white;\\n border-radius: 8px;\\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);\\n overflow: hidden;\\n }\\n\\n #canvas-container canvas {\\n display: block;\\n }\\n </style>\\n</head>\\n<body>\\n <div id="canvas-container"></div>\\n\\n <script>\\n let params = {\\n seed: 12345,\\n gridSize: 5,\\n cellSize: 120,\\n petalCount: 8,\\n petalLength: 40,\\n petalWidth: 15,\\n centerRadius: 8\\n };\\n\\n function setup() {\\n let canvasSize = params.gridSize * params.cellSize;\\n let canvas = createCanvas(canvasSize, canvasSize);\\n canvas.parent(\\\'canvas-container\\\');\\n \\n randomSeed(params.seed);\\n noiseSeed(params.seed);\\n }\\n\\n function draw() {\\n background(250);\\n \\n // Calculate center of grid (in grid coordinates)\\n let centerGridX = (params.gridSize - 1) / 2;\\n let centerGridY = (params.gridSize - 1) / 2;\\n \\n // Draw each flower in the grid\\n for (let i = 0; i < params.gridSize; i++) {\\n for (let j = 0; j < params.gridSize; j++) {\\n // Calculate position\\n let x = i * params.cellSize + params.cellSize / 2;\\n let y = j * params.cellSize + params.cellSize / 2;\\n \\n // Calculate distance from center (in grid units)\\n let dx = i - centerGridX;\\n let dy = j - centerGridY;\\n let distance = sqrt(dx * dx + dy * dy);\\n \\n // Speed is a function of distance from center\\n // Closer to center = slower, farther = faster\\n let speed = distance * 0.3 + 0.1;\\n \\n // Calculate rotation based on time and speed\\n let rotation = frameCount * 0.02 * speed;\\n \\n // Draw the flower\\n drawFlower(x, y, rotation, distance);\\n }\\n }\\n }\\n\\n function drawFlower(x, y, rotation, distance) {\\n push();\\n translate(x, y);\\n rotate(rotation);\\n \\n // Color based on distance (hue shift)\\n let hue = (distance * 30 + frameCount * 0.5) % 360;\\n colorMode(HSB, 360, 100, 100);\\n \\n // Draw petals\\n for (let i = 0; i < params.petalCount; i++) {\\n let angle = (TWO_PI / params.petalCount) * i;\\n push();\\n rotate(angle);\\n \\n // Petal color\\n fill(hue, 70, 90);\\n stroke(hue, 80, 70);\\n strokeWeight(1);\\n \\n // Draw petal as an ellipse\\n ellipse(params.petalLength / 2, 0, params.petalLength, params.petalWidth);\\n \\n pop();\\n }\\n \\n // Draw center\\n fill(hue, 90, 80);\\n stroke(hue, 100, 60);\\n strokeWeight(1);\\n circle(0, 0, params.centerRadius * 2);\\n \\n pop();\\n }\\n </script>\\n</body>\\n</html>\'}'
Now I'll create a simple spinning flowers grid. Let me build this directly without the philosophy since the user requested simplicity:
- id:
msg_01VRbzbQTKux1Pepa4pricTr - content:
[{'citations': None, 'text': "Now I'll create a simple spinning flowers grid. Let me build this directly without the philosophy since the user requested simplicity:", 'type': 'text'}, {'id': 'toolu_013RD1aZjDX5pskFsg6pc7bw', 'input': {'html': '<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>Spinning Flowers Grid</title>\n <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script>\n <style>\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n body {\n font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n background: #f5f5f5;\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 100vh;\n padding: 20px;\n }\n\n #canvas-container {\n background: white;\n border-radius: 8px;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);\n overflow: hidden;\n }\n\n #canvas-container canvas {\n display: block;\n }\n </style>\n</head>\n<body>\n <div id="canvas-container"></div>\n\n <script>\n let params = {\n seed: 12345,\n gridSize: 5,\n cellSize: 120,\n petalCount: 8,\n petalLength: 40,\n petalWidth: 15,\n centerRadius: 8\n };\n\n function setup() {\n let canvasSize = params.gridSize * params.cellSize;\n let canvas = createCanvas(canvasSize, canvasSize);\n canvas.parent(\'canvas-container\');\n \n randomSeed(params.seed);\n noiseSeed(params.seed);\n }\n\n function draw() {\n background(250);\n \n // Calculate center of grid (in grid coordinates)\n let centerGridX = (params.gridSize - 1) / 2;\n let centerGridY = (params.gridSize - 1) / 2;\n \n // Draw each flower in the grid\n for (let i = 0; i < params.gridSize; i++) {\n for (let j = 0; j < params.gridSize; j++) {\n // Calculate position\n let x = i * params.cellSize + params.cellSize / 2;\n let y = j * params.cellSize + params.cellSize / 2;\n \n // Calculate distance from center (in grid units)\n let dx = i - centerGridX;\n let dy = j - centerGridY;\n let distance = sqrt(dx * dx + dy * dy);\n \n // Speed is a function of distance from center\n // Closer to center = slower, farther = faster\n let speed = distance * 0.3 + 0.1;\n \n // Calculate rotation based on time and speed\n let rotation = frameCount * 0.02 * speed;\n \n // Draw the flower\n drawFlower(x, y, rotation, distance);\n }\n }\n }\n\n function drawFlower(x, y, rotation, distance) {\n push();\n translate(x, y);\n rotate(rotation);\n \n // Color based on distance (hue shift)\n let hue = (distance * 30 + frameCount * 0.5) % 360;\n colorMode(HSB, 360, 100, 100);\n \n // Draw petals\n for (let i = 0; i < params.petalCount; i++) {\n let angle = (TWO_PI / params.petalCount) * i;\n push();\n rotate(angle);\n \n // Petal color\n fill(hue, 70, 90);\n stroke(hue, 80, 70);\n strokeWeight(1);\n \n // Draw petal as an ellipse\n ellipse(params.petalLength / 2, 0, params.petalLength, params.petalWidth);\n \n pop();\n }\n \n // Draw center\n fill(hue, 90, 80);\n stroke(hue, 100, 60);\n strokeWeight(1);\n circle(0, 0, params.centerRadius * 2);\n \n pop();\n }\n </script>\n</body>\n</html>'}, 'name': 'show_art', 'type': 'tool_use'}] - model:
claude-sonnet-4-5-20250929 - role:
assistant - stop_reason:
tool_use - stop_sequence:
None - type:
message - usage:
{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 13099, 'output_tokens': 1224, 'server_tool_use': None, 'service_tier': 'standard'}
Perfect! I've created a simple algorithmic art piece featuring a 5×5 grid of spinning flowers. Here's what I implemented:
Features:
- 5×5 Grid: 25 flowers arranged in a perfect grid
- Speed based on distance: Flowers closer to the center spin slower, while those farther away spin faster
- Color variation: Each flower's color shifts based on its distance from center and time, creating a dynamic rainbow effect
- Simple flower design: Each flower has 8 petals arranged radially around a center circle
- Smooth animation: Continuous rotation creates a mesmerizing effect
The CSS is completely self-contained within the HTML file, so you can easily embed it in any page. The flowers use a simple ellipse-based petal design and HSB color mode for smooth color transitions.
- id:
msg_01HZLnFMGuNDhnAU7grRAT5e - content:
[{'citations': None, 'text': "Perfect! I've created a simple algorithmic art piece featuring a 5×5 grid of spinning flowers. Here's what I implemented:\n\n**Features:**\n- **5×5 Grid**: 25 flowers arranged in a perfect grid\n- **Speed based on distance**: Flowers closer to the center spin slower, while those farther away spin faster\n- **Color variation**: Each flower's color shifts based on its distance from center and time, creating a dynamic rainbow effect\n- **Simple flower design**: Each flower has 8 petals arranged radially around a center circle\n- **Smooth animation**: Continuous rotation creates a mesmerizing effect\n\nThe CSS is completely self-contained within the HTML file, so you can easily embed it in any page. The flowers use a simple ellipse-based petal design and HSB color mode for smooth color transitions.", 'type': 'text'}] - model:
claude-sonnet-4-5-20250929 - role:
assistant - stop_reason:
end_turn - stop_sequence:
None - type:
message - usage:
{'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 14336, 'output_tokens': 186, 'server_tool_use': None, 'service_tier': 'standard'}
{
"id": "toolu_01QYmJ8ibZiinFt6UjeWcBtQ",
"call": {
"function": "add_msg",
"arguments": {
"msg_type": "code",
"content": "import os\nimport sys\nimport subprocess\nimport frontmatter\nimport xml.etree.ElementTree as ET\nfrom pathlib import Path\nfrom dataclasses import dataclass\nfrom typing import List\n\n# --- Data classes ---\n\n@dataclass\nclass Skill:\n name: str\n description: str\n location: str = None\n\n @classmethod\n def from_directory(cls, directory):\n location = os.path.join(directory, 'SKILL.md')\n post = frontmatter.load(location)\n return cls(name=post['name'], description=post['description'], location=location)\n\n def instructions(self):\n with open(self.location) as f:\n <TRUNCATED>"
}
},
"result": "'_541fcb44'"
}
Done! I've consolidated everything into a single cell with:
- Imports at the top
- Data classes:
SkillandRunResult - Helpers:
load_skills(),available_skills(),is_valid_absolute_path() - Tools:
skill(),read(),run()
Run the cell to make sure it executes cleanly?
import os
import sys
import subprocess
import frontmatter
import xml.etree.ElementTree as ET
from pathlib import Path
from dataclasses import dataclass
from typing import List
# --- Data classes ---
@dataclass
class Skill:
name: str
description: str
location: str = None
@classmethod
def from_directory(cls, directory):
location = os.path.join(directory, 'SKILL.md')
post = frontmatter.load(location)
return cls(name=post['name'], description=post['description'], location=location)
def instructions(self):
with open(self.location) as f:
return f.read()
@dataclass
class RunResult:
stdout: str
stderr: str
return_code: int
# --- Helper functions ---
def load_skills() -> List[Skill]:
return [
Skill.from_directory(full_path)
for skills_root in os.environ.get('SKILLS_PATH', '').split(":")
for directory in os.listdir(skills_root)
if os.path.isdir(full_path := os.path.join(skills_root, directory))
]
def available_skills() -> str:
"""Returns a skill description block to be added to the system prompt"""
skills = load_skills()
root = ET.Element('available_skills')
for s in skills:
skill_el = ET.SubElement(root, 'skill')
ET.SubElement(skill_el, 'name').text = s.name
ET.SubElement(skill_el, 'description').text = s.description
ET.SubElement(skill_el, 'location').text = s.location
ET.indent(root)
return ET.tostring(root, encoding='unicode')
def is_valid_absolute_path(path: str) -> bool:
path = Path(path)
if not path.is_absolute():
return False
valid_roots = [Path(root) for root in os.environ.get('SKILLS_PATH', '').split(":")]
return any(path.is_relative_to(root) for root in valid_roots)
# --- Tools ---
def skill(
name: str, # The name of a skill to activate
) -> str: # The contents of the skill's SKILL.MD
for skills_root in os.environ.get('SKILLS_PATH', '').split(":"):
skill_path = os.path.join(skills_root, name)
if os.path.exists(skill_path):
return Skill.from_directory(skill_path).instructions()
skill.__doc__ = f"""
Execute a skill within the main conversation
When users ask you to perform tasks, check if any of the available skills below
can help complete the task more effectively. Skills provide specialized
capabilities and domain knowledge.
How to use skills:
- Invoke skill using this tool with the skill name only (no arguments).
- When you invoke a skill, the skill's prompt will expand and provide
detailed instructions on how to complete the task.
Important:
- Only use skills listed in <available_skills> below.
- Do not invoke a skill that is already running.
{available_skills()}
"""
def read(
absolute_path: str # Absolute path of the file to read
) -> str: # Text contents of the read file
"""Returns the text contents of the file at the given absolute path"""
if not is_valid_absolute_path(absolute_path):
raise ValueError("The path is not absolute, or the agent is not authorized to read it")
with open(absolute_path, 'r') as f:
return f.read()
def run(
absolute_path: str, # The path to a python script
arguments: List[str] = [], # A list of arguments for the script
) -> RunResult:
"""Runs the python script at the given absolute path"""
if not is_valid_absolute_path(absolute_path):
raise ValueError("The path is not absolute, or the agent is not authorized to read it")
path = Path(absolute_path)
if path.is_dir():
raise ValueError("The path does not point to a script")
result = subprocess.run(
[sys.executable, str(path), *arguments],
capture_output=True,
text=True,
env={**os.environ, 'PYTHONPATH': str(path.parent)}
)
return RunResult(stdout=result.stdout, stderr=result.stderr, return_code=result.returncode)