AI Workflows: Small Tools

building utility scripts with direct prompts or a pytools.md context

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:

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:
ElementWithout it, Claude might...
root path as argumenthardcode a path or use cwd
relative pathsprint absolute paths
stdlib onlyimport pathlib + click + colorama
error behaviorraise 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.