1. Summary
Small tools - focused utility scripts - are the easiest entry point for Claude Code-assisted
development. A script that renames files, parses a log, or generates a report can often be built
in a single well-formed prompt with no md file at all. When you build similar tools repeatedly
against the same codebase, a pytools.md file encodes the shared context so you don't restate
it each session.
Two modes:
- Direct prompt - describe everything inline. Best for one-off tasks where the
full context fits in a single message.
- pytools.md - load shared conventions at session start, then state only what
differs for this specific tool.
2. Direct Prompting
When the task is self-contained and you only need it once, describe everything inline. A good
direct prompt for a utility script has four parts: what the script reads, what it produces,
hard constraints, and error behavior.
Write a Python script that walks a directory tree starting at a given path,
finds all .html files whose filenames contain "AIBites", and prints their
relative paths to stdout. Accept the root path as a command-line argument.
No dependencies beyond the standard library. If the path doesn't exist,
print an error to stderr and exit with code 1.
Every element in that prompt prevents a common wrong assumption:
| Element | Without it, Claude might... |
| root path as argument | hardcode a path or use cwd |
| relative paths | print absolute paths |
| stdlib only | import pathlib + click + colorama |
| error behavior | raise an unhandled exception |
3. The pytools.md Context File
When you build tools targeting the same codebase repeatedly, a pytools.md captures the shared
context once. Directory layout, file conventions, output format, and hard constraints go in the
file. Each session prompt then states only what is specific to that tool.
pytools.md Structure:
# pytools context
## Codebase
Root: [project root path]
[file type]: [directories]
[file type]: [directories]
## Tool Conventions
Language: [language and version]
Output: [output format and destination]
Errors: [error handling behavior]
Paths: [path format - relative/absolute, from where]
## What to Avoid
[constraint]
[constraint]
[constraint]
4. Using pytools.md
Load it at session start, then state only the specific task. Because the shared constraints are
already in the md file, the prompt focuses on what differs.
Read pytools.md.
Write a script that finds all HTML files in the site that reference a given
CSS file via a <link rel="stylesheet"> tag. Accept the CSS filename as a
command-line argument. Print matching HTML files as relative paths.
The prompt says nothing about paths, output format, error handling, or language - pytools.md
already covers those. The prompt states only the search logic and the argument.
Session management: continue the same session when iterating on a single script. Start a fresh
session (with /clear, then reload pytools.md) when switching to a different tool - the clean
context prevents the previous script's specifics from bleeding into the new one.
5. More Example Prompts
Find unreferenced CSS files:
Read pytools.md.
Write a script that scans all HTML files in the site and collects every CSS
filename referenced via <link rel="stylesheet">. Then check which files in
css/ are never referenced by any HTML file. Print the unreferenced filenames.
Inventory files by type and size:
Read pytools.md.
Write a script that walks the Code/ directory and prints a tab-separated list:
filename, extension, size in bytes. One file per line. Skip directories named
obj, bin, or __pycache__.
Count nav links in an explorer page:
Read pytools.md.
Write a script that reads ExploreCode.html and counts how many <a href> elements
appear inside the element with id="nav". Print the count and each href found.
6. Case Study: check_unused_css2.py
check_unused_css2.py is a tool built for this site using the pytools.md workflow.
It reports which CSS stylesheets loaded by an HTML page contribute no matching selectors - either
because the page doesn't use any of the classes or IDs the stylesheet defines, or because the file
is missing entirely. The prompts below produce it from scratch; the usage and run example use
this generated version.
pytools.md:
# pytools context
## Codebase
Root: c:\github\JimFawcett\NewSite
HTML files: Code/, Rust/, Cpp/, CSharp/, Python/, SWDev/, WebDev/
JS files: js/ (site-wide) or Code/js/ (code-track-specific)
CSS files: css/
## Tool Conventions
Language: Python 3, stdlib only unless the task requires otherwise
Output: print to stdout, one result per line
Errors: print to stderr, continue processing remaining items
Paths: print as relative paths from the root
## What to Avoid
No hardcoded absolute paths
No pip dependencies unless explicitly requested
No interactive prompts - all configuration via command-line arguments
The initial prompt:
Read pytools.md.
Write a script that reports which CSS stylesheets loaded by an HTML page are
unused - meaning none of their selectors match any element on the page.
Accept one or more HTML files or directory paths as arguments. For each page,
list every linked stylesheet and classify it as:
ok - at least one selector matches an element on the page
partial - some selectors match but not all; show count as N/M
UNUSED - no selector matches any element
MISSING - the CSS file doesn't exist on disk
A selector "matches" if every class, ID, and custom element name it references
exists somewhere in the page. Standard HTML element names (div, span, p, etc.)
don't count - they're nearly always present and produce false positives if used.
Add a --detail flag to also print the non-matching selectors for partial results.
This task needs tinycss2 and beautifulsoup4 - those are acceptable here.
Two things distinguish this from the simpler examples in section 5. First, the prompt explicitly
overrides the stdlib-only default from pytools.md ("those are acceptable here") - the task genuinely
requires a CSS parser and an HTML parser, and stating the override in the prompt is cleaner than
editing pytools.md for a single exception. Second, the match strategy is stated precisely up front:
ignore standard HTML element names, require every class/ID/custom element in the selector to exist
on the page. Without that precision, the model would have made its own call and likely produced
false positives.
The script grew through one follow-up iteration. After the first version ran, some "UNUSED" results
were found to be stylesheets whose selector names appeared in dynamically generated markup from
JavaScript. A second prompt added JS reference detection:
When a stylesheet is classified UNUSED, check whether any of its selector names
(class names, IDs, custom element names) appear as string literals in the page's
inline scripts or linked JS files. If they do, append [JS: name1, name2] to the
UNUSED line so it's visible but not a false alarm.
That is the normal shape of a small-tool session: one prompt for the core behavior, one follow-up
for the edge case discovered on first run, done. The pytools.md context meant neither prompt needed
to restate paths, output format, or error handling.
Usage:
python check_unused_css2.py <page.html|dir> [<page.html|dir> ...] [--detail]
page.html one or more HTML files to check
dir expand to all .html files in that directory (non-recursive)
--detail for each partial result, list each non-matching selector,
annotated with [JS: name] if that selector name appears in
the page's inline or linked JavaScript
(UNUSED lines always show [JS: ...] regardless of --detail)
Running it on this page:
python check_unused_css2.py AIWorkflows_SmallTools.html
AIWorkflows_SmallTools.html (12 stylesheets)
----------------------------------------------------------------
partial 29/35 reset.css
partial 11/20 ContentMenus.css
partial 40/138 Content.css
partial 10/11 help.css
partial 2/5 ThemeCode.css
UNUSED StylesPhoto.css
UNUSED StylesSizerComp.css [JS: lightElem]
UNUSED StylesWebComponents.css [JS: code-block, hidephotosizer-block, photo-block, photosizer-block]
UNUSED FigureSizer.css [JS: caption, content, figure-sizer]
UNUSED link-nav.css [JS: active, controls, link-container]
partial 10/12 content-links.css
partial 6/58 prism.css
check_unused_css2.py
#!/usr/bin/env python3
# check_unused_css2.py -- reports CSS stylesheets with no matching selectors in an HTML page
# Generated from pytools.md + case study prompt
# Requires: pip install tinycss2 beautifulsoup4
# Usage: python check_unused_css2.py <page.html|dir> [<page.html|dir> ...] [--detail]
import sys
import re
from pathlib import Path
import tinycss2
from bs4 import BeautifulSoup
_PSEUDO = re.compile(r'::?[\w-]+(?:\([^)]*\))?')
_CLASS = re.compile(r'\.([\w-]+)')
_ID_SEL = re.compile(r'#([\w-]+)')
_CUSTOM_ELEM = re.compile(r'\b([a-z][\w]*-[\w-]+)\b')
SKIP_AT = {'keyframes', '-webkit-keyframes', 'font-face', 'charset', 'import'}
def parse_selectors(rules):
"""Recursively collect all selectors from a CSS rule list."""
selectors = []
for rule in rules:
if rule.type == 'qualified-rule':
text = tinycss2.serialize(rule.prelude).strip()
for part in text.split(','):
part = part.strip()
if part:
selectors.append(part)
elif (rule.type == 'at-rule'
and rule.at_keyword.lower() not in SKIP_AT
and rule.content):
inner = tinycss2.parse_rule_list(
rule.content, skip_whitespace=True, skip_comments=True
)
selectors.extend(parse_selectors(inner))
return selectors
def scan_page(html_text):
"""Return (tags, classes, ids) present in the page."""
soup = BeautifulSoup(html_text, 'html.parser')
tags = {el.name for el in soup.find_all(True)}
classes = set()
for el in soup.find_all(True):
classes.update(el.get('class', []))
ids = {el['id'] for el in soup.find_all(id=True)}
return tags, classes, ids
def collect_js_text(html_text, html_path):
"""Return concatenated text of all inline and linked scripts for the page."""
soup = BeautifulSoup(html_text, 'html.parser')
parts = []
for tag in soup.find_all('script'):
src = tag.get('src')
if src:
js_path = (html_path.parent / src).resolve()
if js_path.exists():
try:
parts.append(js_path.read_text(encoding='utf-8-sig'))
except Exception:
pass
elif tag.string:
parts.append(tag.string)
return '\n'.join(parts)
def token_names(selector):
"""Extract class names, IDs, and custom element names from a selector."""
clean = _PSEUDO.sub('', selector)
names = set()
names.update(_CLASS.findall(clean))
names.update(_ID_SEL.findall(clean))
names.update(_CUSTOM_ELEM.findall(clean))
return names
def matches(selector, tags, classes, ids):
"""Return True if every named token in the selector exists on the page."""
clean = _PSEUDO.sub('', selector).strip()
if not clean:
return False
for cls in _CLASS.findall(clean):
if cls not in classes:
return False
for id_ in _ID_SEL.findall(clean):
if id_ not in ids:
return False
for elem in _CUSTOM_ELEM.findall(clean):
if elem not in tags:
return False
return True
def js_hits(names, js_text):
"""Return names that appear as substrings in js_text."""
return sorted(n for n in names if n in js_text)
def check_page(html_path, detail):
html_text = html_path.read_text(encoding='utf-8')
tags, classes, ids = scan_page(html_text)
js_text = collect_js_text(html_text, html_path)
soup = BeautifulSoup(html_text, 'html.parser')
links = [lnk for lnk in soup.find_all('link', rel='stylesheet') if lnk.get('href')]
stylesheets = [
(lnk['href'], (html_path.parent / lnk['href']).resolve())
for lnk in links
]
print(f"\n{html_path.name} ({len(stylesheets)} stylesheets)")
print('-' * 64)
for href, css_path in stylesheets:
name = css_path.name
if not css_path.exists():
print(f" MISSING {name} ({href})")
continue
css_text = css_path.read_text(encoding='utf-8-sig')
selectors = parse_selectors(
tinycss2.parse_stylesheet(css_text, skip_whitespace=True, skip_comments=True)
)
if not selectors:
print(f" ok {name} (no selectors)")
continue
hits = [s for s in selectors if matches(s, tags, classes, ids)]
misses = [s for s in selectors if not matches(s, tags, classes, ids)]
if not hits:
all_names = set()
for s in selectors:
all_names.update(token_names(s))
refs = js_hits(all_names, js_text)
suffix = f" [JS: {', '.join(refs)}]" if refs else ""
print(f" UNUSED {name}{suffix}")
elif misses:
print(f" partial {len(hits):2}/{len(selectors):<2} {name}")
if detail:
for m in misses:
refs = js_hits(token_names(m), js_text)
js_note = f" [JS: {', '.join(refs)}]" if refs else ""
print(f" no match: {m}{js_note}")
else:
print(f" ok {name}")
def main():
sys.stdout.reconfigure(encoding='utf-8')
args = sys.argv[1:]
detail = '--detail' in args
paths = [a for a in args if not a.startswith('--')]
if not paths:
print("Usage: python check_unused_css2.py <page.html|dir> [<page.html|dir> ...] [--detail]")
sys.exit(1)
for p in paths:
target = Path(p).resolve()
if target.is_dir():
html_files = sorted(target.glob('*.html'))
if not html_files:
print(f"\nNo .html files found in {target}", file=sys.stderr)
for f in html_files:
check_page(f, detail)
else:
check_page(target, detail)
if __name__ == '__main__':
main()
Four of the five UNUSED entries carry [JS: ...] annotations - their selector names
are referenced by JavaScript that generates markup dynamically, so they are not dead weight.
Only StylesPhoto.css is fully unused with no JS refs. All five are standard includes carried
by every page in this site, so removing StylesPhoto from this one page would require per-page
CSS management that costs more than the bandwidth saved. The output is useful precisely because
it surfaces that distinction: a stylesheet with no page references and no JS refs is a genuine
candidate for removal; one with JS refs is not.