Engineering notes · 8 min read
lords-taste: an allowlist, a hash, and a validator
How three short Python scripts keep a 57-pack archive of visual Codex skills honest: an explicit allowlist, a per-skill tree digest, and a validator that re-derives everything.
Read the source ↗
My local Codex skills directory had grown into a flat pile. Motion recipes, generative art workflows, diagram helpers, and video editing packs sat next to finance spreadsheets and cold-outreach templates. I wanted the visual ones in a separate repository I could browse and version. Copying folders takes a minute. The harder part is keeping the copy trustworthy. I need to know exactly which packs are in it, that nothing private came along, and that no copied file has quietly changed since the catalog was written. lords-taste is my answer to that, and nearly all of the answer is three Python files under scripts/.
The problem
A copied archive can go wrong in a few quiet ways, and none of them announce themselves:
- A pack gets added or dropped, and the index doesn’t say so.
- Generated junk comes along:
__pycache__,.DS_Store, lock files, a virtualenv. - Local state leaks. One of my video packs keeps sync metadata and evidence files that describe my own media library.
- A file is edited in the archive or in the source, and the two drift apart.
I wanted every one of those to fail a command, not depend on me remembering to look.
Architecture
The repository is 57 skill packs in five category folders, skills/<category>/<skill-name>/, 281 files in all. Three scripts sit beside them:
catalog_config.pyholds the allowlist and the exclusion rules. It is the only file I edit by hand.build_catalog.pywalks the allowlisted folders and writescatalog.json.validate_catalog.pyreadscatalog.json, re-derives everything from the tree, and exits non-zero on any mismatch.
skills/ · 5 drawers · 57 packs
frontend-motion12
- 60fps-animation
- accessible-animation
- glassmorphism
- gsap-web
- lottie-animation
- micro-interaction
- motion-background
- page-transition-animation
- particle-system
- shader-glsl
- svg-animation
- threejs-animation
generative-art6
- ascii-animation
- hatch-pet
- oiljs
- p5js
- procedural-doodle-characters
- python-procedural-art
visual-design12
- animated-infographic
- animation-principles
- aphrodite-blog-visuals
- brand-motion-guidelines
- chart-animation
- color-motion
- diagram-animation
- isometric-animation
- manim
- map-animation
- motion-art-direction
- shot-composition
image-assets1
- gemini-image-generation
video-production26
- ad-creative-video
- after-effects
- audiogram
- beat-sync-editing
- caption-animation
- countdown-video
- explainer-video
- faceless-marketing-skill
- kinetic-typography
- launch-video
- logo-animation
- lower-thirds
- photo-slideshow
- presentation-video
- product-demo-video
- promo-video
- recreate-gym-reel-edits
- remotion-video
- short-form-video
- testimonial-video
- text-message-animation
- video-delivery-specs
- vox-faceless-marketing-reel-edit
- whiteboard-animation
- wrapped-video
- youtube-intro-outro
There are no dependencies beyond the Python standard library: argparse, hashlib, json, and pathlib.
Decision: an explicit allowlist, not a filter
A rule like “copy anything that looks visual” would drift. What I wrote instead is a literal dictionary of category to skill names:
CATEGORIES = { "frontend-motion": [ "60fps-animation", "accessible-animation", "glassmorphism", # ... ], "image-assets": [ "gemini-image-generation", ], # ...}The packs I reviewed and left out are listed too, in EXCLUDED_SKILLS, and that list ends up in catalog.json as excluded_skill_packs. So the catalog records what I decided against as well as what I kept. Adding a pack means editing one list and rebuilding. That costs a little effort, and I think the effort is worth it: every pack in the archive got there because I named it.
Order matters too. Category order and skill order drive the order of catalog.json, so rebuilding an unchanged tree produces an identical file. A unit test pins the category order and the count of 57.
One skill lives in exactly one category. Some packs could fit two places. chart-animation is both design and motion. I still picked one folder rather than copying it twice, because two copies are two things to keep in sync.
Decision: the catalog is not an install directory
Codex expects skills directly under its skills root. The nested skills/<category>/ layout breaks that on purpose. This repository is for browsing and diffing. When I want to use a skill, I copy one leaf folder into my user-level skills directory. Nothing here installs or syncs automatically, and I would rather do a one-folder copy I can see than have a bulk sync rewrite my skills directory.
Implementation: one digest per skill
Each catalog entry carries files, bytes, and tree_sha256. The digest comes from one small function:
def file_digest(files): digest = hashlib.sha256() file_count = 0 byte_count = 0 for relative_path, path in files: content = path.read_bytes() digest.update(relative_path.as_posix().encode("utf-8")) digest.update(b"\0") digest.update(content) file_count += 1 byte_count += len(content) return file_count, byte_count, digest.hexdigest()- files
- 2
- bytes
- 64
- tree_sha256
- 432961269e29f2a0590c569131a270b175098fbc794d8bd94ebd428363bdc5ba
Three details do most of the work:
- The file list comes from
sorted(skill_root.rglob("*")), so the order is the same on every machine. - Paths go through
as_posix(), so the separator doesn’t change the hash across operating systems. - The path is hashed before the content. Renaming or moving a file changes the digest even if no bytes change.
The NUL separator keeps path and content apart in the stream. It isn’t a full framing scheme, since content can contain NUL bytes. For spotting drift in my own files, that is fine. It is not built to resist tampering.
The same walk, iter_skill_files, refuses symlinks outright and skips anything is_excluded matches. That function checks four things: exact source-relative paths, whole excluded directories, directory names like .git and __pycache__, and suffixes like .pyc and .lock. The build and the validator share it, so they can’t disagree about what counts as part of a skill.
Implementation: what the validator checks
validate(root, source=None) returns a list of error strings rather than stopping at the first failure. One run shows everything that is wrong. In order, it checks:
- Categories in
catalog.jsonequal theCATEGORIESdictionary exactly. - Catalog skill names match the allowlist with no gaps or duplicates.
- The set of folders that contain a
SKILL.mdequals the expectedcategory/nameset. - No file under
skills/is a symlink or an excluded path. - No file contains a private marker.
- Each skill’s
SKILL.mdname:matches its folder, and its recomputed(files, bytes, tree_sha256)equals the catalog entry.
The private-marker check is a plain byte search:
PRIVATE_MARKERS = ( b"/Users/", b"/Volumes/", b"lordpatil-air", # ...)It is blunt, and I chose blunt on purpose. Any skill that mentions a macOS home path, even as an example, fails. I would rather rewrite that line than keep an allowlist of acceptable leaks.
Passing --source adds a parity check. For each selected skill, the validator builds a {relative_path: bytes} map for the source copy and for the archive copy, both through the same exclusion rules, and compares the two dictionaries. A file that was added, removed, or edited on either side shows up as Source and copied files differ.
The unit tests are short. They check that the allowlist has 57 unique names, that the category order is stable, and that every configured exclusion is really excluded. They also run the validator as a subprocess against the current tree, adding --source when CODEX_SKILLS_SOURCE is set.
What was awkward
The metadata parser is the weakest part. skill_metadata scans only the first 30 lines of SKILL.md and takes whatever follows name: and description: on the same line. It is not a YAML parser. A folded multi-line description would be recorded as > or an empty string, and an empty one fails the build. All 57 current packs use one-line values, so this hasn’t bitten me, but it is a constraint on anything I add later.
The source check has a small wart. If a source skill folder is missing, rglob on a missing path yields nothing, so the comparison fails first and the explicit Missing source skill check fires after it. Both lines are true, but a missing folder produces two errors where one would do.
The recreate-gym-reel-edits pack took the most care. Its sync metadata, source catalog, learned patterns, saved reel formats, and an evidence/ directory all describe my own media setup. I list those paths one by one in EXCLUDED_SOURCE_RELATIVE and EXCLUDED_SOURCE_DIRECTORIES, and a test asserts each is really excluded. Listing them explicitly felt clumsy, but a pattern broad enough to catch them would also catch real reference files.
Licensing isn’t something code can check. PROVENANCE.md notes the Apache 2.0 notice in hatch-pet, the OFL notice next to the Yatra One font in the gym-edit pack, and that oiljs points at CC BY-NC 4.0 upstream code. The validator doesn’t read any of that. It keeps files byte-identical, which keeps each notice next to the file it covers, and that is as far as it goes.
What’s next
- Replace the line scanner in
skill_metadatawith a real frontmatter parse, so multi-line descriptions work. - Collapse the double error for a missing source skill into one.
- Run the build and validator in CI, so a stale
catalog.jsoncan’t be merged.
The whole thing is about 400 lines of Python. It doesn’t judge whether a skill is any good. It makes sure the archive holds what I said it holds, and nothing else.